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 = ({