feat: activate standard objects in New Object page (#2232)

* feat: activate standard objects in New Object page

Closes #2010, Closes #2173

* Pagination limit = 1000

* Various fixes

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Thaïs
2023-10-27 15:46:29 +02:00
committed by GitHub
parent ec3327ca81
commit 3c6ce75606
29 changed files with 470 additions and 343 deletions

View File

@ -2,7 +2,7 @@ import { gql } from '@apollo/client';
export const FIND_MANY_METADATA_OBJECTS = gql`
query MetadataObjects {
objects(paging: { first: 100 }) {
objects(paging: { first: 1000 }) {
edges {
node {
id
@ -17,7 +17,7 @@ export const FIND_MANY_METADATA_OBJECTS = gql`
isActive
createdAt
updatedAt
fields(paging: { first: 100 }) {
fields(paging: { first: 1000 }) {
edges {
node {
id

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import {
MetadataObjectsQuery,
MetadataObjectsQueryVariables,
@ -15,15 +16,28 @@ import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useFindManyMetadataObjects = () => {
const apolloMetadataClient = useApolloMetadataClient();
const { enqueueSnackBar } = useSnackBar();
const {
data,
fetchMore: fetchMoreInternal,
loading,
error,
} = useQuery<MetadataObjectsQuery, MetadataObjectsQueryVariables>(
FIND_MANY_METADATA_OBJECTS,
{
client: apolloMetadataClient ?? undefined,
skip: !apolloMetadataClient,
onError: (error) => {
// eslint-disable-next-line no-console
console.error('useFindManyMetadataObjects error : ', error);
enqueueSnackBar(
`Error during useFindManyMetadataObjects, ${error.message}`,
{
variant: 'error',
},
);
},
},
);
@ -47,5 +61,6 @@ export const useFindManyMetadataObjects = () => {
hasMore,
fetchMore,
loading,
error,
};
};

View File

@ -1,6 +1,8 @@
import { useMemo } from 'react';
import { useQuery } from '@apollo/client';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { PaginatedObjectType } from '../types/PaginatedObjectType';
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
@ -19,10 +21,25 @@ export const useFindManyObjects = <
objectNamePlural,
});
const { enqueueSnackBar } = useSnackBar();
const { data, loading, error } = useQuery<PaginatedObjectType<ObjectType>>(
findManyQuery,
{
skip: !foundMetadataObject,
onError: (error) => {
// eslint-disable-next-line no-console
console.error(
`useFindManyObjects for "${objectNamePlural}" error : `,
error,
);
enqueueSnackBar(
`Error during useFindManyObjects for "${objectNamePlural}", ${error.message}`,
{
variant: 'error',
},
);
},
},
);

View File

@ -1,4 +1,4 @@
import { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { MetadataFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { Field } from '~/generated/graphql';
import { formatMetadataFieldInput } from '../utils/formatMetadataFieldInput';
@ -7,15 +7,15 @@ import { useCreateOneMetadataField } from './useCreateOneMetadataField';
import { useDeleteOneMetadataField } from './useDeleteOneMetadataField';
import { useUpdateOneMetadataField } from './useUpdateOneMetadataField';
export const useFieldMetadata = () => {
export const useMetadataField = () => {
const { createOneMetadataField } = useCreateOneMetadataField();
const { updateOneMetadataField } = useUpdateOneMetadataField();
const { deleteOneMetadataField } = useDeleteOneMetadataField();
const createField = (
const createMetadataField = (
input: Pick<Field, 'label' | 'icon' | 'description'> & {
objectId: string;
type: ObjectFieldDataType;
type: MetadataFieldDataType;
},
) =>
createOneMetadataField({
@ -23,25 +23,25 @@ export const useFieldMetadata = () => {
objectId: input.objectId,
});
const activateField = (metadataField: Field) =>
const activateMetadataField = (metadataField: Field) =>
updateOneMetadataField({
fieldIdToUpdate: metadataField.id,
updatePayload: { isActive: true },
});
const disableField = (metadataField: Field) =>
const disableMetadataField = (metadataField: Field) =>
updateOneMetadataField({
fieldIdToUpdate: metadataField.id,
updatePayload: { isActive: false },
});
const eraseField = (metadataField: Field) =>
const eraseMetadataField = (metadataField: Field) =>
deleteOneMetadataField(metadataField.id);
return {
activateField,
createField,
disableField,
eraseField,
activateMetadataField,
createMetadataField,
disableMetadataField,
eraseMetadataField,
};
};

View File

@ -7,7 +7,7 @@ import { useDeleteOneMetadataObject } from './useDeleteOneMetadataObject';
import { useFindManyMetadataObjects } from './useFindManyMetadataObjects';
import { useUpdateOneMetadataObject } from './useUpdateOneMetadataObject';
export const useObjectMetadata = () => {
export const useMetadataObjectForSettings = () => {
const { metadataObjects, loading } = useFindManyMetadataObjects();
const activeMetadataObjects = metadataObjects.filter(
@ -17,23 +17,23 @@ export const useObjectMetadata = () => {
({ isActive }) => !isActive,
);
const findActiveObjectBySlug = (slug: string) =>
const findActiveMetadataObjectBySlug = (slug: string) =>
activeMetadataObjects.find(
(activeObject) => getObjectSlug(activeObject) === slug,
(activeMetadataObject) => getObjectSlug(activeMetadataObject) === slug,
);
const { createOneMetadataObject } = useCreateOneMetadataObject();
const { updateOneMetadataObject } = useUpdateOneMetadataObject();
const { deleteOneMetadataObject } = useDeleteOneMetadataObject();
const createObject = (
const createMetadataObject = (
input: Pick<
MetadataObject,
'labelPlural' | 'labelSingular' | 'icon' | 'description'
>,
) => createOneMetadataObject(formatMetadataObjectInput(input));
const editObject = (
const editMetadataObject = (
input: Pick<
MetadataObject,
'id' | 'labelPlural' | 'labelSingular' | 'icon' | 'description'
@ -44,30 +44,30 @@ export const useObjectMetadata = () => {
updatePayload: formatMetadataObjectInput(input),
});
const activateObject = (metadataObject: MetadataObject) =>
const activateMetadataObject = (metadataObject: Pick<MetadataObject, 'id'>) =>
updateOneMetadataObject({
idToUpdate: metadataObject.id,
updatePayload: { isActive: true },
});
const disableObject = (metadataObject: MetadataObject) =>
const disableMetadataObject = (metadataObject: Pick<MetadataObject, 'id'>) =>
updateOneMetadataObject({
idToUpdate: metadataObject.id,
updatePayload: { isActive: false },
});
const eraseObject = (metadataObject: Pick<MetadataObject, 'id'>) =>
const eraseMetadataObject = (metadataObject: Pick<MetadataObject, 'id'>) =>
deleteOneMetadataObject(metadataObject.id);
return {
activateObject,
activeObjects: activeMetadataObjects,
createObject,
disabledObjects: disabledMetadataObjects,
disableObject,
editObject,
eraseObject,
findActiveObjectBySlug,
activateMetadataObject,
activeMetadataObjects,
createMetadataObject,
disabledMetadataObjects,
disableMetadataObject,
editMetadataObject,
eraseMetadataObject,
findActiveMetadataObjectBySlug,
loading,
};
};

View File

@ -1,11 +1,11 @@
import toCamelCase from 'lodash.camelcase';
import { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { MetadataFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { Field } from '~/generated-metadata/graphql';
export const formatMetadataFieldInput = (
input: Pick<Field, 'label' | 'icon' | 'description'> & {
type: ObjectFieldDataType;
type: MetadataFieldDataType;
},
) => ({
description: input.description?.trim() ?? null,

View File

@ -3,12 +3,12 @@ import { Select } from '@/ui/input/components/Select';
import { Section } from '@/ui/layout/section/components/Section';
import { dataTypes } from '../constants/dataTypes';
import { ObjectFieldDataType } from '../types/ObjectFieldDataType';
import { MetadataFieldDataType } from '../types/ObjectFieldDataType';
type SettingsObjectFieldTypeSelectSectionProps = {
disabled?: boolean;
onChange?: (value: ObjectFieldDataType) => void;
type: ObjectFieldDataType;
onChange?: (value: MetadataFieldDataType) => void;
type: MetadataFieldDataType;
};
// TODO: remove "relation" type for now, add it back when the backend is ready.
@ -31,7 +31,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
onChange={onChange}
options={Object.entries(dataTypesWithoutRelation).map(
([key, dataType]) => ({
value: key as ObjectFieldDataType,
value: key as MetadataFieldDataType,
...dataType,
}),
)}

View File

@ -7,10 +7,10 @@ import {
} from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { ObjectFieldDataType } from '../types/ObjectFieldDataType';
import { MetadataFieldDataType } from '../types/ObjectFieldDataType';
export const dataTypes: Record<
ObjectFieldDataType,
MetadataFieldDataType,
{ label: string; Icon: IconComponent }
> = {
number: { label: 'Number', Icon: IconNumbers },

View File

@ -0,0 +1,65 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { MetadataObject } from '@/metadata/types/MetadataObject';
import { Checkbox } from '@/ui/input/components/Checkbox';
import { useLazyLoadIcon } from '@/ui/input/hooks/useLazyLoadIcon';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
type SettingsAvailableStandardObjectItemTableRowProps = {
isSelected?: boolean;
objectItem: MetadataObject;
onClick?: () => void;
};
export const StyledAvailableStandardObjectTableRow = styled(TableRow)`
grid-template-columns: 28px 148px 256px 80px;
`;
const StyledCheckboxTableCell = styled(TableCell)`
justify-content: center;
padding: 0;
padding-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledNameTableCell = styled(TableCell)`
color: ${({ theme }) => theme.font.color.primary};
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledDescription = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const SettingsAvailableStandardObjectItemTableRow = ({
isSelected,
objectItem,
onClick,
}: SettingsAvailableStandardObjectItemTableRowProps) => {
const theme = useTheme();
const { Icon } = useLazyLoadIcon(objectItem.icon ?? '');
return (
<StyledAvailableStandardObjectTableRow
key={objectItem.namePlural}
isSelected={isSelected}
onClick={onClick}
>
<StyledCheckboxTableCell>
<Checkbox checked={!!isSelected} />
</StyledCheckboxTableCell>
<StyledNameTableCell>
{!!Icon && <Icon size={theme.icon.size.md} />}
{objectItem.labelPlural}
</StyledNameTableCell>
<TableCell>
<StyledDescription>{objectItem.description}</StyledDescription>
</TableCell>
<TableCell align="right">{objectItem.fields.length}</TableCell>
</StyledAvailableStandardObjectTableRow>
);
};

View File

@ -0,0 +1,53 @@
import { MetadataObject } from '@/metadata/types/MetadataObject';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Section } from '@/ui/layout/section/components/Section';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import {
SettingsAvailableStandardObjectItemTableRow,
StyledAvailableStandardObjectTableRow,
} from './SettingsAvailableStandardObjectItemTableRow';
type SettingsAvailableStandardObjectsSectionProps = {
objectItems: MetadataObject[];
onChange: (selectedIds: Record<string, boolean>) => void;
selectedIds: Record<string, boolean>;
};
export const SettingsAvailableStandardObjectsSection = ({
objectItems,
onChange,
selectedIds,
}: SettingsAvailableStandardObjectsSectionProps) => (
<Section>
<H2Title
title="Available"
description="Select one or several standard objects to activate below"
/>
<Table>
<StyledAvailableStandardObjectTableRow>
<TableHeader></TableHeader>
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader align="right">Fields</TableHeader>
</StyledAvailableStandardObjectTableRow>
<TableBody>
{objectItems.map((objectItem) => (
<SettingsAvailableStandardObjectItemTableRow
key={objectItem.id}
isSelected={selectedIds[objectItem.id]}
objectItem={objectItem}
onClick={() =>
onChange({
...selectedIds,
[objectItem.id]: !selectedIds[objectItem.id],
})
}
/>
))}
</TableBody>
</Table>
</Section>
);

View File

@ -4,7 +4,6 @@ import styled from '@emotion/styled';
import { IconBox, IconDatabase, IconFileCheck } from '@/ui/display/icon';
import { SettingsObjectTypeCard } from './SettingsObjectTypeCard';
import { SettingsStandardObjects } from './SettingsStandardObjects';
export type NewObjectType = 'Standard' | 'Custom' | 'Remote';
@ -17,8 +16,6 @@ const StyledContainer = styled.div`
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
margin-bottom: ${({ theme }) => theme.spacing(8)};
width: 100%;
`;
export const SettingsNewObjectType = ({
@ -27,50 +24,47 @@ export const SettingsNewObjectType = ({
}: SettingsNewObjectTypeProps) => {
const theme = useTheme();
return (
<>
<StyledContainer>
<SettingsObjectTypeCard
title={'Standard'}
color="blue"
selected={selectedType === 'Standard'}
prefixIcon={
<IconFileCheck
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
onClick={() => onTypeSelect?.('Standard')}
/>
<SettingsObjectTypeCard
title="Custom"
color="orange"
selected={selectedType === 'Custom'}
prefixIcon={
<IconBox
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
onClick={() => onTypeSelect?.('Custom')}
/>
<SettingsObjectTypeCard
title="Remote"
soon
disabled
color="green"
selected={selectedType === 'Remote'}
prefixIcon={
<IconDatabase
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
/>
</StyledContainer>
{selectedType === 'Standard' && <SettingsStandardObjects />}
</>
<StyledContainer>
<SettingsObjectTypeCard
title={'Standard'}
color="blue"
selected={selectedType === 'Standard'}
prefixIcon={
<IconFileCheck
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
onClick={() => onTypeSelect?.('Standard')}
/>
<SettingsObjectTypeCard
title="Custom"
color="orange"
selected={selectedType === 'Custom'}
prefixIcon={
<IconBox
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
onClick={() => onTypeSelect?.('Custom')}
/>
<SettingsObjectTypeCard
title="Remote"
soon
disabled
color="green"
selected={selectedType === 'Remote'}
prefixIcon={
<IconDatabase
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
color={theme.font.color.tertiary}
/>
}
/>
</StyledContainer>
);
};

View File

@ -1,107 +0,0 @@
import { useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { H2Title } from '@/ui/display/typography/components/H2Title';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { standardObjects } from '../../constants/mockObjects';
const StyledTableRow = styled(TableRow)<{
selectedRows?: number[];
rowNumber?: number;
}>`
align-items: center;
background: ${({ selectedRows, rowNumber, theme }) =>
selectedRows?.includes(rowNumber!)
? theme.accent.quaternary
: theme.background.primary};
grid-template-columns: 36px 132px 240px 98.7px;
`;
const StyledCheckboxCell = styled(TableCell)`
padding: 0 ${({ theme }) => theme.spacing(2)} 0
${({ theme }) => theme.spacing(1)};
`;
const StyledNameTableCell = styled(TableCell)`
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledDescriptionCell = styled.div<{
align?: 'left' | 'center' | 'right';
}>`
color: ${({ theme }) => theme.font.color.secondary};
justify-content: ${({ align }) =>
align === 'right'
? 'flex-end'
: align === 'center'
? 'center'
: 'flex-start'};
overflow: hidden;
padding: 0 ${({ theme }) => theme.spacing(2)};
padding: 0 ${({ theme }) => theme.spacing(2)};
text-align: ${({ align }) => align ?? 'left'};
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledTable = styled(Table)`
display: grid;
gap: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsStandardObjects = () => {
const theme = useTheme();
const [selectedRows, setSelectedRows] = useState<number[]>([]);
return (
<>
<H2Title
title="Available"
description="Select one or several standard objects to activate below"
/>
<StyledTable>
<StyledTableRow>
<TableHeader></TableHeader>
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader align="right">Fields</TableHeader>
</StyledTableRow>
{standardObjects.map((object, rowNumber) => (
<StyledTableRow
selectedRows={selectedRows}
rowNumber={rowNumber}
onClick={() => {
const indexOfRowClicked = selectedRows.indexOf(rowNumber);
if (indexOfRowClicked === -1) {
setSelectedRows([...selectedRows, rowNumber]);
} else {
const newSelectedRows = [...selectedRows];
newSelectedRows.splice(indexOfRowClicked, 1);
setSelectedRows(newSelectedRows);
}
}}
key={object.name}
>
<StyledCheckboxCell>
<input
type="checkbox"
checked={selectedRows.includes(rowNumber)}
/>
</StyledCheckboxCell>
<StyledNameTableCell>
<object.Icon size={theme.icon.size.md} />
{object.name}
</StyledNameTableCell>
<StyledDescriptionCell>{object.description}</StyledDescriptionCell>
<TableCell align="right">{object.fields}</TableCell>
</StyledTableRow>
))}
</StyledTable>
</>
);
};

View File

@ -2,9 +2,9 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { dataTypes } from '../../constants/dataTypes';
import { ObjectFieldDataType } from '../../types/ObjectFieldDataType';
import { MetadataFieldDataType } from '../../types/ObjectFieldDataType';
const StyledDataType = styled.div<{ value: ObjectFieldDataType }>`
const StyledDataType = styled.div<{ value: MetadataFieldDataType }>`
align-items: center;
border: 1px solid transparent;
border-radius: ${({ theme }) => theme.border.radius.sm};
@ -24,7 +24,7 @@ const StyledDataType = styled.div<{ value: ObjectFieldDataType }>`
`;
type SettingsObjectFieldDataTypeProps = {
value: ObjectFieldDataType;
value: MetadataFieldDataType;
};
export const SettingsObjectFieldDataType = ({

View File

@ -7,7 +7,7 @@ import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Field } from '~/generated-metadata/graphql';
import { ObjectFieldDataType } from '../../types/ObjectFieldDataType';
import { MetadataFieldDataType } from '../../types/ObjectFieldDataType';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
@ -58,7 +58,7 @@ export const SettingsObjectFieldItemTableRow = ({
<TableCell>{fieldItem.isCustom ? 'Custom' : 'Standard'}</TableCell>
<TableCell>
<SettingsObjectFieldDataType
value={fieldItem.type as ObjectFieldDataType}
value={fieldItem.type as MetadataFieldDataType}
/>
</TableCell>
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell>

View File

@ -1,4 +1,4 @@
export type ObjectFieldDataType =
export type MetadataFieldDataType =
| 'boolean'
| 'number'
| 'relation'

View File

@ -0,0 +1,10 @@
import styled from '@emotion/styled';
const StyledTableBody = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
padding: ${({ theme }) => theme.spacing(2)} 0;
`;
export { StyledTableBody as TableBody };

View File

@ -1,6 +1,11 @@
import styled from '@emotion/styled';
const StyledTableRow = styled.div<{ onClick?: () => void }>`
const StyledTableRow = styled.div<{
isSelected?: boolean;
onClick?: () => void;
}>`
background-color: ${({ isSelected, theme }) =>
isSelected ? theme.accent.quaternary : 'transparent'};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: grid;
grid-auto-columns: 1fr;

View File

@ -4,6 +4,8 @@ import styled from '@emotion/styled';
import { IconChevronDown, IconChevronUp } from '@/ui/display/icon';
import { TableBody } from './TableBody';
type TableSectionProps = {
children: ReactNode;
isInitiallyExpanded?: boolean;
@ -34,9 +36,8 @@ const StyledSection = styled.div<{ isExpanded: boolean }>`
opacity ${({ theme }) => theme.animation.duration.normal}s;
`;
const StyledSectionContent = styled.div`
const StyledSectionContent = styled(TableBody)`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
padding: ${({ theme }) => theme.spacing(2)} 0;
`;
export const TableSection = ({

View File

@ -1,11 +1,12 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
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,
@ -22,7 +23,15 @@ export const SettingsNewObject = () => {
const [selectedObjectType, setSelectedObjectType] =
useState<NewObjectType>('Standard');
const { createObject } = useObjectMetadata();
const {
activateMetadataObject: activateObject,
createMetadataObject: createObject,
disabledMetadataObjects: disabledObjects,
} = useMetadataObjectForSettings();
const [selectedStandardObjectIds, setSelectedStandardObjectIds] = useState<
Record<string, boolean>
>({});
const [customFormValues, setCustomFormValues] = useState<{
description?: string;
@ -32,11 +41,24 @@ export const SettingsNewObject = () => {
}>({ icon: 'IconPigMoney', labelPlural: '', labelSingular: '' });
const canSave =
selectedObjectType === 'Custom' &&
!!customFormValues.labelPlural &&
!!customFormValues.labelSingular;
(selectedObjectType === 'Standard' &&
Object.values(selectedStandardObjectIds).some(
(isSelected) => isSelected,
)) ||
(selectedObjectType === 'Custom' &&
!!customFormValues.labelPlural &&
!!customFormValues.labelSingular);
const handleSave = async () => {
if (selectedObjectType === 'Standard') {
await Promise.all(
Object.entries(selectedStandardObjectIds).map(
([standardObjectId, isSelected]) =>
isSelected ? activateObject({ id: standardObjectId }) : undefined,
),
);
}
if (selectedObjectType === 'Custom') {
await createObject({
labelPlural: customFormValues.labelPlural,
@ -69,7 +91,7 @@ export const SettingsNewObject = () => {
</SettingsHeaderContainer>
<Section>
<H2Title
title="Object Type"
title="Object type"
description="The type of object you want to add"
/>
<SettingsNewObjectType
@ -77,6 +99,18 @@ export const SettingsNewObject = () => {
onTypeSelect={setSelectedObjectType}
/>
</Section>
{selectedObjectType === 'Standard' && (
<SettingsAvailableStandardObjectsSection
objectItems={disabledObjects.filter(({ isCustom }) => !isCustom)}
onChange={(selectedIds) =>
setSelectedStandardObjectIds((previousSelectedIds) => ({
...previousSelectedIds,
...selectedIds,
}))
}
selectedIds={selectedStandardObjectIds}
/>
)}
{selectedObjectType === 'Custom' && (
<>
<SettingsObjectIconSection

View File

@ -2,8 +2,8 @@ import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { useFieldMetadata } from '@/metadata/hooks/useFieldMetadata';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataField } from '@/metadata/hooks/useMetadataField';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
import { getFieldSlug } from '@/metadata/utils/getFieldSlug';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsAboutSection } from '@/settings/data-model/object-details/components/SettingsObjectAboutSection';
@ -34,28 +34,30 @@ export const SettingsObjectDetail = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { disableObject, findActiveObjectBySlug, loading } =
useObjectMetadata();
const activeObject = findActiveObjectBySlug(objectSlug);
const { disableMetadataObject, findActiveMetadataObjectBySlug, loading } =
useMetadataObjectForSettings();
const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
useEffect(() => {
if (loading) return;
if (!activeObject) navigate(AppPath.NotFound);
}, [activeObject, loading, navigate]);
if (!activeMetadataObject) navigate(AppPath.NotFound);
}, [activeMetadataObject, loading, navigate]);
const { activateField, disableField, eraseField } = useFieldMetadata();
const { activateMetadataField, disableMetadataField, eraseMetadataField } =
useMetadataField();
if (!activeObject) return null;
if (!activeMetadataObject) return null;
const activeFields = activeObject.fields.filter(
(fieldItem) => fieldItem.isActive,
const activeMetadataFields = activeMetadataObject.fields.filter(
(metadataField) => metadataField.isActive,
);
const disabledFields = activeObject.fields.filter(
(fieldItem) => !fieldItem.isActive,
const disabledMetadataFields = activeMetadataObject.fields.filter(
(metadataField) => !metadataField.isActive,
);
const handleDisable = async () => {
await disableObject(activeObject);
await disableMetadataObject(activeMetadataObject);
navigate('/settings/objects');
};
@ -65,20 +67,20 @@ export const SettingsObjectDetail = () => {
<Breadcrumb
links={[
{ children: 'Objects', href: '/settings/objects' },
{ children: activeObject.labelPlural },
{ children: activeMetadataObject.labelPlural },
]}
/>
<SettingsAboutSection
iconKey={activeObject.icon ?? undefined}
name={activeObject.labelPlural || ''}
isCustom={activeObject.isCustom}
iconKey={activeMetadataObject.icon ?? undefined}
name={activeMetadataObject.labelPlural || ''}
isCustom={activeMetadataObject.isCustom}
onDisable={handleDisable}
onEdit={() => navigate('./edit')}
/>
<Section>
<H2Title
title="Fields"
description={`Customise the fields available in the ${activeObject.labelSingular} views and their display order in the ${activeObject.labelSingular} detail view and menus.`}
description={`Customise the fields available in the ${activeMetadataObject.labelSingular} views and their display order in the ${activeMetadataObject.labelSingular} detail view and menus.`}
/>
<Table>
<StyledObjectFieldTableRow>
@ -87,36 +89,44 @@ export const SettingsObjectDetail = () => {
<TableHeader>Data type</TableHeader>
<TableHeader></TableHeader>
</StyledObjectFieldTableRow>
{!!activeFields.length && (
{!!activeMetadataFields.length && (
<TableSection title="Active">
{activeFields.map((fieldItem) => (
{activeMetadataFields.map((activeMetadataField) => (
<SettingsObjectFieldItemTableRow
key={fieldItem.id}
fieldItem={fieldItem}
key={activeMetadataField.id}
fieldItem={activeMetadataField}
ActionIcon={
<SettingsObjectFieldActiveActionDropdown
isCustomField={fieldItem.isCustom}
scopeKey={fieldItem.id}
onEdit={() => navigate(`./${getFieldSlug(fieldItem)}`)}
onDisable={() => disableField(fieldItem)}
isCustomField={activeMetadataField.isCustom}
scopeKey={activeMetadataField.id}
onEdit={() =>
navigate(`./${getFieldSlug(activeMetadataField)}`)
}
onDisable={() =>
disableMetadataField(activeMetadataField)
}
/>
}
/>
))}
</TableSection>
)}
{!!disabledFields.length && (
{!!disabledMetadataFields.length && (
<TableSection isInitiallyExpanded={false} title="Disabled">
{disabledFields.map((fieldItem) => (
{disabledMetadataFields.map((disabledMetadataField) => (
<SettingsObjectFieldItemTableRow
key={fieldItem.id}
fieldItem={fieldItem}
key={disabledMetadataField.id}
fieldItem={disabledMetadataField}
ActionIcon={
<SettingsObjectFieldDisabledActionDropdown
isCustomField={fieldItem.isCustom}
scopeKey={fieldItem.id}
onActivate={() => activateField(fieldItem)}
onErase={() => eraseField(fieldItem)}
isCustomField={disabledMetadataField.isCustom}
scopeKey={disabledMetadataField.id}
onActivate={() =>
activateMetadataField(disabledMetadataField)
}
onErase={() =>
eraseMetadataField(disabledMetadataField)
}
/>
}
/>
@ -132,7 +142,7 @@ export const SettingsObjectDetail = () => {
variant="secondary"
onClick={() =>
navigate(
disabledFields.length
disabledMetadataFields.length
? './new-field/step-1'
: './new-field/step-2',
)

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
import { getObjectSlug } from '@/metadata/utils/getObjectSlug';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
@ -20,9 +20,14 @@ export const SettingsObjectEdit = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { disableObject, editObject, findActiveObjectBySlug, loading } =
useObjectMetadata();
const activeObject = findActiveObjectBySlug(objectSlug);
const {
disableMetadataObject,
editMetadataObject,
findActiveMetadataObjectBySlug,
loading,
} = useMetadataObjectForSettings();
const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
const [formValues, setFormValues] = useState<
Partial<{
@ -36,44 +41,44 @@ export const SettingsObjectEdit = () => {
useEffect(() => {
if (loading) return;
if (!activeObject) {
if (!activeMetadataObject) {
navigate(AppPath.NotFound);
return;
}
if (!Object.keys(formValues).length) {
setFormValues({
icon: activeObject.icon ?? undefined,
labelSingular: activeObject.labelSingular,
labelPlural: activeObject.labelPlural,
description: activeObject.description ?? undefined,
icon: activeMetadataObject.icon ?? undefined,
labelSingular: activeMetadataObject.labelSingular,
labelPlural: activeMetadataObject.labelPlural,
description: activeMetadataObject.description ?? undefined,
});
}
}, [activeObject, formValues, loading, navigate]);
}, [activeMetadataObject, formValues, loading, navigate]);
if (!activeObject) return null;
if (!activeMetadataObject) return null;
const areRequiredFieldsFilled =
!!formValues.labelSingular && !!formValues.labelPlural;
const hasChanges =
formValues.description !== activeObject.description ||
formValues.icon !== activeObject.icon ||
formValues.labelPlural !== activeObject.labelPlural ||
formValues.labelSingular !== activeObject.labelSingular;
formValues.description !== activeMetadataObject.description ||
formValues.icon !== activeMetadataObject.icon ||
formValues.labelPlural !== activeMetadataObject.labelPlural ||
formValues.labelSingular !== activeMetadataObject.labelSingular;
const canSave = areRequiredFieldsFilled && hasChanges;
const handleSave = async () => {
const editedObject = { ...activeObject, ...formValues };
const editedMetadataObject = { ...activeMetadataObject, ...formValues };
await editObject(editedObject);
await editMetadataObject(editedMetadataObject);
navigate(`/settings/objects/${getObjectSlug(editedObject)}`);
navigate(`/settings/objects/${getObjectSlug(editedMetadataObject)}`);
};
const handleDisable = async () => {
await disableObject(activeObject);
await disableMetadataObject(activeMetadataObject);
navigate('/settings/objects');
};
@ -85,13 +90,13 @@ export const SettingsObjectEdit = () => {
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObject.labelPlural,
children: activeMetadataObject.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'Edit' },
]}
/>
{!!activeObject.isCustom && (
{!!activeMetadataObject.isCustom && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
@ -100,7 +105,7 @@ export const SettingsObjectEdit = () => {
)}
</SettingsHeaderContainer>
<SettingsObjectIconSection
disabled={!activeObject.isCustom}
disabled={!activeMetadataObject.isCustom}
iconKey={formValues.icon}
label={formValues.labelPlural}
onChange={({ iconKey }) =>
@ -111,7 +116,7 @@ export const SettingsObjectEdit = () => {
}
/>
<SettingsObjectFormSection
disabled={!activeObject.isCustom}
disabled={!activeMetadataObject.isCustom}
singularName={formValues.labelSingular}
pluralName={formValues.labelPlural}
description={formValues.description}

View File

@ -1,14 +1,14 @@
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useFieldMetadata } from '@/metadata/hooks/useFieldMetadata';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataField } from '@/metadata/hooks/useMetadataField';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
import { getFieldSlug } from '@/metadata/utils/getFieldSlug';
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 { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { MetadataFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { AppPath } from '@/types/AppPath';
import { IconArchive, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -21,23 +21,27 @@ export const SettingsObjectFieldEdit = () => {
const navigate = useNavigate();
const { objectSlug = '', fieldSlug = '' } = useParams();
const { findActiveObjectBySlug, loading } = useObjectMetadata();
const activeObject = findActiveObjectBySlug(objectSlug);
const { findActiveMetadataObjectBySlug, loading } =
useMetadataObjectForSettings();
const { disableField } = useFieldMetadata();
const activeField = activeObject?.fields.find(
(field) => field.isActive && getFieldSlug(field) === fieldSlug,
const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
const { disableMetadataField: disableField } = useMetadataField();
const activeMetadataField = activeMetadataObject?.fields.find(
(metadataField) =>
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
);
useEffect(() => {
if (loading) return;
if (!activeObject || !activeField) navigate(AppPath.NotFound);
}, [activeField, activeObject, loading, navigate]);
if (!activeMetadataObject || !activeMetadataField)
navigate(AppPath.NotFound);
}, [activeMetadataField, activeMetadataObject, loading, navigate]);
if (!activeObject || !activeField) return null;
if (!activeMetadataObject || !activeMetadataField) return null;
const handleDisable = async () => {
await disableField(activeField);
await disableField(activeMetadataField);
navigate(`/settings/objects/${objectSlug}`);
};
@ -49,23 +53,23 @@ export const SettingsObjectFieldEdit = () => {
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObject.labelPlural,
children: activeMetadataObject.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: activeField.label },
{ children: activeMetadataField.label },
]}
/>
</SettingsHeaderContainer>
<SettingsObjectFieldFormSection
disabled={!activeField.isCustom}
name={activeField.label}
description={activeField.description ?? undefined}
iconKey={activeField.icon ?? undefined}
disabled={!activeMetadataField.isCustom}
name={activeMetadataField.label}
description={activeMetadataField.description ?? undefined}
iconKey={activeMetadataField.icon ?? undefined}
onChange={() => undefined}
/>
<SettingsObjectFieldTypeSelectSection
disabled
type={activeField.type as ObjectFieldDataType}
type={activeMetadataField.type as MetadataFieldDataType}
/>
<Section>
<H2Title title="Danger zone" description="Disable this field" />

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -36,21 +36,23 @@ export const SettingsObjectNewFieldStep1 = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { findActiveObjectBySlug, loading } = useObjectMetadata();
const activeObject = findActiveObjectBySlug(objectSlug);
const { findActiveMetadataObjectBySlug, loading } =
useMetadataObjectForSettings();
const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
useEffect(() => {
if (loading) return;
if (!activeObject) navigate(AppPath.NotFound);
}, [activeObject, loading, navigate]);
if (!activeMetadataObject) navigate(AppPath.NotFound);
}, [activeMetadataObject, loading, navigate]);
if (!activeObject) return null;
if (!activeMetadataObject) return null;
const activeFields = activeObject.fields.filter(
(fieldItem) => fieldItem.isActive,
const activeMetadataFields = activeMetadataObject.fields.filter(
(metadataField) => metadataField.isActive,
);
const disabledFields = activeObject.fields.filter(
(fieldItem) => !fieldItem.isActive,
const disabledMetadataFields = activeMetadataObject.fields.filter(
(metadataField) => !metadataField.isActive,
);
return (
@ -61,7 +63,7 @@ export const SettingsObjectNewFieldStep1 = () => {
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObject.labelPlural,
children: activeMetadataObject.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'New Field' },
@ -85,12 +87,12 @@ export const SettingsObjectNewFieldStep1 = () => {
<TableHeader>Data type</TableHeader>
<TableHeader></TableHeader>
</StyledObjectFieldTableRow>
{!!activeFields.length && (
{!!activeMetadataFields.length && (
<TableSection isInitiallyExpanded={false} title="Active">
{activeFields.map((fieldItem) => (
{activeMetadataFields.map((activeMetadataField) => (
<SettingsObjectFieldItemTableRow
key={fieldItem.id}
fieldItem={fieldItem}
key={activeMetadataField.id}
fieldItem={activeMetadataField}
ActionIcon={
<LightIconButton Icon={IconMinus} accent="tertiary" />
}
@ -98,12 +100,12 @@ export const SettingsObjectNewFieldStep1 = () => {
))}
</TableSection>
)}
{!!disabledFields.length && (
{!!disabledMetadataFields.length && (
<TableSection title="Disabled">
{disabledFields.map((fieldItem) => (
{disabledMetadataFields.map((disabledMetadataField) => (
<SettingsObjectFieldItemTableRow
key={fieldItem.name}
fieldItem={fieldItem}
key={disabledMetadataField.name}
fieldItem={disabledMetadataField}
ActionIcon={
<LightIconButton Icon={IconPlus} accent="tertiary" />
}

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useFieldMetadata } from '@/metadata/hooks/useFieldMetadata';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataField } from '@/metadata/hooks/useMetadataField';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
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 { ObjectFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { MetadataFieldDataType } from '@/settings/data-model/types/ObjectFieldDataType';
import { AppPath } from '@/types/AppPath';
import { IconSettings } from '@/ui/display/icon';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
@ -17,28 +17,34 @@ import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
export const SettingsObjectNewFieldStep2 = () => {
const navigate = useNavigate();
const { objectSlug = '' } = useParams();
const { findActiveObjectBySlug, loading } = useObjectMetadata();
const activeObject = findActiveObjectBySlug(objectSlug);
const { createField } = useFieldMetadata();
const { findActiveMetadataObjectBySlug, loading } =
useMetadataObjectForSettings();
const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
const { createMetadataField } = useMetadataField();
useEffect(() => {
if (loading) return;
if (!activeObject) navigate(AppPath.NotFound);
}, [activeObject, loading, navigate]);
if (!activeMetadataObject) navigate(AppPath.NotFound);
}, [activeMetadataObject, loading, navigate]);
const [formValues, setFormValues] = useState<{
description?: string;
icon: string;
label: string;
type: ObjectFieldDataType;
type: MetadataFieldDataType;
}>({ icon: 'IconUsers', label: '', type: 'number' });
if (!activeObject) return null;
if (!activeMetadataObject) return null;
const canSave = !!formValues.label;
const handleSave = async () => {
await createField({ ...formValues, objectId: activeObject.id });
await createMetadataField({
...formValues,
objectId: activeMetadataObject.id,
});
navigate(`/settings/objects/${objectSlug}`);
};
@ -50,7 +56,7 @@ export const SettingsObjectNewFieldStep2 = () => {
links={[
{ children: 'Objects', href: '/settings/objects' },
{
children: activeObject.labelPlural,
children: activeMetadataObject.labelPlural,
href: `/settings/objects/${objectSlug}`,
},
{ children: 'New Field' },

View File

@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata';
import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
import { getObjectSlug } from '@/metadata/utils/getObjectSlug';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -34,8 +34,12 @@ export const SettingsObjects = () => {
const theme = useTheme();
const navigate = useNavigate();
const { activateObject, activeObjects, disabledObjects, eraseObject } =
useObjectMetadata();
const {
activateMetadataObject,
activeMetadataObjects,
disabledMetadataObjects,
eraseMetadataObject,
} = useMetadataObjectForSettings();
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
@ -62,12 +66,12 @@ export const SettingsObjects = () => {
<TableHeader align="right">Instances</TableHeader>
<TableHeader></TableHeader>
</StyledObjectTableRow>
{!!activeObjects.length && (
{!!activeMetadataObjects.length && (
<TableSection title="Active">
{activeObjects.map((objectItem) => (
{activeMetadataObjects.map((activeMetadataObject) => (
<SettingsObjectItemTableRow
key={objectItem.namePlural}
objectItem={objectItem}
key={activeMetadataObject.namePlural}
objectItem={activeMetadataObject}
action={
<StyledIconChevronRight
size={theme.icon.size.md}
@ -76,25 +80,31 @@ export const SettingsObjects = () => {
}
onClick={() =>
navigate(
`/settings/objects/${getObjectSlug(objectItem)}`,
`/settings/objects/${getObjectSlug(
activeMetadataObject,
)}`,
)
}
/>
))}
</TableSection>
)}
{!!disabledObjects.length && (
{!!disabledMetadataObjects.length && (
<TableSection title="Disabled">
{disabledObjects.map((objectItem) => (
{disabledMetadataObjects.map((disabledMetadataObject) => (
<SettingsObjectItemTableRow
key={objectItem.namePlural}
objectItem={objectItem}
key={disabledMetadataObject.namePlural}
objectItem={disabledMetadataObject}
action={
<SettingsObjectDisabledMenuDropDown
isCustomObject={objectItem.isCustom}
scopeKey={objectItem.namePlural}
onActivate={() => activateObject(objectItem)}
onErase={() => eraseObject(objectItem)}
isCustomObject={disabledMetadataObject.isCustom}
scopeKey={disabledMetadataObject.namePlural}
onActivate={() =>
activateMetadataObject(disabledMetadataObject)
}
onErase={() =>
eraseMetadataObject(disabledMetadataObject)
}
/>
}
/>