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` export const FIND_MANY_METADATA_OBJECTS = gql`
query MetadataObjects { query MetadataObjects {
objects(paging: { first: 100 }) { objects(paging: { first: 1000 }) {
edges { edges {
node { node {
id id
@ -17,7 +17,7 @@ export const FIND_MANY_METADATA_OBJECTS = gql`
isActive isActive
createdAt createdAt
updatedAt updatedAt
fields(paging: { first: 100 }) { fields(paging: { first: 1000 }) {
edges { edges {
node { node {
id id

View File

@ -1,6 +1,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import { import {
MetadataObjectsQuery, MetadataObjectsQuery,
MetadataObjectsQueryVariables, MetadataObjectsQueryVariables,
@ -15,15 +16,28 @@ import { useApolloMetadataClient } from './useApolloMetadataClient';
export const useFindManyMetadataObjects = () => { export const useFindManyMetadataObjects = () => {
const apolloMetadataClient = useApolloMetadataClient(); const apolloMetadataClient = useApolloMetadataClient();
const { enqueueSnackBar } = useSnackBar();
const { const {
data, data,
fetchMore: fetchMoreInternal, fetchMore: fetchMoreInternal,
loading, loading,
error,
} = useQuery<MetadataObjectsQuery, MetadataObjectsQueryVariables>( } = useQuery<MetadataObjectsQuery, MetadataObjectsQueryVariables>(
FIND_MANY_METADATA_OBJECTS, FIND_MANY_METADATA_OBJECTS,
{ {
client: apolloMetadataClient ?? undefined, client: apolloMetadataClient ?? undefined,
skip: !apolloMetadataClient, 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, hasMore,
fetchMore, fetchMore,
loading, loading,
error,
}; };
}; };

View File

@ -1,6 +1,8 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier'; import { MetadataObjectIdentifier } from '../types/MetadataObjectIdentifier';
import { PaginatedObjectType } from '../types/PaginatedObjectType'; import { PaginatedObjectType } from '../types/PaginatedObjectType';
import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects'; import { formatPagedObjectsToObjects } from '../utils/formatPagedObjectsToObjects';
@ -19,10 +21,25 @@ export const useFindManyObjects = <
objectNamePlural, objectNamePlural,
}); });
const { enqueueSnackBar } = useSnackBar();
const { data, loading, error } = useQuery<PaginatedObjectType<ObjectType>>( const { data, loading, error } = useQuery<PaginatedObjectType<ObjectType>>(
findManyQuery, findManyQuery,
{ {
skip: !foundMetadataObject, 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 { Field } from '~/generated/graphql';
import { formatMetadataFieldInput } from '../utils/formatMetadataFieldInput'; import { formatMetadataFieldInput } from '../utils/formatMetadataFieldInput';
@ -7,15 +7,15 @@ import { useCreateOneMetadataField } from './useCreateOneMetadataField';
import { useDeleteOneMetadataField } from './useDeleteOneMetadataField'; import { useDeleteOneMetadataField } from './useDeleteOneMetadataField';
import { useUpdateOneMetadataField } from './useUpdateOneMetadataField'; import { useUpdateOneMetadataField } from './useUpdateOneMetadataField';
export const useFieldMetadata = () => { export const useMetadataField = () => {
const { createOneMetadataField } = useCreateOneMetadataField(); const { createOneMetadataField } = useCreateOneMetadataField();
const { updateOneMetadataField } = useUpdateOneMetadataField(); const { updateOneMetadataField } = useUpdateOneMetadataField();
const { deleteOneMetadataField } = useDeleteOneMetadataField(); const { deleteOneMetadataField } = useDeleteOneMetadataField();
const createField = ( const createMetadataField = (
input: Pick<Field, 'label' | 'icon' | 'description'> & { input: Pick<Field, 'label' | 'icon' | 'description'> & {
objectId: string; objectId: string;
type: ObjectFieldDataType; type: MetadataFieldDataType;
}, },
) => ) =>
createOneMetadataField({ createOneMetadataField({
@ -23,25 +23,25 @@ export const useFieldMetadata = () => {
objectId: input.objectId, objectId: input.objectId,
}); });
const activateField = (metadataField: Field) => const activateMetadataField = (metadataField: Field) =>
updateOneMetadataField({ updateOneMetadataField({
fieldIdToUpdate: metadataField.id, fieldIdToUpdate: metadataField.id,
updatePayload: { isActive: true }, updatePayload: { isActive: true },
}); });
const disableField = (metadataField: Field) => const disableMetadataField = (metadataField: Field) =>
updateOneMetadataField({ updateOneMetadataField({
fieldIdToUpdate: metadataField.id, fieldIdToUpdate: metadataField.id,
updatePayload: { isActive: false }, updatePayload: { isActive: false },
}); });
const eraseField = (metadataField: Field) => const eraseMetadataField = (metadataField: Field) =>
deleteOneMetadataField(metadataField.id); deleteOneMetadataField(metadataField.id);
return { return {
activateField, activateMetadataField,
createField, createMetadataField,
disableField, disableMetadataField,
eraseField, eraseMetadataField,
}; };
}; };

View File

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

View File

@ -1,11 +1,11 @@
import toCamelCase from 'lodash.camelcase'; 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'; import { Field } from '~/generated-metadata/graphql';
export const formatMetadataFieldInput = ( export const formatMetadataFieldInput = (
input: Pick<Field, 'label' | 'icon' | 'description'> & { input: Pick<Field, 'label' | 'icon' | 'description'> & {
type: ObjectFieldDataType; type: MetadataFieldDataType;
}, },
) => ({ ) => ({
description: input.description?.trim() ?? null, 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 { Section } from '@/ui/layout/section/components/Section';
import { dataTypes } from '../constants/dataTypes'; import { dataTypes } from '../constants/dataTypes';
import { ObjectFieldDataType } from '../types/ObjectFieldDataType'; import { MetadataFieldDataType } from '../types/ObjectFieldDataType';
type SettingsObjectFieldTypeSelectSectionProps = { type SettingsObjectFieldTypeSelectSectionProps = {
disabled?: boolean; disabled?: boolean;
onChange?: (value: ObjectFieldDataType) => void; onChange?: (value: MetadataFieldDataType) => void;
type: ObjectFieldDataType; type: MetadataFieldDataType;
}; };
// TODO: remove "relation" type for now, add it back when the backend is ready. // TODO: remove "relation" type for now, add it back when the backend is ready.
@ -31,7 +31,7 @@ export const SettingsObjectFieldTypeSelectSection = ({
onChange={onChange} onChange={onChange}
options={Object.entries(dataTypesWithoutRelation).map( options={Object.entries(dataTypesWithoutRelation).map(
([key, dataType]) => ({ ([key, dataType]) => ({
value: key as ObjectFieldDataType, value: key as MetadataFieldDataType,
...dataType, ...dataType,
}), }),
)} )}

View File

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

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 styled from '@emotion/styled';
import { dataTypes } from '../../constants/dataTypes'; 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; align-items: center;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
@ -24,7 +24,7 @@ const StyledDataType = styled.div<{ value: ObjectFieldDataType }>`
`; `;
type SettingsObjectFieldDataTypeProps = { type SettingsObjectFieldDataTypeProps = {
value: ObjectFieldDataType; value: MetadataFieldDataType;
}; };
export const SettingsObjectFieldDataType = ({ 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 { TableRow } from '@/ui/layout/table/components/TableRow';
import { Field } from '~/generated-metadata/graphql'; import { Field } from '~/generated-metadata/graphql';
import { ObjectFieldDataType } from '../../types/ObjectFieldDataType'; import { MetadataFieldDataType } from '../../types/ObjectFieldDataType';
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
@ -58,7 +58,7 @@ export const SettingsObjectFieldItemTableRow = ({
<TableCell>{fieldItem.isCustom ? 'Custom' : 'Standard'}</TableCell> <TableCell>{fieldItem.isCustom ? 'Custom' : 'Standard'}</TableCell>
<TableCell> <TableCell>
<SettingsObjectFieldDataType <SettingsObjectFieldDataType
value={fieldItem.type as ObjectFieldDataType} value={fieldItem.type as MetadataFieldDataType}
/> />
</TableCell> </TableCell>
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell> <StyledIconTableCell>{ActionIcon}</StyledIconTableCell>

View File

@ -1,4 +1,4 @@
export type ObjectFieldDataType = export type MetadataFieldDataType =
| 'boolean' | 'boolean'
| 'number' | 'number'
| 'relation' | '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'; 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}; border-radius: ${({ theme }) => theme.border.radius.sm};
display: grid; display: grid;
grid-auto-columns: 1fr; grid-auto-columns: 1fr;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useFieldMetadata } from '@/metadata/hooks/useFieldMetadata'; import { useMetadataField } from '@/metadata/hooks/useMetadataField';
import { useObjectMetadata } from '@/metadata/hooks/useObjectMetadata'; import { useMetadataObjectForSettings } from '@/metadata/hooks/useMetadataObjectForSettings';
import { getFieldSlug } from '@/metadata/utils/getFieldSlug'; import { getFieldSlug } from '@/metadata/utils/getFieldSlug';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection'; import { SettingsObjectFieldFormSection } from '@/settings/data-model/components/SettingsObjectFieldFormSection';
import { SettingsObjectFieldTypeSelectSection } from '@/settings/data-model/components/SettingsObjectFieldTypeSelectSection'; 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 { AppPath } from '@/types/AppPath';
import { IconArchive, IconSettings } from '@/ui/display/icon'; import { IconArchive, IconSettings } from '@/ui/display/icon';
import { H2Title } from '@/ui/display/typography/components/H2Title'; import { H2Title } from '@/ui/display/typography/components/H2Title';
@ -21,23 +21,27 @@ export const SettingsObjectFieldEdit = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '', fieldSlug = '' } = useParams(); const { objectSlug = '', fieldSlug = '' } = useParams();
const { findActiveObjectBySlug, loading } = useObjectMetadata(); const { findActiveMetadataObjectBySlug, loading } =
const activeObject = findActiveObjectBySlug(objectSlug); useMetadataObjectForSettings();
const { disableField } = useFieldMetadata(); const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
const activeField = activeObject?.fields.find(
(field) => field.isActive && getFieldSlug(field) === fieldSlug, const { disableMetadataField: disableField } = useMetadataField();
const activeMetadataField = activeMetadataObject?.fields.find(
(metadataField) =>
metadataField.isActive && getFieldSlug(metadataField) === fieldSlug,
); );
useEffect(() => { useEffect(() => {
if (loading) return; if (loading) return;
if (!activeObject || !activeField) navigate(AppPath.NotFound); if (!activeMetadataObject || !activeMetadataField)
}, [activeField, activeObject, loading, navigate]); navigate(AppPath.NotFound);
}, [activeMetadataField, activeMetadataObject, loading, navigate]);
if (!activeObject || !activeField) return null; if (!activeMetadataObject || !activeMetadataField) return null;
const handleDisable = async () => { const handleDisable = async () => {
await disableField(activeField); await disableField(activeMetadataField);
navigate(`/settings/objects/${objectSlug}`); navigate(`/settings/objects/${objectSlug}`);
}; };
@ -49,23 +53,23 @@ export const SettingsObjectFieldEdit = () => {
links={[ links={[
{ children: 'Objects', href: '/settings/objects' }, { children: 'Objects', href: '/settings/objects' },
{ {
children: activeObject.labelPlural, children: activeMetadataObject.labelPlural,
href: `/settings/objects/${objectSlug}`, href: `/settings/objects/${objectSlug}`,
}, },
{ children: activeField.label }, { children: activeMetadataField.label },
]} ]}
/> />
</SettingsHeaderContainer> </SettingsHeaderContainer>
<SettingsObjectFieldFormSection <SettingsObjectFieldFormSection
disabled={!activeField.isCustom} disabled={!activeMetadataField.isCustom}
name={activeField.label} name={activeMetadataField.label}
description={activeField.description ?? undefined} description={activeMetadataField.description ?? undefined}
iconKey={activeField.icon ?? undefined} iconKey={activeMetadataField.icon ?? undefined}
onChange={() => undefined} onChange={() => undefined}
/> />
<SettingsObjectFieldTypeSelectSection <SettingsObjectFieldTypeSelectSection
disabled disabled
type={activeField.type as ObjectFieldDataType} type={activeMetadataField.type as MetadataFieldDataType}
/> />
<Section> <Section>
<H2Title title="Danger zone" description="Disable this field" /> <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 { useNavigate, useParams } from 'react-router-dom';
import styled from '@emotion/styled'; 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 { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
@ -36,21 +36,23 @@ export const SettingsObjectNewFieldStep1 = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { objectSlug = '' } = useParams(); const { objectSlug = '' } = useParams();
const { findActiveObjectBySlug, loading } = useObjectMetadata(); const { findActiveMetadataObjectBySlug, loading } =
const activeObject = findActiveObjectBySlug(objectSlug); useMetadataObjectForSettings();
const activeMetadataObject = findActiveMetadataObjectBySlug(objectSlug);
useEffect(() => { useEffect(() => {
if (loading) return; if (loading) return;
if (!activeObject) navigate(AppPath.NotFound); if (!activeMetadataObject) navigate(AppPath.NotFound);
}, [activeObject, loading, navigate]); }, [activeMetadataObject, loading, navigate]);
if (!activeObject) return null; if (!activeMetadataObject) return null;
const activeFields = activeObject.fields.filter( const activeMetadataFields = activeMetadataObject.fields.filter(
(fieldItem) => fieldItem.isActive, (metadataField) => metadataField.isActive,
); );
const disabledFields = activeObject.fields.filter( const disabledMetadataFields = activeMetadataObject.fields.filter(
(fieldItem) => !fieldItem.isActive, (metadataField) => !metadataField.isActive,
); );
return ( return (
@ -61,7 +63,7 @@ export const SettingsObjectNewFieldStep1 = () => {
links={[ links={[
{ children: 'Objects', href: '/settings/objects' }, { children: 'Objects', href: '/settings/objects' },
{ {
children: activeObject.labelPlural, children: activeMetadataObject.labelPlural,
href: `/settings/objects/${objectSlug}`, href: `/settings/objects/${objectSlug}`,
}, },
{ children: 'New Field' }, { children: 'New Field' },
@ -85,12 +87,12 @@ export const SettingsObjectNewFieldStep1 = () => {
<TableHeader>Data type</TableHeader> <TableHeader>Data type</TableHeader>
<TableHeader></TableHeader> <TableHeader></TableHeader>
</StyledObjectFieldTableRow> </StyledObjectFieldTableRow>
{!!activeFields.length && ( {!!activeMetadataFields.length && (
<TableSection isInitiallyExpanded={false} title="Active"> <TableSection isInitiallyExpanded={false} title="Active">
{activeFields.map((fieldItem) => ( {activeMetadataFields.map((activeMetadataField) => (
<SettingsObjectFieldItemTableRow <SettingsObjectFieldItemTableRow
key={fieldItem.id} key={activeMetadataField.id}
fieldItem={fieldItem} fieldItem={activeMetadataField}
ActionIcon={ ActionIcon={
<LightIconButton Icon={IconMinus} accent="tertiary" /> <LightIconButton Icon={IconMinus} accent="tertiary" />
} }
@ -98,12 +100,12 @@ export const SettingsObjectNewFieldStep1 = () => {
))} ))}
</TableSection> </TableSection>
)} )}
{!!disabledFields.length && ( {!!disabledMetadataFields.length && (
<TableSection title="Disabled"> <TableSection title="Disabled">
{disabledFields.map((fieldItem) => ( {disabledMetadataFields.map((disabledMetadataField) => (
<SettingsObjectFieldItemTableRow <SettingsObjectFieldItemTableRow
key={fieldItem.name} key={disabledMetadataField.name}
fieldItem={fieldItem} fieldItem={disabledMetadataField}
ActionIcon={ ActionIcon={
<LightIconButton Icon={IconPlus} accent="tertiary" /> <LightIconButton Icon={IconPlus} accent="tertiary" />
} }

View File

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

View File

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

View File

@ -10,6 +10,7 @@ provision-postgres:
@docker rm twenty_postgres || true @docker rm twenty_postgres || true
@docker volume rm twenty_db_data || true @docker volume rm twenty_db_data || true
@docker compose up --build postgres -d @docker compose up --build postgres -d
@cd ../../server && yarn database:setup && yarn database:reset
up: up:
@docker compose up -d @docker compose up -d

View File

@ -15,18 +15,18 @@ export const seedMetadata = async (prisma: PrismaClient) => {
await prisma.$queryRawUnsafe(`CREATE TABLE IF NOT EXISTS await prisma.$queryRawUnsafe(`CREATE TABLE IF NOT EXISTS
workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company( workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
id TEXT PRIMARY KEY, "id" TEXT PRIMARY KEY,
name TEXT NOT NULL, "name" TEXT NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"deletedAt" TIMESTAMP WITH TIME ZONE, "deletedAt" TIMESTAMP WITH TIME ZONE,
domainName TEXT NOT NULL, "domainName" TEXT NOT NULL,
address TEXT NOT NULL, "address" TEXT NOT NULL,
employees INTEGER NOT NULL "employees" INTEGER NOT NULL
); );
`); `);
await prisma.$queryRawUnsafe(`INSERT INTO workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company( await prisma.$queryRawUnsafe(`INSERT INTO workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
id, name, domainName, address, employees "id", "name", "domainName", "address", "employees"
) )
VALUES ( VALUES (
'89bb825c-171e-4bcc-9cf7-43448d6fb278', 'Airbnb', 'airbnb.com', 'San Francisco', 5000 '89bb825c-171e-4bcc-9cf7-43448d6fb278', 'Airbnb', 'airbnb.com', 'San Francisco', 5000
@ -44,19 +44,19 @@ export const seedMetadata = async (prisma: PrismaClient) => {
id, name_singular, name_plural, label_singular, label_plural, description, icon, target_table_name, is_custom, is_active, workspace_id, data_source_id id, name_singular, name_plural, label_singular, label_plural, description, icon, target_table_name, is_custom, is_active, workspace_id, data_source_id
) )
VALUES ( VALUES (
'ba391617-ee08-432f-9438-2e17df5ac279', 'companyV2', 'companiesV2', 'Company', 'Companies', 'Companies', 'company', 'company', false, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1' 'ba391617-ee08-432f-9438-2e17df5ac279', 'companyV2', 'companiesV2', 'Company', 'Companies', 'Companies', 'IconBuilding', 'company', false, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', 'b37b2163-7f63-47a9-b1b3-6c7290ca9fb1'
) ON CONFLICT DO NOTHING`); ) ON CONFLICT DO NOTHING`);
await prisma.$queryRawUnsafe(`INSERT INTO metadata.field_metadata( await prisma.$queryRawUnsafe(`INSERT INTO metadata.field_metadata(
id, object_id, type, name, label, target_column_map, description, icon, enums, is_custom, is_active, is_nullable, workspace_id id, object_id, type, name, label, target_column_map, description, icon, enums, is_custom, is_active, is_nullable, workspace_id
) )
VALUES ( VALUES (
'22f5906d-153f-448c-b254-28adce721dcd', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'name', 'Name', '{"value": "name"}', 'Name', 'user', NULL, false, true, false, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419' '22f5906d-153f-448c-b254-28adce721dcd', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'name', 'Name', '{"value": "name"}', 'Name', 'IconUser', NULL, false, true, false, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
), ( ), (
'19bfab29-1cbb-4ce2-9117-8540ac45a0f1', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'domainName', 'Domain Name', '{"value": "domainName"}', 'Domain Name', 'link', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419' '19bfab29-1cbb-4ce2-9117-8540ac45a0f1', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'domainName', 'Domain Name', '{"value": "domainName"}', 'Domain Name', 'IconExternalLink', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
), ( ), (
'70130f27-9497-4b44-a04c-1a0fb9a4829c', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'address', 'Address', '{"value": "address"}', 'Address', 'location', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419' '70130f27-9497-4b44-a04c-1a0fb9a4829c', 'ba391617-ee08-432f-9438-2e17df5ac279', 'text', 'address', 'Address', '{"value": "address"}', 'Address', 'IconMap', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
), ( ), (
'2a63c30e-8e80-475b-b5d7-9dda17adc537', 'ba391617-ee08-432f-9438-2e17df5ac279', 'number', 'employees', 'Employees', '{"value": "employees"}', 'Employees', 'people', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419' '2a63c30e-8e80-475b-b5d7-9dda17adc537', 'ba391617-ee08-432f-9438-2e17df5ac279', 'number', 'employees', 'Employees', '{"value": "employees"}', 'Employees', 'IconUsers', NULL, false, true, true, 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419'
) ON CONFLICT DO NOTHING`); ) ON CONFLICT DO NOTHING`);
}; };

View File

@ -36,6 +36,7 @@ export type FieldMetadataTargetColumnMap = {
defaultResultSize: 10, defaultResultSize: 10,
disableFilter: true, disableFilter: true,
disableSort: true, disableSort: true,
maxResultsSize: 1000,
}) })
@Unique('IndexOnNameObjectIdAndWorkspaceIdUnique', [ @Unique('IndexOnNameObjectIdAndWorkspaceIdUnique', [
'name', 'name',

View File

@ -33,6 +33,7 @@ import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook';
defaultResultSize: 10, defaultResultSize: 10,
disableFilter: true, disableFilter: true,
disableSort: true, disableSort: true,
maxResultsSize: 1000,
}) })
@CursorConnection('fields', () => FieldMetadata) @CursorConnection('fields', () => FieldMetadata)
@Unique('IndexOnNameSingularAndWorkspaceIdUnique', [ @Unique('IndexOnNameSingularAndWorkspaceIdUnique', [