Migrate to a monorepo structure (#2909)

This commit is contained in:
Charles Bochet
2023-12-10 18:10:54 +01:00
committed by GitHub
parent a70a9281eb
commit 5bdca9de6c
2304 changed files with 37152 additions and 25869 deletions

View File

@ -0,0 +1,162 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
import { SettingsAvailableStandardObjectsSection } from '@/settings/data-model/new-object/components/SettingsAvailableStandardObjectsSection';
import {
NewObjectType,
SettingsNewObjectType,
} from '@/settings/data-model/new-object/components/SettingsNewObjectType';
import { SettingsObjectIconSection } from '@/settings/data-model/object-edit/SettingsObjectIconSection';
import { IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
export const SettingsNewObject = () => {
const navigate = useNavigate();
const [selectedObjectType, setSelectedObjectType] =
useState<NewObjectType>('Standard');
const { enqueueSnackBar } = useSnackBar();
const {
activateObjectMetadataItem: activateObject,
createObjectMetadataItem: createObject,
disabledObjectMetadataItems: disabledObjects,
} = useObjectMetadataItemForSettings();
const [
selectedStandardObjectMetadataIds,
setSelectedStandardObjectMetadataIds,
] = useState<Record<string, boolean>>({});
const [customFormValues, setCustomFormValues] = useState<{
description?: string;
icon: string;
labelPlural: string;
labelSingular: string;
}>({ icon: 'IconPigMoney', labelPlural: '', labelSingular: '' });
const canSave =
(selectedObjectType === 'Standard' &&
Object.values(selectedStandardObjectMetadataIds).some(
(isSelected) => isSelected,
)) ||
(selectedObjectType === 'Custom' &&
!!customFormValues.labelPlural &&
!!customFormValues.labelSingular);
const handleSave = async () => {
if (selectedObjectType === 'Standard') {
await Promise.all(
Object.entries(selectedStandardObjectMetadataIds).map(
([standardObjectMetadataId, isSelected]) =>
isSelected
? activateObject({ id: standardObjectMetadataId })
: undefined,
),
);
navigate('/settings/objects');
}
if (selectedObjectType === 'Custom') {
try {
const createdObject = await createObject({
labelPlural: customFormValues.labelPlural,
labelSingular: customFormValues.labelSingular,
description: customFormValues.description,
icon: customFormValues.icon,
});
navigate(
createdObject.data?.createOneObject.isActive
? `/settings/objects/${getObjectSlug(
createdObject.data.createOneObject,
)}`
: '/settings/objects',
);
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: 'error',
});
}
}
};
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{ children: 'New' },
]}
/>
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => {
navigate('/settings/objects');
}}
onSave={handleSave}
/>
</SettingsHeaderContainer>
<Section>
<H2Title
title="Object type"
description="The type of object you want to add"
/>
<SettingsNewObjectType
selectedType={selectedObjectType}
onTypeSelect={setSelectedObjectType}
/>
</Section>
{selectedObjectType === 'Standard' && (
<SettingsAvailableStandardObjectsSection
objectItems={disabledObjects.filter(({ isCustom }) => !isCustom)}
onChange={(selectedIds) =>
setSelectedStandardObjectMetadataIds((previousSelectedIds) => ({
...previousSelectedIds,
...selectedIds,
}))
}
selectedIds={selectedStandardObjectMetadataIds}
/>
)}
{selectedObjectType === 'Custom' && (
<>
<SettingsObjectIconSection
label={customFormValues.labelPlural}
iconKey={customFormValues.icon}
onChange={({ iconKey }) => {
setCustomFormValues((previousValues) => ({
...previousValues,
icon: iconKey,
}));
}}
/>
<SettingsObjectFormSection
singularName={customFormValues.labelSingular}
pluralName={customFormValues.labelPlural}
description={customFormValues.description}
onChange={(formValues) => {
setCustomFormValues((previousValues) => ({
...previousValues,
...formValues,
}));
}}
/>
</>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,156 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsAboutSection } from '@/settings/data-model/object-details/components/SettingsObjectAboutSection';
import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown';
import { SettingsObjectFieldDisabledActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown';
import {
SettingsObjectFieldItemTableRow,
StyledObjectFieldTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
import { AppPath } from '@/types/AppPath';
import { IconPlus, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
const StyledDiv = styled.div`
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsObjectDetail = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { disableObjectMetadataItem, findActiveObjectMetadataItemBySlug } =
useObjectMetadataItemForSettings();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
useEffect(() => {
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
}, [activeObjectMetadataItem, navigate]);
const { activateMetadataField, disableMetadataField, eraseMetadataField } =
useFieldMetadataItem();
if (!activeObjectMetadataItem) return null;
const activeMetadataFields = activeObjectMetadataItem.fields.filter(
(metadataField) => metadataField.isActive && !metadataField.isSystem,
);
const disabledMetadataFields = activeObjectMetadataItem.fields.filter(
(metadataField) => !metadataField.isActive && !metadataField.isSystem,
);
const handleDisable = async () => {
await disableObjectMetadataItem(activeObjectMetadataItem);
navigate('/settings/objects');
};
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{ children: activeObjectMetadataItem.labelPlural },
]}
/>
<SettingsAboutSection
iconKey={activeObjectMetadataItem.icon ?? undefined}
name={activeObjectMetadataItem.labelPlural || ''}
isCustom={activeObjectMetadataItem.isCustom}
onDisable={handleDisable}
onEdit={() => navigate('./edit')}
/>
<Section>
<H2Title
title="Fields"
description={`Customise the fields available in the ${activeObjectMetadataItem.labelSingular} views and their display order in the ${activeObjectMetadataItem.labelSingular} detail view and menus.`}
/>
<Table>
<StyledObjectFieldTableRow>
<TableHeader>Name</TableHeader>
<TableHeader>Field type</TableHeader>
<TableHeader>Data type</TableHeader>
<TableHeader></TableHeader>
</StyledObjectFieldTableRow>
{!!activeMetadataFields.length && (
<TableSection title="Active">
{activeMetadataFields.map((activeMetadataField) => (
<SettingsObjectFieldItemTableRow
key={activeMetadataField.id}
fieldMetadataItem={activeMetadataField}
ActionIcon={
<SettingsObjectFieldActiveActionDropdown
isCustomField={!!activeMetadataField.isCustom}
scopeKey={activeMetadataField.id}
onEdit={() =>
navigate(`./${getFieldSlug(activeMetadataField)}`)
}
onDisable={() =>
disableMetadataField(activeMetadataField)
}
/>
}
/>
))}
</TableSection>
)}
{!!disabledMetadataFields.length && (
<TableSection isInitiallyExpanded={false} title="Disabled">
{disabledMetadataFields.map((disabledMetadataField) => (
<SettingsObjectFieldItemTableRow
key={disabledMetadataField.id}
fieldMetadataItem={disabledMetadataField}
ActionIcon={
<SettingsObjectFieldDisabledActionDropdown
isCustomField={!!disabledMetadataField.isCustom}
scopeKey={disabledMetadataField.id}
onActivate={() =>
activateMetadataField(disabledMetadataField)
}
onErase={() =>
eraseMetadataField(disabledMetadataField)
}
/>
}
/>
))}
</TableSection>
)}
</Table>
<StyledDiv>
<Button
Icon={IconPlus}
title="Add Field"
size="small"
variant="secondary"
onClick={() =>
navigate(
disabledMetadataFields.length
? './new-field/step-1'
: './new-field/step-2',
)
}
/>
</StyledDiv>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,151 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFormSection } from '@/settings/data-model/components/SettingsObjectFormSection';
import { SettingsObjectIconSection } from '@/settings/data-model/object-edit/SettingsObjectIconSection';
import { AppPath } from '@/types/AppPath';
import { IconArchive, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Button } from '@/ui/input/button/components/Button';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
export const SettingsObjectEdit = () => {
const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar();
const { objectSlug = '' } = useParams();
const {
disableObjectMetadataItem,
editObjectMetadataItem,
findActiveObjectMetadataItemBySlug,
} = useObjectMetadataItemForSettings();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
const [formValues, setFormValues] = useState<
Partial<{
icon: string;
labelSingular: string;
labelPlural: string;
description: string;
}>
>({});
useEffect(() => {
if (!activeObjectMetadataItem) {
navigate(AppPath.NotFound);
return;
}
if (!Object.keys(formValues).length) {
setFormValues({
icon: activeObjectMetadataItem.icon ?? undefined,
labelSingular: activeObjectMetadataItem.labelSingular,
labelPlural: activeObjectMetadataItem.labelPlural,
description: activeObjectMetadataItem.description ?? undefined,
});
}
}, [activeObjectMetadataItem, formValues, navigate]);
if (!activeObjectMetadataItem) return null;
const areRequiredFieldsFilled =
!!formValues.labelSingular && !!formValues.labelPlural;
const hasChanges =
formValues.description !== activeObjectMetadataItem.description ||
formValues.icon !== activeObjectMetadataItem.icon ||
formValues.labelPlural !== activeObjectMetadataItem.labelPlural ||
formValues.labelSingular !== activeObjectMetadataItem.labelSingular;
const canSave = areRequiredFieldsFilled && hasChanges;
const handleSave = async () => {
const editedObjectMetadataItem = {
...activeObjectMetadataItem,
...formValues,
};
try {
await editObjectMetadataItem(editedObjectMetadataItem);
navigate(`/settings/objects/${getObjectSlug(editedObjectMetadataItem)}`);
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: 'error',
});
}
};
const handleDisable = async () => {
await disableObjectMetadataItem(activeObjectMetadataItem);
navigate('/settings/objects');
};
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'Edit' },
]}
/>
{activeObjectMetadataItem.isCustom && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
/>
)}
</SettingsHeaderContainer>
<SettingsObjectIconSection
disabled={!activeObjectMetadataItem.isCustom}
iconKey={formValues.icon}
label={formValues.labelPlural}
onChange={({ iconKey }) =>
setFormValues((previousFormValues) => ({
...previousFormValues,
icon: iconKey,
}))
}
/>
<SettingsObjectFormSection
disabled={!activeObjectMetadataItem.isCustom}
singularName={formValues.labelSingular}
pluralName={formValues.labelPlural}
description={formValues.description}
onChange={(values) =>
setFormValues((previousFormValues) => ({
...previousFormValues,
...values,
}))
}
/>
<Section>
<H2Title title="Danger zone" description="Disable object" />
<Button
Icon={IconArchive}
title="Disable"
size="small"
onClick={handleDisable}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,207 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useRelationMetadata } from '@/object-metadata/hooks/useRelationMetadata';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
import { AppPath } from '@/types/AppPath';
import { IconArchive, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Button } from '@/ui/input/button/components/Button';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import {
FieldMetadataType,
RelationMetadataType,
} from '~/generated-metadata/graphql';
export const SettingsObjectFieldEdit = () => {
const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar();
const { objectSlug = '', fieldSlug = '' } = useParams();
const { findActiveObjectMetadataItemBySlug } =
useObjectMetadataItemForSettings();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
const { disableMetadataField, editMetadataField } = useFieldMetadataItem();
const activeMetadataField = activeObjectMetadataItem?.fields.find(
(metadataField) =>
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
);
const {
relationFieldMetadataItem,
relationObjectMetadataItem,
relationType,
} = useRelationMetadata({ fieldMetadataItem: activeMetadataField });
const {
formValues,
handleFormChange,
hasFieldFormChanged,
hasFormChanged,
hasRelationFormChanged,
hasSelectFormChanged,
initForm,
isInitialized,
isValid,
validatedFormValues,
} = useFieldMetadataForm();
useEffect(() => {
if (!activeObjectMetadataItem || !activeMetadataField) {
navigate(AppPath.NotFound);
return;
}
const selectOptions = activeMetadataField.options?.map((option) => ({
...option,
isDefault: activeMetadataField.defaultValue === option.value,
}));
selectOptions?.sort(
(optionA, optionB) => optionA.position - optionB.position,
);
initForm({
icon: activeMetadataField.icon ?? undefined,
label: activeMetadataField.label,
description: activeMetadataField.description ?? undefined,
type: activeMetadataField.type,
relation: {
field: {
icon: relationFieldMetadataItem?.icon,
label: relationFieldMetadataItem?.label || '',
},
objectMetadataId: relationObjectMetadataItem?.id || '',
type: relationType || RelationMetadataType.OneToMany,
},
...(selectOptions?.length ? { select: selectOptions } : {}),
});
}, [
activeMetadataField,
activeObjectMetadataItem,
initForm,
navigate,
relationFieldMetadataItem?.icon,
relationFieldMetadataItem?.label,
relationObjectMetadataItem?.id,
relationType,
]);
if (!isInitialized || !activeObjectMetadataItem || !activeMetadataField)
return null;
const canSave = isValid && hasFormChanged;
const handleSave = async () => {
if (!validatedFormValues) return;
try {
if (
validatedFormValues.type === FieldMetadataType.Relation &&
relationFieldMetadataItem?.id &&
hasRelationFormChanged
) {
await editMetadataField({
icon: validatedFormValues.relation.field.icon,
id: relationFieldMetadataItem.id,
label: validatedFormValues.relation.field.label,
});
}
if (hasFieldFormChanged || hasSelectFormChanged) {
await editMetadataField({
description: validatedFormValues.description,
icon: validatedFormValues.icon,
id: activeMetadataField.id,
label: validatedFormValues.label,
options:
validatedFormValues.type === FieldMetadataType.Select
? validatedFormValues.select
: undefined,
});
}
navigate(`/settings/objects/${objectSlug}`);
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: 'error',
});
}
};
const handleDisable = async () => {
await disableMetadataField(activeMetadataField);
navigate(`/settings/objects/${objectSlug}`);
};
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: activeMetadataField.label },
]}
/>
{activeMetadataField.isCustom && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
/>
)}
</SettingsHeaderContainer>
<SettingsObjectFieldFormSection
disabled={!activeMetadataField.isCustom}
disableNameEdition
name={formValues.label}
description={formValues.description}
iconKey={formValues.icon}
onChange={handleFormChange}
/>
<SettingsObjectFieldTypeSelectSection
fieldMetadata={{
icon: formValues.icon,
label: formValues.label || 'Employees',
id: activeMetadataField.id,
}}
objectMetadataId={activeObjectMetadataItem.id}
onChange={handleFormChange}
relationFieldMetadata={relationFieldMetadataItem}
values={{
type: formValues.type,
relation: formValues.relation,
select: formValues.select,
}}
/>
<Section>
<H2Title title="Danger zone" description="Disable this field" />
<Button
Icon={IconArchive}
title="Disable"
size="small"
onClick={handleDisable}
/>
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,182 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import {
SettingsObjectFieldItemTableRow,
StyledObjectFieldTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
import { AppPath } from '@/types/AppPath';
import { IconMinus, IconPlus, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
const StyledSection = styled(Section)`
display: flex;
flex-direction: column;
`;
const StyledAddCustomFieldButton = styled(Button)`
align-self: flex-end;
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsObjectNewFieldStep1 = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { findActiveObjectMetadataItemBySlug } =
useObjectMetadataItemForSettings();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
const { activateMetadataField, disableMetadataField } =
useFieldMetadataItem();
const [metadataFields, setMetadataFields] = useState(
activeObjectMetadataItem?.fields ?? [],
);
const activeMetadataFields = metadataFields.filter((field) => field.isActive);
const disabledMetadataFields = metadataFields.filter(
(field) => !field.isActive,
);
const canSave = metadataFields.some(
(field, index) =>
field.isActive !== activeObjectMetadataItem?.fields[index].isActive,
);
useEffect(() => {
if (!activeObjectMetadataItem) {
navigate(AppPath.NotFound);
return;
}
if (!metadataFields.length)
setMetadataFields(activeObjectMetadataItem.fields);
}, [activeObjectMetadataItem, metadataFields.length, navigate]);
if (!activeObjectMetadataItem) return null;
const handleToggleField = (fieldMetadataId: string) =>
setMetadataFields((previousFields) =>
previousFields.map((field) =>
field.id === fieldMetadataId
? { ...field, isActive: !field.isActive }
: field,
),
);
const handleSave = async () => {
await Promise.all(
metadataFields.map((metadataField, index) => {
if (
metadataField.isActive ===
activeObjectMetadataItem.fields[index].isActive
) {
return;
}
return metadataField.isActive
? activateMetadataField(metadataField)
: disableMetadataField(metadataField);
}),
);
navigate(`/settings/objects/${objectSlug}`);
};
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'New Field' },
]}
/>
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
/>
</SettingsHeaderContainer>
<StyledSection>
<H2Title
title="Check disabled fields"
description="Before creating a custom field, check if it already exists in the disabled section."
/>
<Table>
<StyledObjectFieldTableRow>
<TableHeader>Name</TableHeader>
<TableHeader>Field type</TableHeader>
<TableHeader>Data type</TableHeader>
<TableHeader></TableHeader>
</StyledObjectFieldTableRow>
{!!activeMetadataFields.length && (
<TableSection isInitiallyExpanded={false} title="Active">
{activeMetadataFields.map((field) => (
<SettingsObjectFieldItemTableRow
key={field.id}
fieldMetadataItem={field}
ActionIcon={
<LightIconButton
Icon={IconMinus}
accent="tertiary"
onClick={() => handleToggleField(field.id)}
/>
}
/>
))}
</TableSection>
)}
{!!disabledMetadataFields.length && (
<TableSection title="Disabled">
{disabledMetadataFields.map((field) => (
<SettingsObjectFieldItemTableRow
key={field.name}
fieldMetadataItem={field}
ActionIcon={
<LightIconButton
Icon={IconPlus}
accent="tertiary"
onClick={() => handleToggleField(field.id)}
/>
}
/>
))}
</TableSection>
)}
</Table>
<StyledAddCustomFieldButton
Icon={IconPlus}
title="Add Custom Field"
size="small"
variant="secondary"
onClick={() =>
navigate(`/settings/objects/${objectSlug}/new-field/step-2`)
}
/>
</StyledSection>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,304 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCreateOneRelationMetadataItem';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { PaginatedRecordTypeResults } from '@/object-record/types/PaginatedRecordTypeResults';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection';
import { useFieldMetadataForm } from '@/settings/data-model/hooks/useFieldMetadataForm';
import { AppPath } from '@/types/AppPath';
import { IconSettings } from '@/ui/display/icon';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { View } from '@/views/types/View';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const SettingsObjectNewFieldStep2 = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { enqueueSnackBar } = useSnackBar();
const {
findActiveObjectMetadataItemBySlug,
findObjectMetadataItemById,
findObjectMetadataItemByNamePlural,
} = useObjectMetadataItemForSettings();
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
const { createMetadataField } = useFieldMetadataItem();
const isRelationFieldTypeEnabled = useIsFeatureEnabled(
'IS_RELATION_FIELD_TYPE_ENABLED',
);
const {
formValues,
handleFormChange,
initForm,
isValid: canSave,
validatedFormValues,
} = useFieldMetadataForm();
useEffect(() => {
if (!activeObjectMetadataItem) {
navigate(AppPath.NotFound);
return;
}
initForm({
relation: {
field: { icon: activeObjectMetadataItem.icon },
objectMetadataId:
findObjectMetadataItemByNamePlural('people')?.id || '',
},
});
}, [
activeObjectMetadataItem,
findObjectMetadataItemByNamePlural,
initForm,
navigate,
]);
const [objectViews, setObjectViews] = useState<View[]>([]);
const [relationObjectViews, setRelationObjectViews] = useState<View[]>([]);
const { modifyRecordFromCache: modifyViewFromCache } = useObjectMetadataItem({
objectNameSingular: 'view',
});
useFindManyRecords({
objectNameSingular: 'view',
filter: {
type: { eq: ViewType.Table },
objectMetadataId: { eq: activeObjectMetadataItem?.id },
},
onCompleted: async (data: PaginatedRecordTypeResults<View>) => {
const views = data.edges;
if (!views) return;
setObjectViews(data.edges.map(({ node }) => node));
},
});
useFindManyRecords({
objectNameSingular: 'view',
skip: !formValues.relation?.objectMetadataId,
filter: {
type: { eq: ViewType.Table },
objectMetadataId: { eq: formValues.relation?.objectMetadataId },
},
onCompleted: async (data: PaginatedRecordTypeResults<View>) => {
const views = data.edges;
if (!views) return;
setRelationObjectViews(data.edges.map(({ node }) => node));
},
});
const { createOneRelationMetadataItem: createOneRelationMetadata } =
useCreateOneRelationMetadataItem();
if (!activeObjectMetadataItem) return null;
const handleSave = async () => {
if (!validatedFormValues) return;
try {
if (validatedFormValues.type === FieldMetadataType.Relation) {
const createdRelation = await createOneRelationMetadata({
relationType: validatedFormValues.relation.type,
field: {
description: validatedFormValues.description,
icon: validatedFormValues.icon,
label: validatedFormValues.label,
},
objectMetadataId: activeObjectMetadataItem.id,
connect: {
field: {
icon: validatedFormValues.relation.field.icon,
label: validatedFormValues.relation.field.label,
},
objectMetadataId: validatedFormValues.relation.objectMetadataId,
},
});
const relationObjectMetadataItem = findObjectMetadataItemById(
validatedFormValues.relation.objectMetadataId,
);
objectViews.forEach(async (view) => {
const viewFieldToCreate = {
viewId: view.id,
fieldMetadataId:
validatedFormValues.relation.type === 'MANY_TO_ONE'
? createdRelation.data?.createOneRelation.toFieldMetadataId
: createdRelation.data?.createOneRelation.fromFieldMetadataId,
position: activeObjectMetadataItem.fields.length,
isVisible: true,
size: 100,
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFields: any) => {
return {
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
};
},
});
});
relationObjectViews.forEach(async (view) => {
const viewFieldToCreate = {
viewId: view.id,
fieldMetadataId:
validatedFormValues.relation.type === 'MANY_TO_ONE'
? createdRelation.data?.createOneRelation.fromFieldMetadataId
: createdRelation.data?.createOneRelation.toFieldMetadataId,
position: relationObjectMetadataItem?.fields.length,
isVisible: true,
size: 100,
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFields: any) => {
return {
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
};
},
});
});
} else {
const createdMetadataField = await createMetadataField({
description: validatedFormValues.description,
icon: validatedFormValues.icon,
label: validatedFormValues.label ?? '',
objectMetadataId: activeObjectMetadataItem.id,
type: validatedFormValues.type,
options:
validatedFormValues.type === FieldMetadataType.Select
? validatedFormValues.select
: undefined,
});
objectViews.forEach(async (view) => {
const viewFieldToCreate = {
viewId: view.id,
fieldMetadataId: createdMetadataField.data?.createOneField.id,
position: activeObjectMetadataItem.fields.length,
isVisible: true,
size: 100,
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFields: any) => {
return {
edges: viewFields.edges.concat({ node: viewFieldToCreate }),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '',
endCursor: '',
},
};
},
});
});
}
navigate(`/settings/objects/${objectSlug}`);
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: 'error',
});
}
};
const excludedFieldTypes = [
FieldMetadataType.Currency,
FieldMetadataType.Email,
FieldMetadataType.FullName,
FieldMetadataType.Link,
FieldMetadataType.MultiSelect,
FieldMetadataType.Numeric,
FieldMetadataType.Phone,
FieldMetadataType.Probability,
FieldMetadataType.Rating,
FieldMetadataType.Select,
FieldMetadataType.Uuid,
];
if (!isRelationFieldTypeEnabled) {
excludedFieldTypes.push(FieldMetadataType.Relation);
}
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObjectMetadataItem.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'New Field' },
]}
/>
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
onSave={handleSave}
/>
</SettingsHeaderContainer>
<SettingsObjectFieldFormSection
iconKey={formValues.icon}
name={formValues.label}
description={formValues.description}
onChange={handleFormChange}
/>
<SettingsObjectFieldTypeSelectSection
excludedFieldTypes={excludedFieldTypes}
fieldMetadata={{
icon: formValues.icon,
label: formValues.label || 'Employees',
}}
objectMetadataId={activeObjectMetadataItem.id}
onChange={handleFormChange}
values={{
type: formValues.type,
relation: formValues.relation,
select: formValues.select,
}}
/>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,126 @@
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import {
SettingsObjectItemTableRow,
StyledObjectTableRow,
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage';
import { SettingsObjectDisabledMenuDropDown } from '@/settings/data-model/objects/SettingsObjectDisabledMenuDropDown';
import { IconChevronRight, IconPlus, IconSettings } from '@/ui/display/icon';
import { H1Title } from '@/ui/display/typography/components/H1Title';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Button } from '@/ui/input/button/components/Button';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableSection } from '@/ui/layout/table/components/TableSection';
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledH1Title = styled(H1Title)`
margin-bottom: 0;
`;
export const SettingsObjects = () => {
const theme = useTheme();
const navigate = useNavigate();
const {
activateObjectMetadataItem,
activeObjectMetadataItems,
disabledObjectMetadataItems,
eraseObjectMetadataItem,
} = useObjectMetadataItemForSettings();
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
<SettingsHeaderContainer>
<StyledH1Title title="Objects" />
<Button
Icon={IconPlus}
title="New object"
accent="blue"
size="small"
onClick={() => navigate('/settings/objects/new')}
/>
</SettingsHeaderContainer>
<div>
<SettingsObjectCoverImage />
<Section>
<H2Title title="Existing objects" />
<Table>
<StyledObjectTableRow>
<TableHeader>Name</TableHeader>
<TableHeader>Type</TableHeader>
<TableHeader align="right">Fields</TableHeader>
<TableHeader align="right">Instances</TableHeader>
<TableHeader></TableHeader>
</StyledObjectTableRow>
{!!activeObjectMetadataItems.length && (
<TableSection title="Active">
{activeObjectMetadataItems.map((activeObjectMetadataItem) => (
<SettingsObjectItemTableRow
key={activeObjectMetadataItem.namePlural}
objectItem={activeObjectMetadataItem}
action={
<StyledIconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
}
onClick={() =>
navigate(
`/settings/objects/${getObjectSlug(
activeObjectMetadataItem,
)}`,
)
}
/>
))}
</TableSection>
)}
{!!disabledObjectMetadataItems.length && (
<TableSection title="Disabled">
{disabledObjectMetadataItems.map(
(disabledObjectMetadataItem) => (
<SettingsObjectItemTableRow
key={disabledObjectMetadataItem.namePlural}
objectItem={disabledObjectMetadataItem}
action={
<SettingsObjectDisabledMenuDropDown
isCustomObject={disabledObjectMetadataItem.isCustom}
scopeKey={disabledObjectMetadataItem.namePlural}
onActivate={() =>
activateObjectMetadataItem(
disabledObjectMetadataItem,
)
}
onErase={() =>
eraseObjectMetadataItem(
disabledObjectMetadataItem,
)
}
/>
}
/>
),
)}
</TableSection>
)}
</Table>
</Section>
</div>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -0,0 +1,45 @@
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/test';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
import { SettingsNewObject } from '../SettingsNewObject';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/DataModel/SettingsNewObject',
component: SettingsNewObject,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/new',
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsNewObject>;
export const WithStandardSelected: Story = {
play: async () => {
await sleep(100);
},
};
export const WithCustomSelected: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await sleep(1000);
const customButtonElement = canvas.getByText('Custom');
await userEvent.click(customButtonElement);
},
};

