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)
}
/>
}
/>

View File

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

View File

@ -15,18 +15,18 @@ export const seedMetadata = async (prisma: PrismaClient) => {
await prisma.$queryRawUnsafe(`CREATE TABLE IF NOT EXISTS
workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
"id" TEXT PRIMARY KEY,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"deletedAt" TIMESTAMP WITH TIME ZONE,
domainName TEXT NOT NULL,
address TEXT NOT NULL,
employees INTEGER NOT NULL
"domainName" TEXT NOT NULL,
"address" TEXT NOT NULL,
"employees" INTEGER NOT NULL
);
`);
await prisma.$queryRawUnsafe(`INSERT INTO workspace_twenty_7icsva0r6s00mpcp6cwg4w4rd.company(
id, name, domainName, address, employees
"id", "name", "domainName", "address", "employees"
)
VALUES (
'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
)
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`);
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
)
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`);
};

View File

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

View File

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