Skip to content

Commit

Permalink
Improved typing, testing and file structure under getFormFieldsFromSc…
Browse files Browse the repository at this point in the history
…hema + 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
  • Loading branch information
bilalabbad authored Jul 16, 2024
1 parent 0fefb83 commit 5f5e402
Show file tree
Hide file tree
Showing 14 changed files with 590 additions and 949 deletions.
135 changes: 9 additions & 126 deletions frontend/app/src/components/form/object-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<string, Pick<AttributeType, "value" | "__typename">>;
export type ProfileData = Record<string, Pick<AttributeType, "value" | "__typename">>;

interface ObjectFormProps extends Omit<DynamicFormProps, "fields"> {
kind: string;
onSuccess?: (newObject: any) => void;
currentObject?: Record<string, AttributeType>;
currentProfiles?: Profile[];
currentProfiles?: ProfileData[];
isFilterForm?: boolean;
onSubmit?: (data: any) => Promise<void>;
}
Expand Down Expand Up @@ -122,7 +118,7 @@ const NodeWithProfileForm = ({ kind, currentProfiles, ...props }: ObjectFormProp
const generics = useAtomValue(genericsState);
const profiles = useAtomValue(profilesAtom);

const [selectedProfiles, setSelectedProfiles] = useState<Profile[] | undefined>();
const [selectedProfiles, setSelectedProfiles] = useState<ProfileData[] | undefined>();

const nodeSchema = [...nodes, ...generics, ...profiles].find((node) => node.kind === kind);

Expand All @@ -132,7 +128,7 @@ const NodeWithProfileForm = ({ kind, currentProfiles, ...props }: ObjectFormProp

return (
<>
{nodeSchema.generate_profile && (
{"generate_profile" in nodeSchema && nodeSchema.generate_profile && (
<ProfilesSelector
schema={nodeSchema}
defaultValue={currentProfiles}
Expand All @@ -146,121 +142,10 @@ const NodeWithProfileForm = ({ kind, currentProfiles, ...props }: ObjectFormProp
);
};

type ProfilesSelectorProps = {
schema: iNodeSchema;
value?: any[];
defaultValue?: any[];
onChange: (item: any[]) => 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 <ErrorScreen message="Something went wrong while fetching profiles" />;

const queryString = getProfiles({ profiles: profilesList });

const query = gql`
${queryString}
`;

const { data, error, loading } = useQuery(query);

if (loading) return <LoadingScreen size={30} hideText className="p-4 pb-0" />;

if (error) return <ErrorScreen message={error.message} />;

// 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 (
<div className="p-4 bg-gray-100">
<Label htmlFor={id}>
Select profiles <span className="text-xs italic text-gray-500 ml-1">optional</span>
</Label>

<MultiCombobox id={id} items={items} onChange={handleChange} value={selectedValues} />
</div>
);
};

type NodeFormProps = {
className?: string;
schema: iNodeSchema | IProfileSchema;
profiles?: Profile[];
profiles?: ProfileData[];
onSuccess?: (newObject: any) => void;
currentObject?: Record<string, AttributeType>;
isFilterForm?: boolean;
Expand All @@ -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,
});
Expand Down
127 changes: 127 additions & 0 deletions frontend/app/src/components/form/profiles-selector.tsx
Original file line number Diff line number Diff line change
@@ -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 <ErrorScreen message="Something went wrong while fetching profiles" />;

const queryString = getProfiles({ profiles: profilesList });

const query = gql`
${queryString}
`;

const { data, error, loading } = useQuery(query);

if (loading) return <LoadingScreen size={30} hideText className="p-4 pb-0" />;

if (error) return <ErrorScreen message={error.message} />;

// 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 (
<div className="p-4 bg-gray-100">
<Label htmlFor={id}>
Select profiles <span className="text-xs italic text-gray-500 ml-1">optional</span>
</Label>

<MultiCombobox id={id} items={items} onChange={handleChange} value={selectedValues} />
</div>
);
};
55 changes: 55 additions & 0 deletions frontend/app/src/components/form/utils/getFieldDefaultValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FieldSchema, AttributeType } from "@/utils/getObjectItemDisplayValue";
import { ProfileData } from "@/components/form/object-form";

export type GetFieldDefaultValue = {
fieldSchema: FieldSchema;
initialObject?: Record<string, AttributeType>;
profiles?: Array<ProfileData>;
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<string, AttributeType>) => {
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<ProfileData>) => {
// 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;
};
Loading

0 comments on commit 5f5e402

Please sign in to comment.