Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First Draft of HAL Smart Form Component #82

Open
wants to merge 14 commits into
base: feature/HalForm
Choose a base branch
from
49 changes: 49 additions & 0 deletions lib/src/components/HalForm/HalForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { DxcAlert, DxcFlex } from "@dxc-technology/halstack-react";
import React, { ReactNode } from "react";

import useHalFormChildrenProps from "./useHalFormChildrenProps";

interface HalFormProps {
children: ReactNode;
apiEndpoint: string;
authHeaders?: Record<string, string>;
onSubmit?: (formState: Record<string, string>, onlyUpdatedFields: Record<string, string>) => void;
selfManagedSave?: boolean;
}

const HalForm: React.FC<HalFormProps> = ({
children,
apiEndpoint,
authHeaders,
onSubmit,
selfManagedSave,
}) => {
const { processChildren, formState, onlyUpdatedFields, apiUpdateError } = useHalFormChildrenProps(
children,
apiEndpoint,
authHeaders,
selfManagedSave
);
const handleSubmit = async (e: React.FormEvent) => {
/**
* For future set up of action buttons
*/
e.preventDefault();
if (onSubmit) {
onSubmit(formState, onlyUpdatedFields);
}
};

return (
<DxcFlex gap="2rem" direction="column">
{apiUpdateError?.body?.messages?.map((err: any) => (
<DxcFlex gap="2rem" direction="column">
<DxcAlert type="error" size="fillParent" inlineText={err.message? `${err.context}: ${err.message}` : err} />
</DxcFlex>
))}
<form onSubmit={handleSubmit}>{processChildren(children)}</form>
</DxcFlex>
);
};

export default React.memo(HalForm);
218 changes: 218 additions & 0 deletions lib/src/components/HalForm/HalOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/* eslint-disable no-prototype-builtins */

// on GET, we have _options, on OPTIONS, we got the option detail directly on response
const getOptionFromResponse: (response: any) => { links: any[]; title: string; properties: any; required: string[] } = (
response
) => (response?.hasOwnProperty('_options') ? response['_options'] : response);

type MethodType = 'GET' | 'PATCH' | 'DELETE' | 'POST' | string;

interface MethodInterface {
href: string;
mediaType: string;
method: MethodType;
schema?: any;
rel: string;
title: string;
}

type PropertyType =
| string
| string[]
| number
| number[]
| boolean
| undefined
| null;

interface OptionsProperty {

/**
*
* Return true if the property is patchable
*/
canPatch: () => boolean;
visible: boolean;
minLength: number;
maxLength: number;
min: number;
max: number;

type: string;

/**
*
*/
isRequired: boolean;

/**
* If the property is a complex list
*/
isOneOf: boolean;

/**
* Return the list of values of the oneOf property
*/
getValuesOfOneOf: <T = { value: string; label: string }>() => T[];

/**
* Return the label for a given value
* @param value - value of the property
* @returns -
*/
getLabelOfOneOf: (value: PropertyType) => string;
};

export interface OnOptionsReturn {

/**
* Check if the method is allow
* @param method - method type
* @returns true if its allowed
*/
canMethod: (method: MethodType) => boolean;

/**
* Check if the POST method is allow
* @returns true if its allowed
*/
canPost: () => boolean;

/**
* Check if the DELETE method is allow
* @returns true if its allowed
*/
canDelete: () => boolean;

/**
* Check if the PATCH method is allow
* @returns true if its allowed
*/
canPatch: () => boolean;

/**
* Get the method detail of a rel
* @param rel - rel to check, most of the time it's the inquery
* @returns method detail
*/
getMethodByRel: (rel: string) => MethodInterface;

/**
* Get the options for a property or a sub property of a complexlist
* @param property - property name
* @param subProperty - sub property name for complexlist only
* @returns property options
*/
getProperty: (property: string, subProperty?: string) => OptionsProperty;
}

export const onOptions = (response: any, forCollection = false): OnOptionsReturn => {
const options = getOptionFromResponse(response);



const getMethod = (method: MethodType): MethodInterface => options['links']?.find((item: any) => item.method === method);
const getMethodByRel = (rel: string): MethodInterface => options['links']?.find((item: any) => item.rel === rel);
const getMethodSchema = (method: MethodType): any => getMethod(method)?.schema;
const canMethod = (method: MethodType): boolean => !!getMethod(method);
const canPost = () => canMethod('POST');
const canPatch = () => canMethod('PATCH');
const canDelete = () => canMethod('DELETE');

const getProperty = (property: string, subProperty?: string): OptionsProperty => {
// subProperty in case of complex list
const targetProperty = subProperty ?? property;

// return the property object depends if the property is a complex list or not
// base can be schema or options
const getOptionOrSchemaProperties = (base: any) => {

/**
* For Normal/Root properties
*/
if (!subProperty) {
return base.properties;
}

/**
* For ComplexList or SimpleList properties
*/

if (!forCollection) {
return base.properties?.[property]?.items.items ?? {};
}

/**
* For Collection properties
*/
const baseProperties = base?.properties?._links?.properties?.item?.properties?.[property].properties;
return baseProperties?.oneOf?.[0] ?? baseProperties;
};

const propertiesOptions = getOptionOrSchemaProperties(options);

// complexList attributes are in [property].items.items
const canPatchProperty = () => {
if (!canPatch()) {
return false;
}

const schema = getMethodSchema('PATCH');
// Check is the property can be patchable
return schema ? getOptionOrSchemaProperties(schema).hasOwnProperty(targetProperty) : false;
};

const isRequired = !!(options?.required?.indexOf(property) >= 0);
const isVisible = propertiesOptions.hasOwnProperty(targetProperty);

// If it's not visible, that means this property doesn't exist, so we return an empty object
const propertyOption = isVisible ? propertiesOptions[targetProperty] : {};

const isOneOf: boolean = propertyOption.hasOwnProperty('oneOf') || propertyOption.enum?.length > 0;
const { minLength, maxLength, minimum, maximum, type } = propertyOption;
const getValuesOfOneOf = () => {
if (!isOneOf) {
return [];
}

if (propertyOption?.['oneOf']) {
return propertyOption?.['oneOf'].map(({ enum: [value], title }: { enum: any[]; title: string }) => ({
value,
label: title
}));
}
else {
return propertyOption?.['enum'].map((enumItem: any) => ({
value: enumItem,
label: enumItem.toString()
}));
}
};

// Get the label of a oneOf, if the value doesn't exist, it will return undefined
const getLabelOfOneOf = (givenValue: any) => getValuesOfOneOf().find(({ value }: { value: any }) => value === givenValue)?.label;

return {
canPatch: canPatchProperty,
visible: isVisible,
minLength,
maxLength,
min: minimum,
max: maximum,
type,
isRequired,
isOneOf,
getValuesOfOneOf,
getLabelOfOneOf
};
};

return {
canMethod,
canPost,
canDelete,
canPatch,
getMethodByRel,
getProperty
};
};
3 changes: 3 additions & 0 deletions lib/src/components/HalForm/HalStackClientModule.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// temp
// types must come from halstack-client library
declare module "@dxc-technology/halstack-client";
Loading
Loading