View File

@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
import { SettingsObjectDetail } from '../SettingsObjectDetail';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/DataModel/SettingsObjectDetail',
component: SettingsObjectDetail,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/:objectSlug',
routeParams: { ':objectSlug': 'companies' },
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsObjectDetail>;
export const StandardObject: Story = {
play: async () => {
await sleep(100);
},
};
export const CustomObject: Story = {
args: {
routeParams: { ':objectSlug': 'workspaces' },
},
};

View File

@ -0,0 +1,39 @@
import { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
import { SettingsObjectEdit } from '../SettingsObjectEdit';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/DataModel/SettingsObjectEdit',
component: SettingsObjectEdit,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/:objectSlug/edit',
routeParams: { ':objectSlug': 'companies' },
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsObjectEdit>;
export const StandardObject: Story = {
play: async () => {
await sleep(100);
},
};
export const CustomObject: Story = {
args: {
routeParams: { ':objectSlug': 'workspaces' },
},
};

View File

@ -0,0 +1,37 @@
import { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { SettingsObjectFieldEdit } from '../SettingsObjectFieldEdit';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/DataModel/SettingsObjectFieldEdit',
component: SettingsObjectFieldEdit,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/:objectSlug/:fieldSlug',
routeParams: { ':objectSlug': 'companies', ':fieldSlug': 'name' },
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsObjectFieldEdit>;
export const StandardField: Story = {};
export const CustomField: Story = {
args: {
routeParams: {
':objectSlug': 'companies',
':fieldSlug': 'employees',
},
},
};

View File

@ -0,0 +1,34 @@
import { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
import { SettingsObjectNewFieldStep1 } from '../../SettingsObjectNewField/SettingsObjectNewFieldStep1';
const meta: Meta<PageDecoratorArgs> = {
title:
'Pages/Settings/DataModel/SettingsObjectNewField/SettingsObjectNewFieldStep1',
component: SettingsObjectNewFieldStep1,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/:objectSlug/new-field/step-1',
routeParams: { ':objectSlug': 'companies' },
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsObjectNewFieldStep1>;
export const Default: Story = {
play: async () => {
await sleep(100);
},
};

View File

@ -0,0 +1,34 @@
import { Meta, StoryObj } from '@storybook/react';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
import { SettingsObjectNewFieldStep2 } from '../../SettingsObjectNewField/SettingsObjectNewFieldStep2';
const meta: Meta<PageDecoratorArgs> = {
title:
'Pages/Settings/DataModel/SettingsObjectNewField/SettingsObjectNewFieldStep2',
component: SettingsObjectNewFieldStep2,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/:objectSlug/new-field/step-2',
routeParams: { ':objectSlug': 'companies' },
},
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsObjectNewFieldStep2>;
export const Default: Story = {
play: async () => {
await sleep(100);
},
};

View File

@ -0,0 +1,38 @@
import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test';
import {
PageDecorator,
PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/testing/sleep';
import { SettingsObjects } from '../SettingsObjects';
const meta: Meta<PageDecoratorArgs> = {
title: 'Pages/Settings/DataModel/SettingsObjects',
component: SettingsObjects,
decorators: [PageDecorator],
args: { routePath: '/settings/objects' },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
export type Story = StoryObj<typeof SettingsObjects>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await sleep(1000);
await canvas.getByRole('heading', {
level: 2,
name: 'Objects',
});
},
};