From 5f5e402db524abd97f3a4a425a238ee84cc931cb Mon Sep 17 00:00:00 2001 From: Bilal ABBAD Date: Wed, 17 Jul 2024 00:10:28 +0200 Subject: [PATCH] Improved typing, testing and file structure under getFormFieldsFromSchema + fixed filters on a profiles generic (#3854) - TS typing fixes and improvement on ObjectForm, getFormFieldsFromSchema and function used by them - Extracted ProfilesSelector component into its own file - Split form form utils.ts into multiple files for better readability and navigation - Add more tests on isFieldDisabled, getFieldDefaultValue, getFormFieldsFromSchema - Fixed filters on a generic when generic is a profile - Small renaming improvements - Removed dead code --- .../app/src/components/form/object-form.tsx | 135 +------- .../src/components/form/profiles-selector.tsx | 127 +++++++ .../form/utils/getFieldDefaultValue.ts | 55 +++ .../getFormFieldsFromSchema.ts} | 119 ++----- .../components/form/utils/isFieldDisabled.ts | 24 ++ .../edit-form-hook/dynamic-control-types.ts | 61 ---- frontend/app/src/utils/common.ts | 4 +- .../src/utils/formStructureForCreateEdit.ts | 321 ------------------ .../src/utils/getObjectItemDisplayValue.tsx | 4 +- .../app/src/utils/getSchemaObjectColumns.ts | 94 ----- .../app/tests/unit/isFieldDisabled.test.ts | 103 ++++++ ...e.test.ts => getFieldDefaultValue.test.ts} | 124 ++++++- .../utils/getFormFieldsFromSchema.test.ts | 310 +++++++---------- .../tests/unit/utils/getFormStructure.test.ts | 58 ---- 14 files changed, 590 insertions(+), 949 deletions(-) create mode 100644 frontend/app/src/components/form/profiles-selector.tsx create mode 100644 frontend/app/src/components/form/utils/getFieldDefaultValue.ts rename frontend/app/src/components/form/{utils.ts => utils/getFormFieldsFromSchema.ts} (57%) create mode 100644 frontend/app/src/components/form/utils/isFieldDisabled.ts delete mode 100644 frontend/app/src/utils/formStructureForCreateEdit.ts create mode 100644 frontend/app/tests/unit/isFieldDisabled.test.ts rename frontend/app/tests/unit/utils/{getObjectDefaultValue.test.ts => getFieldDefaultValue.test.ts} (68%) delete mode 100644 frontend/app/tests/unit/utils/getFormStructure.test.ts diff --git a/frontend/app/src/components/form/object-form.tsx b/frontend/app/src/components/form/object-form.tsx index b47f68f3e9..18b23e3c56 100644 --- a/frontend/app/src/components/form/object-form.tsx +++ b/frontend/app/src/components/form/object-form.tsx @@ -6,14 +6,11 @@ import { schemaState, } from "@/state/atoms/schema.atom"; import { useAtomValue } from "jotai/index"; -import { getFormFieldsFromSchema } from "./utils"; import { useId, useState } from "react"; -import { Combobox, MultiCombobox, tComboboxItem } from "@/components/ui/combobox"; +import { Combobox, tComboboxItem } from "@/components/ui/combobox"; import NoDataFound from "@/screens/errors/no-data-found"; import Label from "@/components/ui/label"; import { gql } from "@apollo/client"; -import LoadingScreen from "@/screens/loading-screen/loading-screen"; -import ErrorScreen from "@/screens/errors/error-screen"; import getMutationDetailsFromFormData from "@/utils/getMutationDetailsFromFormData"; import { createObject } from "@/graphql/mutations/objects/createObject"; import { stringifyWithoutQuotes } from "@/utils/string"; @@ -27,17 +24,16 @@ import DynamicForm, { DynamicFormProps } from "@/components/form/dynamic-form"; import { AttributeType } from "@/utils/getObjectItemDisplayValue"; import { useAuth } from "@/hooks/useAuth"; import useFilters from "@/hooks/useFilters"; -import useQuery from "@/hooks/useQuery"; -import { getProfiles } from "@/graphql/queries/objects/getProfiles"; -import { getObjectAttributes } from "@/utils/getSchemaObjectColumns"; +import { getFormFieldsFromSchema } from "@/components/form/utils/getFormFieldsFromSchema"; +import { ProfilesSelector } from "@/components/form/profiles-selector"; -type Profile = Record>; +export type ProfileData = Record>; interface ObjectFormProps extends Omit { kind: string; onSuccess?: (newObject: any) => void; currentObject?: Record; - currentProfiles?: Profile[]; + currentProfiles?: ProfileData[]; isFilterForm?: boolean; onSubmit?: (data: any) => Promise; } @@ -122,7 +118,7 @@ const NodeWithProfileForm = ({ kind, currentProfiles, ...props }: ObjectFormProp const generics = useAtomValue(genericsState); const profiles = useAtomValue(profilesAtom); - const [selectedProfiles, setSelectedProfiles] = useState(); + const [selectedProfiles, setSelectedProfiles] = useState(); const nodeSchema = [...nodes, ...generics, ...profiles].find((node) => node.kind === kind); @@ -132,7 +128,7 @@ const NodeWithProfileForm = ({ kind, currentProfiles, ...props }: ObjectFormProp return ( <> - {nodeSchema.generate_profile && ( + {"generate_profile" in nodeSchema && nodeSchema.generate_profile && ( void; - currentProfiles?: any[]; -}; - -const ProfilesSelector = ({ schema, value, defaultValue, onChange }: ProfilesSelectorProps) => { - const id = useId(); - - const generics = useAtomValue(genericsState); - const profiles = useAtomValue(profilesAtom); - - const nodeGenerics = schema?.inherit_from ?? []; - - // Get all available generic profiles - const nodeGenericsProfiles = nodeGenerics - // Find all generic schema - .map((nodeGeneric: any) => generics.find((generic) => generic.kind === nodeGeneric)) - // Filter for generate_profile ones - .filter((generic: any) => generic.generate_profile) - // Get only the kind - .map((generic: any) => generic.kind) - .filter(Boolean); - - // The profiles should include the current object profile + all generic profiles - const kindList = [schema.kind, ...nodeGenericsProfiles]; - - // Add attributes for each profiles to get the values in the form - const profilesList = kindList - .map((profile) => { - // Get the profile schema for the current kind - const profileSchema = profiles.find((profileSchema) => profileSchema.name === profile); - - // Get attributes for query + form data - const attributes = getObjectAttributes({ schema: profileSchema, forListView: true }); - - if (!attributes.length) return null; - - return { - name: profileSchema?.kind, - schema: profileSchema, - attributes, - }; - }) - .filter(Boolean); - - // Get all profiles name to retrieve the informations from the result - const profilesNameList: string[] = profilesList - .map((profile) => profile?.name ?? "") - .filter(Boolean); - - if (!profilesList.length) - return ; - - const queryString = getProfiles({ profiles: profilesList }); - - const query = gql` - ${queryString} - `; - - const { data, error, loading } = useQuery(query); - - if (loading) return ; - - if (error) return ; - - // Get data for each profile in the query result - const profilesData: any[] = profilesNameList.reduce( - (acc, profile) => [...acc, ...(data?.[profile!]?.edges ?? [])], - [] - ); - - // Build combobox options - const items = profilesData?.map((edge: any) => ({ - value: edge.node.id, - label: edge.node.display_label, - data: edge.node, - })); - - const selectedValues = value?.map((profile) => profile.id) ?? []; - - const handleChange = (newProfilesId: string[]) => { - const newSelectedProfiles = newProfilesId - .map((profileId) => items.find((option) => option.value === profileId)) - .filter(Boolean) - .map((option) => option?.data); - - onChange(newSelectedProfiles); - }; - - if (!profilesData || profilesData.length === 0) return null; - - if (!value && defaultValue) { - const ids = defaultValue.map((profile) => profile.id); - - handleChange(ids); - } - - return ( -
- - - -
- ); -}; - type NodeFormProps = { className?: string; schema: iNodeSchema | IProfileSchema; - profiles?: Profile[]; + profiles?: ProfileData[]; onSuccess?: (newObject: any) => void; currentObject?: Record; isFilterForm?: boolean; @@ -279,16 +164,14 @@ const NodeForm = ({ }: NodeFormProps) => { const branch = useAtomValue(currentBranchAtom); const date = useAtomValue(datetimeAtom); - const schemas = useAtomValue(schemaState); const [filters] = useFilters(); const auth = useAuth(); const fields = getFormFieldsFromSchema({ schema, - schemas, profiles, initialObject: currentObject, - user: auth, + auth, isFilterForm, filters, }); diff --git a/frontend/app/src/components/form/profiles-selector.tsx b/frontend/app/src/components/form/profiles-selector.tsx new file mode 100644 index 0000000000..d412785d56 --- /dev/null +++ b/frontend/app/src/components/form/profiles-selector.tsx @@ -0,0 +1,127 @@ +import { genericsState, iNodeSchema, profilesAtom } from "@/state/atoms/schema.atom"; +import { useId } from "react"; +import { useAtomValue } from "jotai/index"; +import { getObjectAttributes } from "@/utils/getSchemaObjectColumns"; +import ErrorScreen from "@/screens/errors/error-screen"; +import { getProfiles } from "@/graphql/queries/objects/getProfiles"; +import { gql } from "@apollo/client"; +import useQuery from "@/hooks/useQuery"; +import LoadingScreen from "@/screens/loading-screen/loading-screen"; +import Label from "@/components/ui/label"; +import { MultiCombobox } from "@/components/ui/combobox"; + +type ProfilesSelectorProps = { + schema: iNodeSchema; + value?: any[]; + defaultValue?: any[]; + onChange: (item: any[]) => void; + currentProfiles?: any[]; +}; + +export const ProfilesSelector = ({ + schema, + value, + defaultValue, + onChange, +}: ProfilesSelectorProps) => { + const id = useId(); + + const generics = useAtomValue(genericsState); + const profiles = useAtomValue(profilesAtom); + + const nodeGenerics = schema?.inherit_from ?? []; + + // Get all available generic profiles + const nodeGenericsProfiles = nodeGenerics + // Find all generic schema + .map((nodeGeneric: any) => generics.find((generic) => generic.kind === nodeGeneric)) + // Filter for generate_profile ones + .filter((generic: any) => generic.generate_profile) + // Get only the kind + .map((generic: any) => generic.kind) + .filter(Boolean); + + // The profiles should include the current object profile + all generic profiles + const kindList = [schema.kind, ...nodeGenericsProfiles]; + + // Add attributes for each profile to get the values in the form + const profilesList = kindList + .map((profile) => { + // Get the profile schema for the current kind + const profileSchema = profiles.find((profileSchema) => profileSchema.name === profile); + + // Get attributes for query + form data + const attributes = getObjectAttributes({ schema: profileSchema, forListView: true }); + + if (!attributes.length) return null; + + return { + name: profileSchema?.kind, + schema: profileSchema, + attributes, + }; + }) + .filter(Boolean); + + // Get all profiles name to retrieve the informations from the result + const profilesNameList: string[] = profilesList + .map((profile) => profile?.name ?? "") + .filter(Boolean); + + if (!profilesList.length) + return ; + + const queryString = getProfiles({ profiles: profilesList }); + + const query = gql` + ${queryString} + `; + + const { data, error, loading } = useQuery(query); + + if (loading) return ; + + if (error) return ; + + // Get data for each profile in the query result + const profilesData = profilesNameList.reduce( + (acc, profile) => [...acc, ...(data?.[profile!]?.edges ?? [])], + [] + ); + + // Build combobox options + const items = profilesData?.map((edge: any) => ({ + value: edge.node.id, + label: edge.node.display_label, + data: edge.node, + })); + + const selectedValues = value?.map((profile) => profile.id) ?? []; + + const handleChange = (newProfilesId: string[]) => { + const newSelectedProfiles = newProfilesId + .map((profileId) => items.find((option) => option.value === profileId)) + .filter(Boolean) + .map((option) => option?.data); + + onChange(newSelectedProfiles); + }; + + if (!profilesData || profilesData.length === 0) return null; + + if (!value && defaultValue) { + const ids = defaultValue.map((profile) => profile.id); + + handleChange(ids); + } + + return ( +
+ + + +
+ ); +}; diff --git a/frontend/app/src/components/form/utils/getFieldDefaultValue.ts b/frontend/app/src/components/form/utils/getFieldDefaultValue.ts new file mode 100644 index 0000000000..39a016aa7d --- /dev/null +++ b/frontend/app/src/components/form/utils/getFieldDefaultValue.ts @@ -0,0 +1,55 @@ +import { FieldSchema, AttributeType } from "@/utils/getObjectItemDisplayValue"; +import { ProfileData } from "@/components/form/object-form"; + +export type GetFieldDefaultValue = { + fieldSchema: FieldSchema; + initialObject?: Record; + profiles?: Array; + isFilterForm?: boolean; +}; + +export const getFieldDefaultValue = ({ + fieldSchema, + initialObject, + profiles = [], + isFilterForm, +}: GetFieldDefaultValue) => { + // Do not use profiles nor default values in filters + if (isFilterForm) { + return getCurrentFieldValue(fieldSchema.name, initialObject); + } + + return ( + getCurrentFieldValue(fieldSchema.name, initialObject) ?? + getDefaultValueFromProfile(fieldSchema.name, profiles) ?? + getDefaultValueFromSchema(fieldSchema) ?? + null + ); +}; + +const getCurrentFieldValue = (fieldName: string, objectData?: Record) => { + if (!objectData) return null; + + const currentField = objectData[fieldName]; + if (!currentField) return null; + + return currentField.is_from_profile ? null : currentField.value; +}; + +const getDefaultValueFromProfile = (fieldName: string, profiles: Array) => { + // Get value from profiles depending on the priority + const orderedProfiles = profiles.sort((optionA, optionB) => { + if (optionA.profile_priority.value < optionB.profile_priority.value) return -1; + return 1; + }); + + return orderedProfiles.find((profile) => profile?.[fieldName]?.value)?.[fieldName]?.value; +}; + +const getDefaultValueFromSchema = (fieldSchema: FieldSchema) => { + if (fieldSchema.kind === "Boolean" || fieldSchema.kind === "Checkbox") { + return !!fieldSchema.default_value; + } + + return "default_value" in fieldSchema ? fieldSchema.default_value : null; +}; diff --git a/frontend/app/src/components/form/utils.ts b/frontend/app/src/components/form/utils/getFormFieldsFromSchema.ts similarity index 57% rename from frontend/app/src/components/form/utils.ts rename to frontend/app/src/components/form/utils/getFormFieldsFromSchema.ts index 898c954d99..25e7b928d7 100644 --- a/frontend/app/src/components/form/utils.ts +++ b/frontend/app/src/components/form/utils/getFormFieldsFromSchema.ts @@ -1,8 +1,13 @@ -import { DynamicFieldProps } from "./type"; -import { genericsState, iGenericSchema, iNodeSchema, schemaState } from "@/state/atoms/schema.atom"; -import { SchemaAttributeType } from "@/screens/edit-form-hook/dynamic-control-types"; -import { sortByOrderWeight } from "@/utils/common"; -import { SCHEMA_ATTRIBUTE_KIND } from "@/config/constants"; +import { + genericsState, + iGenericSchema, + iNodeSchema, + profilesAtom, + schemaState, +} from "@/state/atoms/schema.atom"; +import { AttributeType } from "@/utils/getObjectItemDisplayValue"; +import { AuthContextType } from "@/hooks/useAuth"; +import { DynamicFieldProps } from "@/components/form/type"; import { getObjectRelationshipsForForm, getOptionsFromAttribute, @@ -10,28 +15,29 @@ import { getRelationshipValue, getSelectParent, } from "@/utils/getSchemaObjectColumns"; -import { AttributeType } from "@/utils/getObjectItemDisplayValue"; +import { isGeneric, sortByOrderWeight } from "@/utils/common"; +import { getFieldDefaultValue } from "@/components/form/utils/getFieldDefaultValue"; +import { SchemaAttributeType } from "@/screens/edit-form-hook/dynamic-control-types"; import { store } from "@/state"; -import { getIsDisabled } from "@/utils/formStructureForCreateEdit"; -import { components } from "@/infraops"; -import { AuthContextType } from "@/hooks/useAuth"; +import { SCHEMA_ATTRIBUTE_KIND } from "@/config/constants"; +import { ProfileData } from "@/components/form/object-form"; +import { isFieldDisabled } from "@/components/form/utils/isFieldDisabled"; +import { useAtomValue } from "jotai/index"; type GetFormFieldsFromSchema = { schema: iNodeSchema | iGenericSchema; - schemas?: iNodeSchema[] | iGenericSchema[]; - profiles?: Object[]; + profiles?: Array; initialObject?: Record; - user?: AuthContextType; + auth?: AuthContextType; isFilterForm?: boolean; filters?: Array; }; export const getFormFieldsFromSchema = ({ schema, - schemas, profiles, initialObject, - user, + auth, isFilterForm, filters, }: GetFormFieldsFromSchema): Array => { @@ -41,28 +47,26 @@ export const getFormFieldsFromSchema = ({ ].filter((attribute) => !attribute.read_only); const orderedFields: typeof unorderedFields = sortByOrderWeight(unorderedFields); - const formFields = orderedFields.map((attribute) => { - const disabled = getIsDisabled({ - owner: initialObject && initialObject[attribute.name]?.owner, - user, - isProtected: - initialObject && - initialObject[attribute.name] && - initialObject[attribute.name].is_protected, - isReadOnly: attribute.read_only, - }); - + const formFields: Array = orderedFields.map((attribute) => { const basicFomFieldProps = { name: attribute.name, label: attribute.label ?? undefined, - defaultValue: getObjectDefaultValue({ + defaultValue: getFieldDefaultValue({ fieldSchema: attribute, initialObject, profiles, isFilterForm, }), description: attribute.description ?? undefined, - disabled, + disabled: isFieldDisabled({ + owner: initialObject && initialObject[attribute.name]?.owner, + auth, + isProtected: + initialObject && + initialObject[attribute.name] && + !!initialObject[attribute.name].is_protected, + isReadOnly: attribute.read_only, + }), type: attribute.kind as Exclude, rules: { required: !isFilterForm && !attribute.optional, @@ -118,25 +122,27 @@ export const getFormFieldsFromSchema = ({ }); // Allow kind filter for generic - if (isFilterForm && schema.used_by?.length) { + if (isFilterForm && isGeneric(schema) && schema.used_by?.length) { const kindFilter = filters?.find((filter) => filter.name == "kind__value"); + const nodes = useAtomValue(schemaState); + const profiles = useAtomValue(profilesAtom); + const schemas = [...nodes, ...profiles]; const items = schema.used_by .map((kind) => { if (!schemas) return null; const relatedSchema = schemas.find((schema) => schema.kind === kind); - console.log("relatedSchema: ", relatedSchema); if (!relatedSchema) return null; return { - id: relatedSchema.kind, + id: relatedSchema.kind as string, name: relatedSchema.label ?? relatedSchema.name, badge: relatedSchema.namespace, }; }) - .filter(Boolean); + .filter((n) => n !== null); return [ { @@ -153,54 +159,3 @@ export const getFormFieldsFromSchema = ({ return formFields; }; - -export type GetObjectDefaultValue = { - fieldSchema: GetObjectDefaultValueFromSchema; - initialObject?: Record; - profiles?: Record[]; - isFilterForm?: boolean; -}; - -export const getObjectDefaultValue = ({ - fieldSchema, - initialObject, - profiles = [], - isFilterForm, -}: GetObjectDefaultValue) => { - // Sort profiles from profile_priority value - const orderedProfiles = profiles.sort((optionA, optionB) => { - if (optionA.profile_priority.value < optionB.profile_priority.value) return -1; - return 1; - }); - - // Get current object value - const currentField = initialObject?.[fieldSchema.name]; - const currentFieldValue = currentField?.is_from_profile ? null : currentField?.value; - - // Get value from profiles depending on the priority - const defaultValueFromProfile = orderedProfiles.find( - (profile) => profile?.[fieldSchema.name]?.value - )?.[fieldSchema.name]?.value; - - // Get default value from schema - const defaultValueFromSchema = getDefaultValueFromSchema(fieldSchema); - - // Do not use profiles nor default values in filters - if (isFilterForm) { - return currentFieldValue ?? null; - } - - return currentFieldValue ?? defaultValueFromProfile ?? defaultValueFromSchema ?? null; -}; - -export type GetObjectDefaultValueFromSchema = - | components["schemas"]["AttributeSchema-Output"] - | components["schemas"]["RelationshipSchema-Output"]; - -const getDefaultValueFromSchema = (fieldSchema: GetObjectDefaultValueFromSchema) => { - if (fieldSchema.kind === "Boolean" || fieldSchema.kind === "Checkbox") { - return !!fieldSchema.default_value; - } - - return "default_value" in fieldSchema ? fieldSchema.default_value : null; -}; diff --git a/frontend/app/src/components/form/utils/isFieldDisabled.ts b/frontend/app/src/components/form/utils/isFieldDisabled.ts new file mode 100644 index 0000000000..43e9aab4d3 --- /dev/null +++ b/frontend/app/src/components/form/utils/isFieldDisabled.ts @@ -0,0 +1,24 @@ +import { LineageOwner } from "@/generated/graphql"; +import { AuthContextType } from "@/hooks/useAuth"; + +export type IsFieldDisabledParams = { + owner?: LineageOwner | null; + auth?: AuthContextType; + isProtected?: boolean; + isReadOnly?: boolean; +}; + +export const isFieldDisabled = ({ + owner, + auth, + isProtected, + isReadOnly, +}: IsFieldDisabledParams) => { + if (isReadOnly) return true; + + // Field is available if there is no owner and if is_protected is not set to true + if (!isProtected || !owner || auth?.permissions?.isAdmin) return false; + + // Field is available only if is_protected is set to true and if the owner is the user + return owner?.id !== auth?.user?.id; +}; diff --git a/frontend/app/src/screens/edit-form-hook/dynamic-control-types.ts b/frontend/app/src/screens/edit-form-hook/dynamic-control-types.ts index 837a567667..e13e2471f1 100644 --- a/frontend/app/src/screens/edit-form-hook/dynamic-control-types.ts +++ b/frontend/app/src/screens/edit-form-hook/dynamic-control-types.ts @@ -68,64 +68,3 @@ export type ControlType = | "dropdown" | "enum" | "color"; - -export type RelationshipCardinality = "one" | "many"; - -export const getInputTypeFromKind = (kind: SchemaAttributeType): ControlType => { - switch (kind) { - case "List": - return "list"; - case "Dropdown": - return "dropdown"; - case "TextArea": - return "textarea"; - case "Number": - case "Bandwidth": - return "number"; - case "Checkbox": - case "Boolean": - return "checkbox"; - case "DateTime": - return "datepicker"; - case "JSON": - return "json"; - case "Password": - case "HashedPassword": - return "password"; - case "Text": - case "ID": - case "Email": - case "URL": - case "File": - case "MacAddress": - case "Color": - case "IPHost": - case "IPNetwork": - case "Any": - default: - return "text"; - } -}; - -export const getInputTypeFromAttribute = (attribute: any): ControlType => { - if (attribute.enum) { - return "enum"; - } - - return getInputTypeFromKind(attribute.kind); -}; - -export const getInputTypeFromRelationship = ( - relationship: any, - isInherited: boolean -): ControlType => { - if (relationship.cardinality === "many") { - return "multiselect"; - } - - if (isInherited) { - return "select2step"; - } - - return "select"; -}; diff --git a/frontend/app/src/utils/common.ts b/frontend/app/src/utils/common.ts index 5dff11dbed..21531132ba 100644 --- a/frontend/app/src/utils/common.ts +++ b/frontend/app/src/utils/common.ts @@ -1,4 +1,4 @@ -import { IModelSchema } from "@/state/atoms/schema.atom"; +import { iGenericSchema, IModelSchema } from "@/state/atoms/schema.atom"; import { clsx, type ClassValue } from "clsx"; import * as R from "ramda"; import { twMerge } from "tailwind-merge"; @@ -85,6 +85,6 @@ export function warnUnexpectedType(x: never) { console.warn(`unexpected type ${x}`); } -export function isGeneric(schema: IModelSchema): boolean { +export function isGeneric(schema: IModelSchema): schema is iGenericSchema { return "used_by" in schema; } diff --git a/frontend/app/src/utils/formStructureForCreateEdit.ts b/frontend/app/src/utils/formStructureForCreateEdit.ts deleted file mode 100644 index 0661f4a05b..0000000000 --- a/frontend/app/src/utils/formStructureForCreateEdit.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { SelectOption } from "@/components/inputs/select"; -import { - DynamicFieldData, - SchemaAttributeType, - getInputTypeFromAttribute, - getInputTypeFromRelationship, -} from "@/screens/edit-form-hook/dynamic-control-types"; -import { iGenericSchema, iNodeSchema } from "@/state/atoms/schema.atom"; -import { isValid } from "date-fns"; -import { sortByOrderWeight } from "./common"; -import { - getFieldValue, - getObjectRelationshipsForForm, - getOptionsFromAttribute, - getRelationshipOptions, - getRelationshipValue, - getSelectParent, -} from "./getSchemaObjectColumns"; -import { AuthContextType } from "@/hooks/useAuth"; - -type getIsDisabledParams = { - owner?: { id: string }; - user?: AuthContextType; - isProtected?: boolean; - isReadOnly?: boolean; -}; - -export const getIsDisabled = ({ owner, user, isProtected, isReadOnly }: getIsDisabledParams) => { - // Field is read only - if (isReadOnly) return true; - - // Field is available if there is no owner and if is_protected is not set to true - if (!isProtected || !owner || user?.permissions?.isAdmin) return false; - - // Field is available only if is_protected is set to true and if the owner is the user - return owner?.id !== user?.data?.sub; -}; - -const validate = (value: any, attribute: any = {}, optional?: boolean) => { - const { default_value: defaultValue } = attribute; - - // If optional, no validator is needed (we try to validate if the value is defined or not) - if (optional) { - return true; - } - - // If the attribute is of kind integer, then it should be a number - if (attribute.kind === "Number" && Number.isInteger(value)) { - return true; - } - - // If the attribute is of kind boolean, then it should - if (attribute.kind === "Boolean") { - return true; - } - - // If the attribute is a date, check if the date is valid - if (attribute.kind === "DateTime") { - if (!value) { - return "Required"; - } - - if (!isValid(value)) { - return "Invalid date"; - } - - return true; - } - - // The value is defined, then we can validate - if (Array.isArray(value) ? value.length : value) { - return true; - } - - // If the value is false but it is the default_value, then validate (checkbox example) - if (defaultValue !== undefined && defaultValue !== null && value === defaultValue) { - return true; - } - - // No value and the value is not the default_value, then mark as required - return "Required"; -}; - -type FormParameters = { - schema: iNodeSchema | undefined; - schemas: iNodeSchema[] | undefined; - generics: iGenericSchema[]; - row?: any; - user?: any; - isUpdate?: boolean; - isFilters?: boolean; - profile?: iNodeSchema | undefined; -}; - -const getFormStructureForCreateEdit = ({ - schema, - schemas, - generics, - row, - user, - isUpdate, - isFilters, - profile, -}: FormParameters): DynamicFieldData[] => { - if (!schema) { - return []; - } - - const fieldsToParse = sortByOrderWeight([ - ...(schema.attributes ?? []), - ...(getObjectRelationshipsForForm(schema, isUpdate) ?? []), - ]); - - const fields = fieldsToParse - .filter((field) => !field.read_only) // Ignore read only fields - .map((field) => { - // Parse a relationship - if (field.cardinality) { - const isInherited = !!generics.find((g) => g.kind === field.peer); - - return { - name: field.name, - kind: "String" as SchemaAttributeType, - peer: field.peer, - type: getInputTypeFromRelationship(field, isInherited), - label: field.label ? field.label : field.name, - value: getRelationshipValue({ row, field, isFilters }), - options: getRelationshipOptions(row, field, schemas, generics), - config: { - validate: (value: any) => (isFilters ? true : validate(value, field, field.optional)), - }, - isOptional: isFilters || field.optional, - isProtected: getIsDisabled({ - owner: row && row[field.name]?.properties?.owner, - user, - isProtected: row && row[field.name] && row[field.name]?.properties?.is_protected, - isReadOnly: field.read_only, - }), - isInherited, - parent: isInherited && row && getSelectParent(row, field), // Used by select 2 steps - field, - schema, - }; - } - - // Parse an attribute - const fieldValue = getFieldValue({ - row, - field, - profile, - isFilters, - }); - - // Quick fix to prevent password in update field, - // TODO: remove HashedPassword test after new mutations are available to better handle accounts - const isOptional = (field.optional || (isUpdate && field.kind === "HashedPassword")) ?? false; - const isProtected = getIsDisabled({ - owner: row && row[field.name]?.owner, - user, - isProtected: row && row[field.name] && row[field.name].is_protected, - isReadOnly: field.read_only, - }); - - return { - name: field.name, - kind: field.kind as SchemaAttributeType, - type: getInputTypeFromAttribute(field), - label: field.label || field.name, - value: fieldValue, - options: getOptionsFromAttribute(field, fieldValue), - config: { - validate: (value: any) => (isFilters ? true : validate(value, field, isOptional)), - }, - isOptional: isFilters || isOptional, - isReadOnly: field.read_only, - isProtected, - isUnique: !isFilters && field.unique, - field, - schema, - }; - }) - .filter(Boolean); - - return fields; -}; - -export default getFormStructureForCreateEdit; - -export const getFormStructureForMetaEdit = ( - row: any, - type: "attribute" | "relationship", - schemaList: iNodeSchema[] -): DynamicFieldData[] => { - const sourceOwnerFields = - type === "attribute" ? ["owner", "source"] : ["_relation__owner", "_relation__source"]; - - const booleanFields = - type === "attribute" - ? ["is_visible", "is_protected"] - : ["_relation__is_visible", "_relation__is_protected"]; - - const relatedObjects: { [key: string]: string } = { - source: "DataSource", - owner: "DataOwner", - _relation__source: "DataSource", - _relation__owner: "DataOwner", - }; - - const sourceOwnerFormFields: DynamicFieldData[] = sourceOwnerFields.map((f) => { - const schemaOptions: SelectOption[] = [ - ...schemaList - .filter((schema) => { - if ((schema.inherit_from || []).indexOf(relatedObjects[f]) > -1) { - return true; - } else { - return false; - } - }) - .map((schema) => ({ - name: schema.kind, - id: schema.name, - })), - ]; - - return { - name: f, - kind: "Text", - isAttribute: false, - isRelationship: false, - type: "select2step", - label: f - .split("_") - .filter((r) => !!r) - .join(" "), - value: row?.[f], - options: schemaOptions, - config: {}, - }; - }); - - const booleanFormFields: DynamicFieldData[] = booleanFields.map((f) => { - return { - name: f, - kind: "Checkbox", - isAttribute: false, - isRelationship: false, - type: "checkbox", - label: f - .split("_") - .filter((r) => !!r) - .join(" "), - value: row?.[f], - options: [], - config: {}, - }; - }); - - return [...sourceOwnerFormFields, ...booleanFormFields]; -}; - -export const getFormStructureForMetaEditPaginated = ( - row: any, - schemaList: iNodeSchema[] -): DynamicFieldData[] => { - const sourceOwnerFields = ["owner", "source"]; - - const booleanFields = ["is_visible", "is_protected"]; - - const relatedObjects: { [key: string]: string } = { - source: "LineageSource", - owner: "LineageOwner", - _relation__source: "LineageSource", - _relation__owner: "LineageOwner", - }; - - const sourceOwnerFormFields: DynamicFieldData[] = sourceOwnerFields.map((field) => { - const schemaOptions: SelectOption[] = [ - ...schemaList - .filter((schema) => { - if ((schema.inherit_from || []).indexOf(relatedObjects[field]) > -1) { - return true; - } else { - return false; - } - }) - .map((schema) => ({ - name: schema.name, - id: schema.kind, - })), - ]; - - return { - name: field, - kind: "Text", - isAttribute: false, - isRelationship: false, - type: "select2step", - label: field.split("_").filter(Boolean).join(" "), - value: row?.[field]?.id, - options: schemaOptions, - config: {}, - parent: row?.[field]?.__typename, - }; - }); - - const booleanFormFields: DynamicFieldData[] = booleanFields.map((field) => { - return { - name: field, - kind: "Checkbox", - isAttribute: false, - isRelationship: false, - type: "checkbox", - label: field.split("_").filter(Boolean).join(" "), - value: row?.[field], - options: [], - config: {}, - }; - }); - - return [...sourceOwnerFormFields, ...booleanFormFields]; -}; diff --git a/frontend/app/src/utils/getObjectItemDisplayValue.tsx b/frontend/app/src/utils/getObjectItemDisplayValue.tsx index cb9d432b84..802544be4e 100644 --- a/frontend/app/src/utils/getObjectItemDisplayValue.tsx +++ b/frontend/app/src/utils/getObjectItemDisplayValue.tsx @@ -148,7 +148,7 @@ export const getObjectItemDisplayValue = ( ); }; -export type AttributeSchema = +export type FieldSchema = | components["schemas"]["AttributeSchema-Output"] | components["schemas"]["RelationshipSchema-Output"]; @@ -167,7 +167,7 @@ export const ObjectAttributeValue = ({ attributeSchema, attributeValue, }: { - attributeSchema: AttributeSchema; + attributeSchema: FieldSchema; attributeValue: AttributeType; }) => { if (!attributeValue.value && attributeValue.value !== 0) return "-"; diff --git a/frontend/app/src/utils/getSchemaObjectColumns.ts b/frontend/app/src/utils/getSchemaObjectColumns.ts index 177634e4e9..7c99b85689 100644 --- a/frontend/app/src/utils/getSchemaObjectColumns.ts +++ b/frontend/app/src/utils/getSchemaObjectColumns.ts @@ -8,7 +8,6 @@ import { } from "@/config/constants"; import { store } from "@/state"; import { iGenericSchema, iNodeSchema, profilesAtom } from "@/state/atoms/schema.atom"; -import { isValid, parseISO } from "date-fns"; import * as R from "ramda"; import { isGeneric, sortByOrderWeight } from "./common"; import { AttributeType } from "@/utils/getObjectItemDisplayValue"; @@ -137,33 +136,6 @@ export const getSchemaObjectColumns = ({ return isGeneric(schema) && columns.length > 0 ? [kindColumn, ...columns] : columns; }; -export const getGroupColumns = (schema?: iNodeSchema | iGenericSchema) => { - if (!schema) { - return []; - } - - const defaultColumns = [{ label: "Type", name: "__typename" }]; - - const columns = getSchemaObjectColumns({ schema }); - - return [...defaultColumns, ...columns]; -}; - -export const getAttributeColumnsFromNodeOrGenericSchema = ( - schema: iNodeSchema | undefined, - generic: iGenericSchema | undefined -) => { - if (generic) { - return getSchemaObjectColumns({ schema: generic }); - } - - if (schema) { - return getSchemaObjectColumns({ schema }); - } - - return []; -}; - export const getObjectTabs = (tabs: any[], data: any) => { return tabs.map((tab: any) => ({ ...tab, @@ -190,72 +162,6 @@ export const getObjectRelationshipsForForm = ( return relationships; }; -// Used by the query to retrieve the data for the form -export const getObjectPeers = (schema?: iNodeSchema | iGenericSchema) => { - const peers = getObjectRelationshipsForForm(schema) - .map((relationship) => relationship.peer) - .filter(Boolean); - - return peers; -}; - -const getValue = (row: any, attribute: any, profile: any) => { - // If the value defined was from the profile, then override it from the new profile value - if (row && row[attribute.name]?.is_from_profile && profile) { - return profile[attribute.name]?.value; - } - - // What comes from the object is priority - if (row && row[attribute.name]?.value) { - return row[attribute.name]?.value; - } - - if (profile && profile[attribute.name]?.value) { - return profile[attribute.name]?.value; - } - - if (attribute.kind === "Boolean") { - return attribute.default_value ?? false; - } - - return attribute.default_value; -}; - -type tgetFieldValue = { - row: any; - field: any; - profile: any; - isFilters?: boolean; -}; - -export const getFieldValue = ({ row, field, profile, isFilters }: tgetFieldValue) => { - // No default value for filters - if (isFilters) return ""; - - const value = getValue(row, field, profile); - - if (value === null || value === undefined) return null; - - if (field.kind === "DateTime") { - if (isValid(value)) { - return value; - } - - if (isValid(parseISO(value))) { - return parseISO(value); - } - - return null; - } - - if (field.kind === "JSON") { - // Ensure we use objects as values - return typeof value === "string" ? JSON.parse(value) : value; - } - - return value ?? null; -}; - type tgetRelationshipValue = { row: any; field: any; diff --git a/frontend/app/tests/unit/isFieldDisabled.test.ts b/frontend/app/tests/unit/isFieldDisabled.test.ts new file mode 100644 index 0000000000..7c8e3f9089 --- /dev/null +++ b/frontend/app/tests/unit/isFieldDisabled.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from "vitest"; +import { + isFieldDisabled, + IsFieldDisabledParams, +} from "../../src/components/form/utils/isFieldDisabled"; + +describe("isFieldDisabled", () => { + it("returns true when field is read only", () => { + // GIVEN + const params: IsFieldDisabledParams = { + isProtected: false, + isReadOnly: true, + owner: null, + auth: null, + }; + + // WHEN + const disabled = isFieldDisabled(params); + + // THEN + expect(disabled).to.equal(true); + }); + + it("returns false when field is not protected", () => { + // GIVEN + const params: IsFieldDisabledParams = { + isProtected: false, + isReadOnly: false, + owner: null, + auth: null, + }; + + // WHEN + const disabled = isFieldDisabled(params); + + // THEN + expect(disabled).to.equal(false); + }); + + it("returns false if field is protected but has no owner", () => { + // GIVEN + const params: IsFieldDisabledParams = { + isProtected: true, + isReadOnly: false, + owner: null, + auth: null, + }; + + // WHEN + const disabled = isFieldDisabled(params); + + // THEN + expect(disabled).to.equal(false); + }); + + it("returns true if field is protected but the current user is not the owner", () => { + // GIVEN + const params: IsFieldDisabledParams = { + isProtected: true, + isReadOnly: false, + owner: { id: "user-1" }, + auth: { user: { id: "not-user-1" } }, + }; + + // WHEN + const disabled = isFieldDisabled(params); + + // THEN + expect(disabled).to.equal(true); + }); + + it("returns false if field is protected and the current user is the owner", () => { + // GIVEN + const params: IsFieldDisabledParams = { + isProtected: true, + isReadOnly: false, + owner: { id: "user-1" }, + auth: { user: { id: "user-1" } }, + }; + + // WHEN + const disabled = isFieldDisabled(params); + + // THEN + expect(disabled).to.equal(false); + }); + + it("returns false if the auth is an admin", () => { + // GIVEN + const params: IsFieldDisabledParams = { + isProtected: true, + isReadOnly: false, + owner: { id: "not-admin" }, + auth: { permissions: { isAdmin: true } }, + }; + + // WHEN + const disabled = isFieldDisabled(params); + + // THEN + expect(disabled).to.equal(false); + }); +}); diff --git a/frontend/app/tests/unit/utils/getObjectDefaultValue.test.ts b/frontend/app/tests/unit/utils/getFieldDefaultValue.test.ts similarity index 68% rename from frontend/app/tests/unit/utils/getObjectDefaultValue.test.ts rename to frontend/app/tests/unit/utils/getFieldDefaultValue.test.ts index 1cd6273d2c..b84770622e 100644 --- a/frontend/app/tests/unit/utils/getObjectDefaultValue.test.ts +++ b/frontend/app/tests/unit/utils/getFieldDefaultValue.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { GetObjectDefaultValue, getObjectDefaultValue } from "@/components/form/utils"; +import { + GetFieldDefaultValue, + getFieldDefaultValue, +} from "@/components/form/utils/getFieldDefaultValue"; describe("getObjectDefaultValue", () => { it("returns null when no default value are found", () => { @@ -24,10 +27,10 @@ describe("getObjectDefaultValue", () => { default_value: null, inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema }); + const defaultValue = getFieldDefaultValue({ fieldSchema }); // THEN expect(defaultValue).to.equal(null); @@ -57,10 +60,10 @@ describe("getObjectDefaultValue", () => { on_delete: "no-action", allow_override: "any", read_only: false, - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema }); + const defaultValue = getFieldDefaultValue({ fieldSchema }); // THEN expect(defaultValue).to.equal(null); @@ -88,10 +91,10 @@ describe("getObjectDefaultValue", () => { default_value: "test-value-form-schema", inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema }); + const defaultValue = getFieldDefaultValue({ fieldSchema }); // THEN expect(defaultValue).to.equal("test-value-form-schema"); @@ -119,7 +122,7 @@ describe("getObjectDefaultValue", () => { default_value: "test-value-form-schema", inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; const profiles = [ { @@ -130,7 +133,7 @@ describe("getObjectDefaultValue", () => { ]; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema, profiles }); + const defaultValue = getFieldDefaultValue({ fieldSchema, profiles }); // THEN expect(defaultValue).to.equal("test-value-form-schema"); @@ -158,7 +161,7 @@ describe("getObjectDefaultValue", () => { default_value: "test-value-form-schema", inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; const profiles = [ { @@ -175,7 +178,7 @@ describe("getObjectDefaultValue", () => { }; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema, initialObject, profiles }); + const defaultValue = getFieldDefaultValue({ fieldSchema, initialObject, profiles }); // THEN expect(defaultValue).to.equal("test-value-form-schema"); @@ -203,7 +206,7 @@ describe("getObjectDefaultValue", () => { default_value: null, inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; const profiles = [ { @@ -220,7 +223,7 @@ describe("getObjectDefaultValue", () => { }; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema, initialObject, profiles }); + const defaultValue = getFieldDefaultValue({ fieldSchema, initialObject, profiles }); // THEN expect(defaultValue).to.equal("test-value-form-profile"); @@ -248,7 +251,7 @@ describe("getObjectDefaultValue", () => { default_value: "test-value-form-schema", inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; const profiles = [ { @@ -265,12 +268,68 @@ describe("getObjectDefaultValue", () => { }; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema, initialObject, profiles }); + const defaultValue = getFieldDefaultValue({ fieldSchema, initialObject, profiles }); // THEN expect(defaultValue).to.equal("test-value-form-profile"); }); + it("returns default value from the profile with the highest priority", () => { + // GIVEN + const fieldSchema = { + id: "17d67b92-f0b9-cf97-3001-c51824a9c7dc", + state: "present", + name: "name", + kind: "Text", + enum: null, + choices: null, + regex: null, + max_length: null, + min_length: null, + label: "Name", + description: null, + read_only: false, + unique: true, + optional: false, + branch: "aware", + order_weight: 1000, + default_value: "test-value-form-schema", + inherited: false, + allow_override: "any", + } satisfies GetFieldDefaultValue["fieldSchema"]; + + const profiles = [ + { + name: { + value: "second", + }, + profile_priority: { + value: 2, + }, + }, + { + name: { + value: "first", + }, + profile_priority: { + value: 1, + }, + }, + ]; + + const initialObject = { + name: { + value: null, + }, + }; + + // WHEN + const defaultValue = getFieldDefaultValue({ fieldSchema, initialObject, profiles }); + + // THEN + expect(defaultValue).to.equal("first"); + }); + it("returns current object field's value when it exists", () => { // GIVEN const fieldSchema = { @@ -293,7 +352,7 @@ describe("getObjectDefaultValue", () => { default_value: null, inherited: false, allow_override: "any", - } satisfies GetObjectDefaultValue["fieldSchema"]; + } satisfies GetFieldDefaultValue["fieldSchema"]; const profiles = [ { @@ -310,9 +369,40 @@ describe("getObjectDefaultValue", () => { }; // WHEN - const defaultValue = getObjectDefaultValue({ fieldSchema, initialObject, profiles }); + const defaultValue = getFieldDefaultValue({ fieldSchema, initialObject, profiles }); // THEN expect(defaultValue).to.equal("data-from-current-object"); }); + + it("returns null if no data is found", () => { + // GIVEN + const fieldSchema = { + id: "17d67b92-f0b9-cf97-3001-c51824a9c7dc", + state: "present", + name: "name", + kind: "Text", + enum: null, + choices: null, + regex: null, + max_length: null, + min_length: null, + label: "Name", + description: null, + read_only: false, + unique: true, + optional: false, + branch: "aware", + order_weight: 1000, + default_value: null, + inherited: false, + allow_override: "any", + } satisfies GetFieldDefaultValue["fieldSchema"]; + + // WHEN + const defaultValue = getFieldDefaultValue({ fieldSchema, initialObject: {} }); + + // THEN + expect(defaultValue).to.equal(null); + }); }); diff --git a/frontend/app/tests/unit/utils/getFormFieldsFromSchema.test.ts b/frontend/app/tests/unit/utils/getFormFieldsFromSchema.test.ts index 93a3687707..ca57482d44 100644 --- a/frontend/app/tests/unit/utils/getFormFieldsFromSchema.test.ts +++ b/frontend/app/tests/unit/utils/getFormFieldsFromSchema.test.ts @@ -1,13 +1,65 @@ import { describe, expect, it } from "vitest"; -import { getFormFieldsFromSchema } from "../../../src/components/form/utils"; -import { IModelSchema } from "../../../src/state/atoms/schema.atom"; +import { getFormFieldsFromSchema } from "@/components/form/utils/getFormFieldsFromSchema"; +import { IModelSchema } from "@/state/atoms/schema.atom"; import { AuthContextType } from "@/hooks/useAuth"; import { AttributeType } from "@/utils/getObjectItemDisplayValue"; +import { components } from "@/infraops"; + +const buildAttribute = ( + override?: Partial +): components["schemas"]["AttributeSchema-Output"] => ({ + id: "17d67b92-f0b9-cf97-3001-c51824a9c7dc", + state: "present", + name: "name", + kind: "Text", + enum: null, + choices: null, + regex: null, + max_length: null, + min_length: null, + label: "Name", + description: null, + read_only: false, + unique: false, + optional: true, + branch: "aware", + order_weight: 1000, + default_value: null, + inherited: false, + allow_override: "any", + ...override, +}); + +const buildRelationship = ( + override?: Partial +): components["schemas"]["RelationshipSchema-Output"] => ({ + id: "17e2718c-73ed-3ffe-3402-c515757ff94f", + state: "present", + name: "tagone", + peer: "BuiltinTag", + kind: "Attribute", + label: "Tagone", + description: "relationship many input for testing and development", + identifier: "builtintag__testallinone", + cardinality: "many", + min_count: 0, + max_count: 0, + order_weight: 24000, + optional: true, + branch: "aware", + inherited: false, + direction: "bidirectional", + hierarchical: null, + on_delete: "no-action", + allow_override: "any", + read_only: false, + ...override, +}); describe("getFormFieldsFromSchema", () => { it("returns no fields if schema has no attributes nor relationships", () => { // GIVEN - const schema = {}; + const schema = {} as IModelSchema; // WHEN const fields = getFormFieldsFromSchema({ schema }); @@ -16,33 +68,45 @@ describe("getFormFieldsFromSchema", () => { expect(fields.length).to.equal(0); }); - it("should map a text attribute correctly", () => { + it("returns no fields that are read only", () => { // GIVEN - const schema: Pick = { + const schema = { + attributes: [buildAttribute({ read_only: true })], + relationships: [buildRelationship({ read_only: true })], + } as IModelSchema; + + // WHEN + const fields = getFormFieldsFromSchema({ schema }); + + // THEN + expect(fields.length).to.equal(0); + }); + + it("returns fields ordered by order_weight", () => { + // GIVEN + const schema = { attributes: [ - { - id: "17d67b92-f0b9-cf97-3001-c51824a9c7dc", - state: "present", - name: "name", - kind: "Text", - enum: null, - choices: null, - regex: null, - max_length: null, - min_length: null, - label: "Name", - description: null, - read_only: false, - unique: true, - optional: false, - branch: "aware", - order_weight: 1000, - default_value: null, - inherited: false, - allow_override: "any", - }, + buildAttribute({ name: "third", order_weight: 3 }), + buildAttribute({ name: "first", order_weight: 1 }), ], - }; + relationships: [buildRelationship({ name: "second", order_weight: 2 })], + } as IModelSchema; + + // WHEN + const fields = getFormFieldsFromSchema({ schema }); + + // THEN + expect(fields.length).to.equal(3); + expect(fields[0].name).to.equal("first"); + expect(fields[1].name).to.equal("second"); + expect(fields[2].name).to.equal("third"); + }); + + it("should map a text attribute correctly", () => { + // GIVEN + const schema = { + attributes: [buildAttribute({ kind: "Text" })], + } as IModelSchema; // WHEN const fields = getFormFieldsFromSchema({ schema }); @@ -56,40 +120,18 @@ describe("getFormFieldsFromSchema", () => { name: "name", label: "Name", type: "Text", - unique: true, + unique: false, rules: { - required: true, + required: false, }, }); }); it("should map a HashedPassword attribute correctly", () => { // GIVEN - const schema: Pick = { - attributes: [ - { - id: "17d67b92-f48c-d385-300d-c518c0f3f7f2", - state: "present", - name: "password", - kind: "HashedPassword", - enum: null, - choices: null, - regex: null, - max_length: null, - min_length: null, - label: "Password", - description: null, - read_only: false, - unique: false, - optional: false, - branch: "agnostic", - order_weight: 2000, - default_value: null, - inherited: false, - allow_override: "any", - }, - ], - }; + const schema = { + attributes: [buildAttribute({ label: "Password", name: "password", kind: "HashedPassword" })], + } as IModelSchema; // WHEN const fields = getFormFieldsFromSchema({ schema }); @@ -105,38 +147,16 @@ describe("getFormFieldsFromSchema", () => { type: "HashedPassword", unique: false, rules: { - required: true, + required: false, }, }); }); it("should map a URL attribute correctly", () => { // GIVEN - const schema: Pick = { - attributes: [ - { - id: "17d67b93-db48-7360-3002-c514e72d4910", - state: "present", - name: "url", - kind: "URL", - enum: null, - choices: null, - regex: null, - max_length: null, - min_length: null, - label: "Url", - description: null, - read_only: false, - unique: false, - optional: true, - branch: "agnostic", - order_weight: 3000, - default_value: null, - inherited: true, - allow_override: "any", - }, - ], - }; + const schema = { + attributes: [buildAttribute({ label: "Url", name: "url", kind: "URL" })], + } as IModelSchema; // WHEN const fields = getFormFieldsFromSchema({ schema }); @@ -159,31 +179,9 @@ describe("getFormFieldsFromSchema", () => { it("should map a JSON attribute correctly", () => { // GIVEN - const schema: Pick = { - attributes: [ - { - id: "17d67b93-cdfa-3f6b-3000-c510e05c9186", - state: "present", - name: "parameters", - kind: "JSON", - enum: null, - choices: null, - regex: null, - max_length: null, - min_length: null, - label: "Parameters", - description: null, - read_only: false, - unique: false, - optional: false, - branch: "aware", - order_weight: 3000, - default_value: null, - inherited: false, - allow_override: "any", - }, - ], - }; + const schema = { + attributes: [buildAttribute({ label: "Parameters", name: "parameters", kind: "JSON" })], + } as IModelSchema; // WHEN const fields = getFormFieldsFromSchema({ schema }); @@ -199,21 +197,20 @@ describe("getFormFieldsFromSchema", () => { type: "JSON", unique: false, rules: { - required: true, + required: false, }, }); }); it("should map a Dropdown attribute correctly", () => { // GIVEN - const schema: Pick = { + const schema = { attributes: [ - { - id: "17d67be6-24a2-4da6-3003-c5130e3a579e", - state: "present", + buildAttribute({ + default_value: "address", + label: "Member Type", name: "member_type", kind: "Dropdown", - enum: null, choices: [ { id: null, @@ -232,22 +229,9 @@ describe("getFormFieldsFromSchema", () => { label: "Address", }, ], - regex: null, - max_length: null, - min_length: null, - label: "Member Type", - description: null, - read_only: false, - unique: false, - optional: true, - branch: "aware", - order_weight: 3000, - default_value: "address", - inherited: true, - allow_override: "any", - }, + }), ], - }; + } as IModelSchema; // WHEN const fields = getFormFieldsFromSchema({ schema }); @@ -286,31 +270,9 @@ describe("getFormFieldsFromSchema", () => { it("should disable a protected field if the owner is not the current user", () => { // GIVEN - const schema: Pick = { - attributes: [ - { - id: "17d67b92-f0b9-cf97-3001-c51824a9c7dc", - state: "present", - name: "name", - kind: "Text", - enum: null, - choices: null, - regex: null, - max_length: null, - min_length: null, - label: "Name", - description: null, - read_only: false, - unique: true, - optional: false, - branch: "aware", - order_weight: 1000, - default_value: null, - inherited: false, - allow_override: "any", - }, - ], - }; + const schema = { + attributes: [buildAttribute()], + } as IModelSchema; const initialObject: { name: AttributeType } = { name: { @@ -320,7 +282,6 @@ describe("getFormFieldsFromSchema", () => { owner: { id: "17dd42a7-d547-60af-3111-c51b4b2fc72e", display_label: "Architecture Team", - __typename: "CoreAccount", }, source: null, updated_at: "2024-07-15T09:32:01.363787+00:00", @@ -329,7 +290,7 @@ describe("getFormFieldsFromSchema", () => { }, }; - const user: AuthContextType = { + const auth: AuthContextType = { accessToken: "abc", isAuthenticated: true, isLoading: false, @@ -344,7 +305,7 @@ describe("getFormFieldsFromSchema", () => { }; // WHEN - const fields = getFormFieldsFromSchema({ schema, initialObject, user }); + const fields = getFormFieldsFromSchema({ schema, initialObject, auth }); // THEN expect(fields.length).to.equal(1); @@ -355,40 +316,18 @@ describe("getFormFieldsFromSchema", () => { name: "name", label: "Name", type: "Text", - unique: true, + unique: false, rules: { - required: true, + required: false, }, }); }); it("should enable a protected field if the owner is the current user", () => { // GIVEN - const schema: Pick = { - attributes: [ - { - id: "17d67b92-f0b9-cf97-3001-c51824a9c7dc", - state: "present", - name: "name", - kind: "Text", - enum: null, - choices: null, - regex: null, - max_length: null, - min_length: null, - label: "Name", - description: null, - read_only: false, - unique: true, - optional: false, - branch: "aware", - order_weight: 1000, - default_value: null, - inherited: false, - allow_override: "any", - }, - ], - }; + const schema = { + attributes: [buildAttribute()], + } as IModelSchema; const initialObject: { name: AttributeType } = { name: { @@ -398,7 +337,6 @@ describe("getFormFieldsFromSchema", () => { owner: { id: "1", display_label: "Architecture Team", - __typename: "CoreAccount", }, source: null, updated_at: "2024-07-15T09:32:01.363787+00:00", @@ -407,7 +345,7 @@ describe("getFormFieldsFromSchema", () => { }, }; - const user: AuthContextType = { + const auth: AuthContextType = { accessToken: "abc", isAuthenticated: true, isLoading: false, @@ -422,7 +360,7 @@ describe("getFormFieldsFromSchema", () => { }; // WHEN - const fields = getFormFieldsFromSchema({ schema, initialObject, user }); + const fields = getFormFieldsFromSchema({ schema, initialObject, auth }); // THEN expect(fields.length).to.equal(1); @@ -433,9 +371,9 @@ describe("getFormFieldsFromSchema", () => { name: "name", label: "Name", type: "Text", - unique: true, + unique: false, rules: { - required: true, + required: false, }, }); }); diff --git a/frontend/app/tests/unit/utils/getFormStructure.test.ts b/frontend/app/tests/unit/utils/getFormStructure.test.ts deleted file mode 100644 index cec7f3e8b3..0000000000 --- a/frontend/app/tests/unit/utils/getFormStructure.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { updateObjectWithId } from "../../../src/graphql/mutations/objects/updateObjectWithId"; -import getFormStructureForCreateEdit from "../../../src/utils/formStructureForCreateEdit"; -import getMutationDetailsFromFormData from "../../../src/utils/getMutationDetailsFromFormData"; -import { stringifyWithoutQuotes } from "../../../src/utils/string"; -import { - accountTokenDetailsMocksDataWithDate, - accountTokenDetailsMocksSchema, - accountTokenDetailsUpdateDataMocksData, - accountTokenDetailsUpdatesMocksData, - accountTokenFormStructure, - accountTokenId, - accountTokenMocksMutation, -} from "../../mocks/data/accountToken"; -import { genericsMocks } from "../../mocks/data/generics"; - -describe("Form structure and object update", () => { - it("should return a correct form structure", () => { - const calculatedFields = getFormStructureForCreateEdit({ - schema: accountTokenDetailsMocksSchema[0], - schemas: accountTokenDetailsMocksSchema, - generics: genericsMocks, - row: accountTokenDetailsMocksDataWithDate.InternalAccountToken.edges[0].node, - }); - - // For each attribute, check from the mock data - calculatedFields.map((attribute, index) => { - // Slices last character to remove the closing bracket - const mockString = JSON.stringify(accountTokenFormStructure[index]); - const mockData = mockString.substring(0, mockString.length - 1); - - expect(JSON.stringify(attribute)).toContain(mockData); - }); - }); - - it("should return a correct updated object for mutation", () => { - const updatedObject = getMutationDetailsFromFormData( - accountTokenDetailsMocksSchema[0], - accountTokenDetailsUpdateDataMocksData, - "update", - accountTokenDetailsMocksDataWithDate.InternalAccountToken.edges[0].node - ); - - expect(JSON.stringify(updatedObject)).toContain( - JSON.stringify(accountTokenDetailsUpdatesMocksData) - ); - - const mutationString = updateObjectWithId({ - kind: accountTokenDetailsMocksSchema[0].kind, - data: stringifyWithoutQuotes({ - id: accountTokenId, - ...updatedObject, - }), - }); - - expect(mutationString).toContain(accountTokenMocksMutation); - }); -});