Migrate to a monorepo structure (#2909)
This commit is contained in:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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' },
|
||||
},
|
||||
};
|
||||
@ -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' },
|
||||
},
|
||||
};
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
},
|
||||
};
|
||||
@ -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',
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user