feat: add Settings Object Edit identifiers form (#4300)

* feat: add Settings Object Edit identifiers form

Closes #3836

* fix: fix wrong imports after renaming directories
This commit is contained in:
Thaïs 2024-03-08 17:55:30 -03:00 committed by GitHub
parent 40a3b7d849
commit 40bea0d95e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 212 additions and 24 deletions

View File

@ -0,0 +1,6 @@
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const LABEL_IDENTIFIER_FIELD_METADATA_TYPES = [
FieldMetadataType.Number,
FieldMetadataType.Text,
];

View File

@ -0,0 +1,9 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActiveFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
) =>
objectMetadataItem.fields.filter(
(fieldMetadataItem) =>
fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
);

View File

@ -0,0 +1,9 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getDisabledFieldMetadataItems = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
) =>
objectMetadataItem.fields.filter(
(fieldMetadataItem) =>
!fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
);

View File

@ -0,0 +1,11 @@
import styled from '@emotion/styled';
const StyledTitle = styled.h3`
color: ${({ theme }) => theme.font.color.extraLight};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin: 0;
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
export { StyledTitle as SettingsDataModelCardTitle };

View File

@ -0,0 +1,101 @@
import { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import styled from '@emotion/styled';
import { z } from 'zod';
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema';
import { IconCircleOff } from '@/ui/display/icon';
import { useIcons } from '@/ui/display/icon/hooks/useIcons';
import { Select, SelectOption } from '@/ui/input/components/Select';
export const settingsDataModelObjectIdentifiersFormSchema =
objectMetadataItemSchema.pick({
labelIdentifierFieldMetadataId: true,
imageIdentifierFieldMetadataId: true,
});
export type SettingsDataModelObjectIdentifiersFormValues = z.infer<
typeof settingsDataModelObjectIdentifiersFormSchema
>;
type SettingsDataModelObjectIdentifiersFormProps = {
objectMetadataItem: ObjectMetadataItem;
};
const StyledContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsDataModelObjectIdentifiersForm = ({
objectMetadataItem,
}: SettingsDataModelObjectIdentifiersFormProps) => {
const { control } =
useFormContext<SettingsDataModelObjectIdentifiersFormValues>();
const { getIcon } = useIcons();
const labelIdentifierFieldOptions = useMemo(
() =>
getActiveFieldMetadataItems(objectMetadataItem)
.filter(
({ id, type }) =>
LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(type) ||
objectMetadataItem.labelIdentifierFieldMetadataId === id,
)
.map<SelectOption<string | null>>((fieldMetadataItem) => ({
Icon: getIcon(fieldMetadataItem.icon),
label: fieldMetadataItem.label,
value: fieldMetadataItem.id,
})),
[getIcon, objectMetadataItem],
);
const imageIdentifierFieldOptions: SelectOption<string | null>[] = [];
const emptyOption: SelectOption<string | null> = {
Icon: IconCircleOff,
label: 'None',
value: null,
};
return (
<StyledContainer>
{[
{
label: 'Record label',
fieldName: 'labelIdentifierFieldMetadataId' as const,
options: labelIdentifierFieldOptions,
},
{
label: 'Record image',
fieldName: 'imageIdentifierFieldMetadataId' as const,
options: imageIdentifierFieldOptions,
},
].map(({ fieldName, label, options }) => (
<Controller
key={fieldName}
name={fieldName}
control={control}
defaultValue={objectMetadataItem[fieldName]}
render={({ field: { onBlur, onChange, value } }) => {
return (
<Select
label={label}
disabled={!objectMetadataItem.isCustom || !options.length}
fullWidth
dropdownId={`${fieldName}-select`}
emptyOption={emptyOption}
options={options}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
}}
/>
))}
</StyledContainer>
);
};

View File

@ -1,9 +1,15 @@
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; import { SettingsDataModelCardTitle } from '@/settings/data-model/components/SettingsDataModelCardTitle';
import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard';
import {
SettingsDataModelObjectIdentifiersForm,
SettingsDataModelObjectIdentifiersFormValues,
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/SettingsDataModelObjectSummary'; import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/SettingsDataModelObjectSummary';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
@ -16,6 +22,10 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
width: 100%; width: 100%;
`; `;
const StyledTopCardContent = styled(CardContent)`
background-color: ${({ theme }) => theme.background.transparent.lighter};
`;
const StyledObjectSummaryCard = styled(Card)` const StyledObjectSummaryCard = styled(Card)`
border-radius: ${({ theme }) => theme.border.radius.md}; border-radius: ${({ theme }) => theme.border.radius.md};
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
@ -29,13 +39,27 @@ const StyledObjectSummaryCardContent = styled(CardContent)`
export const SettingsDataModelObjectSettingsFormCard = ({ export const SettingsDataModelObjectSettingsFormCard = ({
objectMetadataItem, objectMetadataItem,
}: SettingsDataModelObjectSettingsFormCardProps) => { }: SettingsDataModelObjectSettingsFormCardProps) => {
const labelIdentifierFieldMetadataItem = const { watch: watchFormValue } =
getLabelIdentifierFieldMetadataItem(objectMetadataItem); useFormContext<SettingsDataModelObjectIdentifiersFormValues>();
const labelIdentifierFieldMetadataIdFormValue = watchFormValue(
'labelIdentifierFieldMetadataId',
);
const labelIdentifierFieldMetadataItem = useMemo(
() =>
getLabelIdentifierFieldMetadataItem({
fields: objectMetadataItem.fields,
labelIdentifierFieldMetadataId: labelIdentifierFieldMetadataIdFormValue,
}),
[labelIdentifierFieldMetadataIdFormValue, objectMetadataItem],
);
return ( return (
<SettingsDataModelPreviewFormCard <Card fullWidth>
preview={ <StyledTopCardContent divider>
labelIdentifierFieldMetadataItem ? ( <SettingsDataModelCardTitle>Preview</SettingsDataModelCardTitle>
{labelIdentifierFieldMetadataItem ? (
<StyledFieldPreviewCard <StyledFieldPreviewCard
objectMetadataItem={objectMetadataItem} objectMetadataItem={objectMetadataItem}
fieldMetadataItem={labelIdentifierFieldMetadataItem} fieldMetadataItem={labelIdentifierFieldMetadataItem}
@ -49,8 +73,13 @@ export const SettingsDataModelObjectSettingsFormCard = ({
/> />
</StyledObjectSummaryCardContent> </StyledObjectSummaryCardContent>
</StyledObjectSummaryCard> </StyledObjectSummaryCard>
) )}
} </StyledTopCardContent>
<CardContent>
<SettingsDataModelObjectIdentifiersForm
objectMetadataItem={objectMetadataItem}
/> />
</CardContent>
</Card>
); );
}; };

View File

@ -36,6 +36,7 @@ export {
IconChevronsRight, IconChevronsRight,
IconChevronUp, IconChevronUp,
IconCircleDot, IconCircleDot,
IconCircleOff,
IconClick, IconClick,
IconCode, IconCode,
IconCoins, IconCoins,

View File

@ -24,9 +24,11 @@ export type SelectProps<Value extends string | number | null> = {
disabled?: boolean; disabled?: boolean;
dropdownId: string; dropdownId: string;
dropdownWidth?: `${string}px` | 'auto' | number; dropdownWidth?: `${string}px` | 'auto' | number;
emptyOption?: SelectOption<Value>;
fullWidth?: boolean; fullWidth?: boolean;
label?: string; label?: string;
onChange?: (value: Value) => void; onChange?: (value: Value) => void;
onBlur?: () => void;
options: SelectOption<Value>[]; options: SelectOption<Value>[];
value?: Value; value?: Value;
withSearchInput?: boolean; withSearchInput?: boolean;
@ -73,12 +75,14 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
export const Select = <Value extends string | number | null>({ export const Select = <Value extends string | number | null>({
className, className,
disabled, disabled: disabledFromProps,
dropdownId, dropdownId,
dropdownWidth = 176, dropdownWidth = 176,
emptyOption,
fullWidth, fullWidth,
label, label,
onChange, onChange,
onBlur,
options, options,
value, value,
withSearchInput, withSearchInput,
@ -87,7 +91,9 @@ export const Select = <Value extends string | number | null>({
const [searchInputValue, setSearchInputValue] = useState(''); const [searchInputValue, setSearchInputValue] = useState('');
const selectedOption = const selectedOption =
options.find(({ value: key }) => key === value) || options[0]; options.find(({ value: key }) => key === value) ||
options[0] ||
emptyOption;
const filteredOptions = useMemo( const filteredOptions = useMemo(
() => () =>
searchInputValue searchInputValue
@ -98,28 +104,37 @@ export const Select = <Value extends string | number | null>({
[options, searchInputValue], [options, searchInputValue],
); );
const isDisabled = disabledFromProps || options.length <= 1;
const { closeDropdown } = useDropdown(dropdownId); const { closeDropdown } = useDropdown(dropdownId);
const selectControl = ( const selectControl = (
<StyledControlContainer disabled={disabled}> <StyledControlContainer disabled={isDisabled}>
<StyledControlLabel> <StyledControlLabel>
{!!selectedOption?.Icon && ( {!!selectedOption?.Icon && (
<selectedOption.Icon <selectedOption.Icon
color={disabled ? theme.font.color.light : theme.font.color.primary} color={
isDisabled ? theme.font.color.light : theme.font.color.primary
}
size={theme.icon.size.md} size={theme.icon.size.md}
stroke={theme.icon.stroke.sm} stroke={theme.icon.stroke.sm}
/> />
)} )}
{selectedOption?.label} {selectedOption?.label}
</StyledControlLabel> </StyledControlLabel>
<StyledIconChevronDown disabled={disabled} size={theme.icon.size.md} /> <StyledIconChevronDown disabled={isDisabled} size={theme.icon.size.md} />
</StyledControlContainer> </StyledControlContainer>
); );
return ( return (
<StyledContainer className={className} fullWidth={fullWidth}> <StyledContainer
className={className}
fullWidth={fullWidth}
tabIndex={0}
onBlur={onBlur}
>
{!!label && <StyledLabel>{label}</StyledLabel>} {!!label && <StyledLabel>{label}</StyledLabel>}
{disabled ? ( {isDisabled ? (
selectControl selectControl
) : ( ) : (
<Dropdown <Dropdown
@ -148,6 +163,7 @@ export const Select = <Value extends string | number | null>({
text={option.label} text={option.label}
onClick={() => { onClick={() => {
onChange?.(option.value); onChange?.(option.value);
onBlur?.();
closeDropdown(); closeDropdown();
}} }}
/> />

View File

@ -75,7 +75,7 @@ export const RecordShowPage = () => {
labelIdentifierFieldValue?.firstName, labelIdentifierFieldValue?.firstName,
labelIdentifierFieldValue?.lastName, labelIdentifierFieldValue?.lastName,
].join(' ') ].join(' ')
: labelIdentifierFieldValue; : `${labelIdentifierFieldValue}`;
return ( return (
<PageContainer> <PageContainer>

View File

@ -2,10 +2,13 @@ import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
import { getDisabledFieldMetadataItems } from '@/object-metadata/utils/getDisabledFieldMetadataItems';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -29,7 +32,6 @@ import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection'; import { TableSection } from '@/ui/layout/table/components/TableSection';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { FieldMetadataType } from '~/generated-metadata/graphql';
const StyledDiv = styled.div` const StyledDiv = styled.div`
display: flex; display: flex;
@ -57,11 +59,11 @@ export const SettingsObjectDetail = () => {
if (!activeObjectMetadataItem) return null; if (!activeObjectMetadataItem) return null;
const activeMetadataFields = activeObjectMetadataItem.fields.filter( const activeMetadataFields = getActiveFieldMetadataItems(
(metadataField) => metadataField.isActive && !metadataField.isSystem, activeObjectMetadataItem,
); );
const disabledMetadataFields = activeObjectMetadataItem.fields.filter( const disabledMetadataFields = getDisabledFieldMetadataItems(
(metadataField) => !metadataField.isActive && !metadataField.isSystem, activeObjectMetadataItem,
); );
const handleDisableObject = async () => { const handleDisableObject = async () => {
@ -128,7 +130,7 @@ export const SettingsObjectDetail = () => {
const canBeSetAsLabelIdentifier = const canBeSetAsLabelIdentifier =
activeObjectMetadataItem.isCustom && activeObjectMetadataItem.isCustom &&
!isLabelIdentifier && !isLabelIdentifier &&
[FieldMetadataType.Text, FieldMetadataType.Number].includes( LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(
activeMetadataField.type, activeMetadataField.type,
); );

View File

@ -15,6 +15,7 @@ import {
SettingsDataModelObjectAboutForm, SettingsDataModelObjectAboutForm,
settingsDataModelObjectAboutFormSchema, settingsDataModelObjectAboutFormSchema,
} from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm'; } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm';
import { settingsDataModelObjectIdentifiersFormSchema } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm';
import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard'; import { SettingsDataModelObjectSettingsFormCard } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard';
import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema'; import { settingsUpdateObjectInputSchema } from '@/settings/data-model/validation-schemas/settingsUpdateObjectInputSchema';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
@ -28,7 +29,10 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
import { Section } from '@/ui/layout/section/components/Section'; import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
const objectEditFormSchema = settingsDataModelObjectAboutFormSchema; const objectEditFormSchema = z
.object({})
.merge(settingsDataModelObjectAboutFormSchema)
.merge(settingsDataModelObjectIdentifiersFormSchema);
type SettingsDataModelObjectEditFormValues = z.infer< type SettingsDataModelObjectEditFormValues = z.infer<
typeof objectEditFormSchema typeof objectEditFormSchema