[Issue-5772] Add sort feature on settings tables (#5787)
## Proposed Changes - Introduce a new custom hook - useTableSort to sort table content - Add test cases for the new custom hook - Integrate useTableSort hook on to the table in settings object and settings object field pages ## Related Issue https://github.com/twentyhq/twenty/issues/5772 ## Evidence https://github.com/twentyhq/twenty/assets/87609792/8be456ce-2fa5-44ec-8bbd-70fb6c8fdb30 ## Evidence after addressing review comments https://github.com/twentyhq/twenty/assets/87609792/c267e3da-72f9-4c0e-8c94-a38122d6395e ## Further comments Apologies for the large PR. Looking forward for the review --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0f75e14ab2
commit
59e14fabb4
@ -59,7 +59,7 @@ import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccoun
|
|||||||
import { SettingsNewAccount } from '~/pages/settings/accounts/SettingsNewAccount';
|
import { SettingsNewAccount } from '~/pages/settings/accounts/SettingsNewAccount';
|
||||||
import { SettingsCRMMigration } from '~/pages/settings/crm-migration/SettingsCRMMigration';
|
import { SettingsCRMMigration } from '~/pages/settings/crm-migration/SettingsCRMMigration';
|
||||||
import { SettingsNewObject } from '~/pages/settings/data-model/SettingsNewObject';
|
import { SettingsNewObject } from '~/pages/settings/data-model/SettingsNewObject';
|
||||||
import { SettingsObjectDetail } from '~/pages/settings/data-model/SettingsObjectDetail';
|
import { SettingsObjectDetailPage } from '~/pages/settings/data-model/SettingsObjectDetailPage';
|
||||||
import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit';
|
import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit';
|
||||||
import { SettingsObjectFieldEdit } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
import { SettingsObjectFieldEdit } from '~/pages/settings/data-model/SettingsObjectFieldEdit';
|
||||||
import { SettingsObjectNewFieldStep1 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
import { SettingsObjectNewFieldStep1 } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
|
||||||
@ -218,7 +218,7 @@ const createRouter = (
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.ObjectDetail}
|
path={SettingsPath.ObjectDetail}
|
||||||
element={<SettingsObjectDetail />}
|
element={<SettingsObjectDetailPage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.ObjectEdit}
|
path={SettingsPath.ObjectEdit}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Reference, StoreObject } from '@apollo/client';
|
import { Reference, StoreObject } from '@apollo/client';
|
||||||
import { ReadFieldFunction } from '@apollo/client/cache/core/types/common';
|
import { ReadFieldFunction } from '@apollo/client/cache/core/types/common';
|
||||||
|
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { sortAsc, sortDesc, sortNullsFirst, sortNullsLast } from '~/utils/sort';
|
import { sortAsc, sortDesc, sortNullsFirst, sortNullsLast } from '~/utils/sort';
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { getOrderByFieldForObjectMetadataItem } from '@/object-metadata/utils/getObjectOrderByField';
|
import { getOrderByFieldForObjectMetadataItem } from '@/object-metadata/utils/getObjectOrderByField';
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
|
|
||||||
export const useGetObjectOrderByField = ({
|
export const useGetObjectOrderByField = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||||
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
|
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const getOrderByFieldForObjectMetadataItem = (
|
export const getOrderByFieldForObjectMetadataItem = (
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const getOrderByForFieldMetadataType = (
|
export const getOrderByForFieldMetadataType = (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
|
|
||||||
export type RecordGqlOperationOrderBy = Array<{
|
export type RecordGqlOperationOrderBy = Array<{
|
||||||
[fieldName: string]: OrderBy | { [subFieldName: string]: OrderBy };
|
[fieldName: string]: OrderBy | { [subFieldName: string]: OrderBy };
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||||
|
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||||
|
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||||
|
import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray';
|
||||||
|
|
||||||
|
export const useCombinedGetTotalCount = ({
|
||||||
|
objectMetadataItems,
|
||||||
|
skip = false,
|
||||||
|
}: {
|
||||||
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
|
skip?: boolean;
|
||||||
|
}) => {
|
||||||
|
const operationSignatures = objectMetadataItems.map(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
variables: {},
|
||||||
|
fields: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
}) satisfies RecordGqlOperationSignature,
|
||||||
|
);
|
||||||
|
|
||||||
|
const findManyQuery = useGenerateCombinedFindManyRecordsQuery({
|
||||||
|
operationSignatures,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = useQuery<MultiObjectRecordQueryResult>(
|
||||||
|
findManyQuery ?? EMPTY_QUERY,
|
||||||
|
{
|
||||||
|
skip,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalCountByObjectMetadataItemNamePlural = Object.fromEntries(
|
||||||
|
Object.entries(data ?? {}).map(([namePlural, objectRecordConnection]) => [
|
||||||
|
namePlural,
|
||||||
|
objectRecordConnection.totalCount,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCountByObjectMetadataItemNamePlural,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { hasPositionField } from '@/object-metadata/utils/hasPositionField';
|
import { hasPositionField } from '@/object-metadata/utils/hasPositionField';
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
|
||||||
@ -8,6 +8,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|||||||
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
|
import { getOrderByForFieldMetadataType } from '@/object-metadata/utils/getOrderByForFieldMetadataType';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
import { Sort } from '../types/Sort';
|
import { Sort } from '../types/Sort';
|
||||||
|
|
||||||
export const turnSortsIntoOrderBy = (
|
export const turnSortsIntoOrderBy = (
|
||||||
|
|||||||
@ -2,13 +2,14 @@ import { isNonEmptyString } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOrderByField';
|
import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOrderByField';
|
||||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
import { SelectableRecord } from '@/object-record/select/types/SelectableRecord';
|
||||||
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
|
import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields';
|
||||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
|
|
||||||
export const useRecordsForSelect = ({
|
export const useRecordsForSelect = ({
|
||||||
searchFilterText,
|
searchFilterText,
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
||||||
import { OrderBy } from '@/object-metadata/types/OrderBy';
|
|
||||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
@ -10,6 +9,7 @@ import { EntityForSelect } from '@/object-record/relation-picker/types/EntityFor
|
|||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
|||||||
@ -1,28 +1,36 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ReactNode, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Nullable, useIcons } from 'twenty-ui';
|
import { IconMinus, IconPlus, isDefined, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType';
|
|
||||||
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
|
import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings';
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
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 { RELATION_TYPES } from '../../constants/RelationTypes';
|
import { RELATION_TYPES } from '../../constants/RelationTypes';
|
||||||
|
|
||||||
|
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
|
||||||
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
|
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||||
|
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||||
|
import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown';
|
||||||
|
import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown';
|
||||||
|
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
|
||||||
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
import { RelationMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
|
||||||
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
|
import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType';
|
||||||
|
|
||||||
type SettingsObjectFieldItemTableRowProps = {
|
type SettingsObjectFieldItemTableRowProps = {
|
||||||
ActionIcon: ReactNode;
|
settingsObjectDetailTableItem: SettingsObjectDetailTableItem;
|
||||||
fieldMetadataItem: FieldMetadataItem;
|
status: 'active' | 'disabled';
|
||||||
identifierType?: Nullable<FieldIdentifierType>;
|
mode: 'view' | 'new-field';
|
||||||
variant?: 'field-type' | 'identifier';
|
|
||||||
isRemoteObjectField?: boolean;
|
|
||||||
to?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledObjectFieldTableRow = styled(TableRow)`
|
export const StyledObjectFieldTableRow = styled(TableRow)`
|
||||||
@ -40,13 +48,19 @@ const StyledIconTableCell = styled(TableCell)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsObjectFieldItemTableRow = ({
|
export const SettingsObjectFieldItemTableRow = ({
|
||||||
ActionIcon,
|
settingsObjectDetailTableItem,
|
||||||
fieldMetadataItem,
|
mode,
|
||||||
identifierType,
|
status,
|
||||||
variant = 'field-type',
|
|
||||||
isRemoteObjectField,
|
|
||||||
to,
|
|
||||||
}: SettingsObjectFieldItemTableRowProps) => {
|
}: SettingsObjectFieldItemTableRowProps) => {
|
||||||
|
const { fieldMetadataItem, identifierType, objectMetadataItem } =
|
||||||
|
settingsObjectDetailTableItem;
|
||||||
|
|
||||||
|
const isRemoteObjectField = objectMetadataItem.isRemote;
|
||||||
|
|
||||||
|
const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type';
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const Icon = getIcon(fieldMetadataItem.icon);
|
const Icon = getIcon(fieldMetadataItem.icon);
|
||||||
@ -62,31 +76,94 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
const fieldType = fieldMetadataItem.type;
|
const fieldType = fieldMetadataItem.type;
|
||||||
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
|
const isFieldTypeSupported = isFieldTypeSupportedInSettings(fieldType);
|
||||||
|
|
||||||
if (!isFieldTypeSupported) return null;
|
|
||||||
|
|
||||||
const RelationIcon = relationType
|
const RelationIcon = relationType
|
||||||
? RELATION_TYPES[relationType].Icon
|
? RELATION_TYPES[relationType].Icon
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const isLabelIdentifier = isLabelIdentifierField({
|
||||||
|
fieldMetadataItem,
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canToggleField = !isLabelIdentifier;
|
||||||
|
|
||||||
|
const canBeSetAsLabelIdentifier =
|
||||||
|
objectMetadataItem.isCustom &&
|
||||||
|
!isLabelIdentifier &&
|
||||||
|
LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type);
|
||||||
|
|
||||||
|
const linkToNavigate = `./${getFieldSlug(fieldMetadataItem)}`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
activateMetadataField,
|
||||||
|
deactivateMetadataField,
|
||||||
|
deleteMetadataField,
|
||||||
|
} = useFieldMetadataItem();
|
||||||
|
|
||||||
|
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
|
||||||
|
deactivateMetadataField(activeFieldMetadatItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
|
||||||
|
const handleSetLabelIdentifierField = (
|
||||||
|
activeFieldMetadatItem: FieldMetadataItem,
|
||||||
|
) =>
|
||||||
|
updateOneObjectMetadataItem({
|
||||||
|
idToUpdate: objectMetadataItem.id,
|
||||||
|
updatePayload: {
|
||||||
|
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [, setActiveSettingsObjectFields] = useRecoilState(
|
||||||
|
settingsObjectFieldsFamilyState({
|
||||||
|
objectMetadataItemId: objectMetadataItem.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleField = () => {
|
||||||
|
setActiveSettingsObjectFields((previousFields) => {
|
||||||
|
const newFields = isDefined(previousFields)
|
||||||
|
? previousFields?.map((field) =>
|
||||||
|
field.id === fieldMetadataItem.id
|
||||||
|
? { ...field, isActive: !field.isActive }
|
||||||
|
: field,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return newFields;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeLabel =
|
||||||
|
variant === 'field-type'
|
||||||
|
? isRemoteObjectField
|
||||||
|
? 'Remote'
|
||||||
|
: fieldMetadataItem.isCustom
|
||||||
|
? 'Custom'
|
||||||
|
: 'Standard'
|
||||||
|
: variant === 'identifier'
|
||||||
|
? isDefined(identifierType)
|
||||||
|
? identifierType === 'label'
|
||||||
|
? 'Record text'
|
||||||
|
: 'Record image'
|
||||||
|
: ''
|
||||||
|
: '';
|
||||||
|
|
||||||
|
if (!isFieldTypeSupported) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledObjectFieldTableRow to={to}>
|
<StyledObjectFieldTableRow
|
||||||
|
to={mode === 'view' ? linkToNavigate : undefined}
|
||||||
|
>
|
||||||
<StyledNameTableCell>
|
<StyledNameTableCell>
|
||||||
{!!Icon && (
|
{!!Icon && (
|
||||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||||
)}
|
)}
|
||||||
{fieldMetadataItem.label}
|
{fieldMetadataItem.label}
|
||||||
</StyledNameTableCell>
|
</StyledNameTableCell>
|
||||||
<TableCell>
|
<TableCell>{typeLabel}</TableCell>
|
||||||
{variant === 'field-type' &&
|
|
||||||
(isRemoteObjectField
|
|
||||||
? 'Remote'
|
|
||||||
: fieldMetadataItem.isCustom
|
|
||||||
? 'Custom'
|
|
||||||
: 'Standard')}
|
|
||||||
{variant === 'identifier' &&
|
|
||||||
!!identifierType &&
|
|
||||||
(identifierType === 'label' ? 'Record text' : 'Record image')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<SettingsObjectFieldDataType
|
<SettingsObjectFieldDataType
|
||||||
Icon={RelationIcon}
|
Icon={RelationIcon}
|
||||||
@ -105,7 +182,48 @@ export const SettingsObjectFieldItemTableRow = ({
|
|||||||
value={fieldType}
|
value={fieldType}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<StyledIconTableCell>{ActionIcon}</StyledIconTableCell>
|
<StyledIconTableCell>
|
||||||
|
{status === 'active' ? (
|
||||||
|
mode === 'view' ? (
|
||||||
|
<SettingsObjectFieldActiveActionDropdown
|
||||||
|
isCustomField={fieldMetadataItem.isCustom === true}
|
||||||
|
scopeKey={fieldMetadataItem.id}
|
||||||
|
onEdit={() => navigate(linkToNavigate)}
|
||||||
|
onSetAsLabelIdentifier={
|
||||||
|
canBeSetAsLabelIdentifier
|
||||||
|
? () => handleSetLabelIdentifierField(fieldMetadataItem)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onDeactivate={
|
||||||
|
isLabelIdentifier
|
||||||
|
? undefined
|
||||||
|
: () => handleDisableField(fieldMetadataItem)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
canToggleField && (
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconMinus}
|
||||||
|
accent="tertiary"
|
||||||
|
onClick={handleToggleField}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : mode === 'view' ? (
|
||||||
|
<SettingsObjectFieldInactiveActionDropdown
|
||||||
|
isCustomField={fieldMetadataItem.isCustom === true}
|
||||||
|
scopeKey={fieldMetadataItem.id}
|
||||||
|
onActivate={() => activateMetadataField(fieldMetadataItem)}
|
||||||
|
onDelete={() => deleteMetadataField(fieldMetadataItem)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconPlus}
|
||||||
|
accent="tertiary"
|
||||||
|
onClick={handleToggleField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</StyledIconTableCell>
|
||||||
</StyledObjectFieldTableRow>
|
</StyledObjectFieldTableRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { useIcons } from 'twenty-ui';
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
||||||
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag';
|
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag';
|
||||||
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
||||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
|
||||||
type SettingsObjectItemTableRowProps = {
|
export type SettingsObjectMetadataItemTableRowProps = {
|
||||||
action: ReactNode;
|
action: ReactNode;
|
||||||
objectItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
to?: string;
|
link?: string;
|
||||||
|
totalObjectCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StyledObjectTableRow = styled(TableRow)`
|
export const StyledObjectTableRow = styled(TableRow)`
|
||||||
@ -30,35 +30,33 @@ const StyledActionTableCell = styled(TableCell)`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsObjectItemTableRow = ({
|
export const SettingsObjectMetadataItemTableRow = ({
|
||||||
action,
|
action,
|
||||||
objectItem,
|
objectMetadataItem,
|
||||||
to,
|
link,
|
||||||
}: SettingsObjectItemTableRowProps) => {
|
totalObjectCount,
|
||||||
|
}: SettingsObjectMetadataItemTableRowProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { totalCount } = useFindManyRecords({
|
|
||||||
objectNameSingular: objectItem.nameSingular,
|
|
||||||
});
|
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const Icon = getIcon(objectItem.icon);
|
const Icon = getIcon(objectMetadataItem.icon);
|
||||||
const objectTypeLabel = getObjectTypeLabel(objectItem);
|
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledObjectTableRow key={objectItem.namePlural} to={to}>
|
<StyledObjectTableRow key={objectMetadataItem.namePlural} to={link}>
|
||||||
<StyledNameTableCell>
|
<StyledNameTableCell>
|
||||||
{!!Icon && (
|
{!!Icon && (
|
||||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||||
)}
|
)}
|
||||||
{objectItem.labelPlural}
|
{objectMetadataItem.labelPlural}
|
||||||
</StyledNameTableCell>
|
</StyledNameTableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
<SettingsDataModelObjectTypeTag objectTypeLabel={objectTypeLabel} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{objectItem.fields.filter((field) => !field.isSystem).length}
|
{objectMetadataItem.fields.filter((field) => !field.isSystem).length}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{totalCount}</TableCell>
|
<TableCell align="right">{totalObjectCount}</TableCell>
|
||||||
<StyledActionTableCell>{action}</StyledActionTableCell>
|
<StyledActionTableCell>{action}</StyledActionTableCell>
|
||||||
</StyledObjectTableRow>
|
</StyledObjectTableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
|
export type SortedFieldByTableFamilyStateKey = {
|
||||||
|
objectMetadataItemId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const settingsObjectFieldsFamilyState = createFamilyState<
|
||||||
|
FieldMetadataItem[] | null,
|
||||||
|
SortedFieldByTableFamilyStateKey
|
||||||
|
>({
|
||||||
|
key: 'settingsObjectFieldsFamilyState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
// @ts-expect-error // Todo: remove usage of react-data-grid
|
|
||||||
import DataGrid, { DataGridProps } from 'react-data-grid';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
// @ts-expect-error // Todo: remove usage of react-data-grid
|
||||||
|
import DataGrid, { DataGridProps } from 'react-data-grid';
|
||||||
import { RGBA } from 'twenty-ui';
|
import { RGBA } from 'twenty-ui';
|
||||||
|
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
@ -107,12 +107,12 @@ const StyledDataGrid = styled(DataGrid)`
|
|||||||
}
|
}
|
||||||
` as typeof DataGrid;
|
` as typeof DataGrid;
|
||||||
|
|
||||||
type TableProps<Data> = DataGridProps<Data> & {
|
type SpreadsheetImportTableProps<Data> = DataGridProps<Data> & {
|
||||||
rowHeight?: number;
|
rowHeight?: number;
|
||||||
hiddenHeader?: boolean;
|
hiddenHeader?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Table = <Data,>({
|
export const SpreadsheetImportTable = <Data,>({
|
||||||
className,
|
className,
|
||||||
columns,
|
columns,
|
||||||
components,
|
components,
|
||||||
@ -123,7 +123,7 @@ export const Table = <Data,>({
|
|||||||
onRowsChange,
|
onRowsChange,
|
||||||
onSelectedRowsChange,
|
onSelectedRowsChange,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
}: TableProps<Data>) => {
|
}: SpreadsheetImportTableProps<Data>) => {
|
||||||
const { rtl } = useSpreadsheetImportInternal();
|
const { rtl } = useSpreadsheetImportInternal();
|
||||||
|
|
||||||
if (!rows?.length || !columns?.length) return null;
|
if (!rows?.length || !columns?.length) return null;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { Table } from '@/spreadsheet-import/components/Table';
|
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||||
|
|
||||||
import { generateSelectionColumns } from './SelectColumn';
|
import { generateSelectionColumns } from './SelectColumn';
|
||||||
@ -22,7 +22,7 @@ export const SelectHeaderTable = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<SpreadsheetImportTable
|
||||||
// Todo: remove usage of react-data-grid
|
// Todo: remove usage of react-data-grid
|
||||||
rowKeyGetter={(row: any) => importedRows.indexOf(row)}
|
rowKeyGetter={(row: any) => importedRows.indexOf(row)}
|
||||||
rows={importedRows}
|
rows={importedRows}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { Table } from '@/spreadsheet-import/components/Table';
|
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||||
import { Fields } from '@/spreadsheet-import/types';
|
import { Fields } from '@/spreadsheet-import/types';
|
||||||
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
|
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
|
||||||
|
|
||||||
@ -16,5 +16,11 @@ export const ExampleTable = <T extends string>({
|
|||||||
const data = useMemo(() => generateExampleRow(fields), [fields]);
|
const data = useMemo(() => generateExampleRow(fields), [fields]);
|
||||||
const columns = useMemo(() => generateColumns(fields), [fields]);
|
const columns = useMemo(() => generateColumns(fields), [fields]);
|
||||||
|
|
||||||
return <Table rows={data} columns={columns} className={'rdg-example'} />;
|
return (
|
||||||
|
<SpreadsheetImportTable
|
||||||
|
rows={data}
|
||||||
|
columns={columns}
|
||||||
|
className={'rdg-example'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { RowsChangeData } from 'react-data-grid';
|
|||||||
import { IconTrash } from 'twenty-ui';
|
import { IconTrash } from 'twenty-ui';
|
||||||
|
|
||||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||||
|
import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable';
|
||||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||||
import { Table } from '@/spreadsheet-import/components/Table';
|
|
||||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||||
import {
|
import {
|
||||||
Columns,
|
Columns,
|
||||||
@ -277,7 +277,7 @@ export const ValidationStep = <T extends string>({
|
|||||||
/>
|
/>
|
||||||
</StyledToolbar>
|
</StyledToolbar>
|
||||||
<StyledScrollContainer>
|
<StyledScrollContainer>
|
||||||
<Table
|
<SpreadsheetImportTable
|
||||||
rowKeyGetter={rowKeyGetter}
|
rowKeyGetter={rowKeyGetter}
|
||||||
rows={tableData}
|
rows={tableData}
|
||||||
onRowsChange={updateRow}
|
onRowsChange={updateRow}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
Placement,
|
Placement,
|
||||||
useFloating,
|
useFloating,
|
||||||
} from '@floating-ui/react';
|
} from '@floating-ui/react';
|
||||||
import { useRef } from 'react';
|
import { MouseEvent, useRef } from 'react';
|
||||||
import { Keys } from 'react-hotkeys-hook';
|
import { Keys } from 'react-hotkeys-hook';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
@ -93,6 +93,14 @@ export const Dropdown = ({
|
|||||||
toggleDropdown();
|
toggleDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClickableComponentClick = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
toggleDropdown();
|
||||||
|
onClickOutside?.();
|
||||||
|
};
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [refs.floating],
|
refs: [refs.floating],
|
||||||
callback: () => {
|
callback: () => {
|
||||||
@ -126,10 +134,7 @@ export const Dropdown = ({
|
|||||||
{clickableComponent && (
|
{clickableComponent && (
|
||||||
<div
|
<div
|
||||||
ref={refs.setReference}
|
ref={refs.setReference}
|
||||||
onClick={() => {
|
onClick={handleClickableComponentClick}
|
||||||
toggleDropdown();
|
|
||||||
onClickOutside?.();
|
|
||||||
}}
|
|
||||||
className={className}
|
className={className}
|
||||||
>
|
>
|
||||||
{clickableComponent}
|
{clickableComponent}
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
|
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
|
||||||
|
import { TableSortValue } from '@/ui/layout/table/types/TableSortValue';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { IconArrowDown, IconArrowUp } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const SortableTableHeader = ({
|
||||||
|
tableId,
|
||||||
|
fieldName,
|
||||||
|
label,
|
||||||
|
align = 'left',
|
||||||
|
initialSort,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
fieldName: string;
|
||||||
|
label: string;
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
initialSort?: TableSortValue;
|
||||||
|
}) => {
|
||||||
|
const [sortedFieldByTable, setSortedFieldByTable] = useRecoilState(
|
||||||
|
sortedFieldByTableFamilyState({ tableId }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortValue = sortedFieldByTable ?? initialSort;
|
||||||
|
|
||||||
|
const isSortOnThisField = sortValue?.fieldName === fieldName;
|
||||||
|
|
||||||
|
const sortDirection = isSortOnThisField ? sortValue.orderBy : null;
|
||||||
|
|
||||||
|
const isAsc =
|
||||||
|
sortDirection === 'AscNullsLast' || sortDirection === 'AscNullsFirst';
|
||||||
|
const isDesc =
|
||||||
|
sortDirection === 'DescNullsLast' || sortDirection === 'DescNullsFirst';
|
||||||
|
|
||||||
|
const isSortActive = isAsc || isDesc;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setSortedFieldByTable({
|
||||||
|
fieldName,
|
||||||
|
orderBy: isSortOnThisField
|
||||||
|
? sortValue.orderBy === 'AscNullsLast'
|
||||||
|
? 'DescNullsLast'
|
||||||
|
: 'AscNullsLast'
|
||||||
|
: 'DescNullsLast',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableHeader align={align} onClick={handleClick}>
|
||||||
|
{isSortActive && align === 'right' ? (
|
||||||
|
isAsc ? (
|
||||||
|
<IconArrowUp size="14" />
|
||||||
|
) : (
|
||||||
|
<IconArrowDown size="14" />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{label}
|
||||||
|
{isSortActive && align === 'left' ? (
|
||||||
|
isAsc ? (
|
||||||
|
<IconArrowUp size="14" />
|
||||||
|
) : (
|
||||||
|
<IconArrowDown size="14" />
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</TableHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,9 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledTableHeader = styled.div<{ align?: 'left' | 'center' | 'right' }>`
|
const StyledTableHeader = styled.div<{
|
||||||
|
align?: 'left' | 'center' | 'right';
|
||||||
|
onClick?: () => void;
|
||||||
|
}>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
@ -15,6 +18,7 @@ const StyledTableHeader = styled.div<{ align?: 'left' | 'center' | 'right' }>`
|
|||||||
: 'flex-start'};
|
: 'flex-start'};
|
||||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
text-align: ${({ align }) => align ?? 'left'};
|
text-align: ${({ align }) => align ?? 'left'};
|
||||||
|
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export { StyledTableHeader as TableHeader };
|
export { StyledTableHeader as TableHeader };
|
||||||
|
|||||||
@ -0,0 +1,119 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { MutableSnapshot, RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import {
|
||||||
|
mockedTableMetadata,
|
||||||
|
MockedTableType,
|
||||||
|
mockedTableData as tableData,
|
||||||
|
tableDataSortedByFieldsCountInAscendingOrder,
|
||||||
|
tableDataSortedByFieldsCountInDescendingOrder,
|
||||||
|
tableDataSortedBylabelInAscendingOrder,
|
||||||
|
tableDataSortedBylabelInDescendingOrder,
|
||||||
|
} from '~/testing/mock-data/tableData';
|
||||||
|
|
||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
|
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
|
||||||
|
|
||||||
|
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
||||||
|
|
||||||
|
interface WrapperProps {
|
||||||
|
children: ReactNode;
|
||||||
|
initializeState?: (mutableSnapshot: MutableSnapshot) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper: React.FC<WrapperProps> = ({ children, initializeState }) => (
|
||||||
|
<RecoilRoot initializeState={initializeState}>{children}</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useSortedArray hook', () => {
|
||||||
|
const initializeState =
|
||||||
|
(fieldName: keyof MockedTableType, orderBy: OrderBy) =>
|
||||||
|
({ set }: MutableSnapshot) => {
|
||||||
|
set(
|
||||||
|
sortedFieldByTableFamilyState({
|
||||||
|
tableId: mockedTableMetadata.tableId,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
fieldName,
|
||||||
|
orderBy,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
test('initial sorting behavior for string fields - Ascending', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useSortedArray(tableData, mockedTableMetadata),
|
||||||
|
{
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => (
|
||||||
|
<Wrapper
|
||||||
|
initializeState={initializeState('labelPlural', 'AscNullsLast')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Wrapper>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedData = result.current;
|
||||||
|
|
||||||
|
expect(sortedData).toEqual(tableDataSortedBylabelInAscendingOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initial sorting behavior for string fields - Descending', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useSortedArray(tableData, mockedTableMetadata),
|
||||||
|
{
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => (
|
||||||
|
<Wrapper
|
||||||
|
initializeState={initializeState('labelPlural', 'DescNullsLast')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Wrapper>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedData = result.current;
|
||||||
|
|
||||||
|
expect(sortedData).toEqual(tableDataSortedBylabelInDescendingOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initial sorting behavior for number fields - Ascending', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useSortedArray(tableData, mockedTableMetadata),
|
||||||
|
{
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => (
|
||||||
|
<Wrapper
|
||||||
|
initializeState={initializeState('fieldsCount', 'AscNullsLast')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Wrapper>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedData = result.current;
|
||||||
|
|
||||||
|
expect(sortedData).toEqual(tableDataSortedByFieldsCountInAscendingOrder);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initial sorting behavior for number fields - Descending', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useSortedArray(tableData, mockedTableMetadata),
|
||||||
|
{
|
||||||
|
wrapper: ({ children }: { children: ReactNode }) => (
|
||||||
|
<Wrapper
|
||||||
|
initializeState={initializeState('fieldsCount', 'DescNullsLast')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Wrapper>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedData = result.current;
|
||||||
|
|
||||||
|
expect(sortedData).toEqual(tableDataSortedByFieldsCountInDescendingOrder);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { sortedFieldByTableFamilyState } from '@/ui/layout/table/states/sortedFieldByTableFamilyState';
|
||||||
|
import { TableMetadata } from '@/ui/layout/table/types/TableMetadata';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const useSortedArray = <T>(
|
||||||
|
arrayToSort: T[],
|
||||||
|
tableMetadata: TableMetadata<T>,
|
||||||
|
): T[] => {
|
||||||
|
const sortedFieldByTable = useRecoilValue(
|
||||||
|
sortedFieldByTableFamilyState({ tableId: tableMetadata.tableId }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSort = tableMetadata.initialSort;
|
||||||
|
|
||||||
|
const sortedArray = useMemo(() => {
|
||||||
|
const sortValueToUse = isDefined(sortedFieldByTable)
|
||||||
|
? sortedFieldByTable
|
||||||
|
: initialSort;
|
||||||
|
|
||||||
|
if (!isDefined(sortValueToUse)) {
|
||||||
|
return arrayToSort;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortFieldName = sortValueToUse.fieldName as keyof T;
|
||||||
|
const sortFieldType = tableMetadata.fields.find(
|
||||||
|
(field) => field.fieldName === sortFieldName,
|
||||||
|
)?.fieldType;
|
||||||
|
const sortOrder = sortValueToUse.orderBy;
|
||||||
|
|
||||||
|
return [...arrayToSort].sort((a: T, b: T) => {
|
||||||
|
if (sortFieldType === 'string') {
|
||||||
|
return sortOrder === 'AscNullsLast' || sortOrder === 'AscNullsFirst'
|
||||||
|
? (a[sortFieldName] as string)?.localeCompare(
|
||||||
|
b[sortFieldName] as string,
|
||||||
|
)
|
||||||
|
: (b[sortFieldName] as string)?.localeCompare(
|
||||||
|
a[sortFieldName] as string,
|
||||||
|
);
|
||||||
|
} else if (sortFieldType === 'number') {
|
||||||
|
return sortOrder === 'AscNullsLast' || sortOrder === 'AscNullsFirst'
|
||||||
|
? (a[sortFieldName] as number) - (b[sortFieldName] as number)
|
||||||
|
: (b[sortFieldName] as number) - (a[sortFieldName] as number);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [arrayToSort, tableMetadata, initialSort, sortedFieldByTable]);
|
||||||
|
|
||||||
|
return sortedArray;
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { TableSortValue } from '@/ui/layout/table/types/TableSortValue';
|
||||||
|
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||||
|
|
||||||
|
export type SortedFieldByTableFamilyStateKey = {
|
||||||
|
tableId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortedFieldByTableFamilyState = createFamilyState<
|
||||||
|
TableSortValue | null,
|
||||||
|
SortedFieldByTableFamilyStateKey
|
||||||
|
>({
|
||||||
|
key: 'sortedFieldByTableFamilyState',
|
||||||
|
defaultValue: null,
|
||||||
|
});
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
export type TableFieldMetadata<ItemType> = {
|
||||||
|
fieldLabel: string;
|
||||||
|
fieldName: keyof ItemType;
|
||||||
|
fieldType: 'string' | 'number';
|
||||||
|
align: 'left' | 'right';
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { TableFieldMetadata } from '@/ui/layout/table/types/TableFieldMetadata';
|
||||||
|
import { TableSortValue } from '@/ui/layout/table/types/TableSortValue';
|
||||||
|
|
||||||
|
export type TableMetadata<ItemType> = {
|
||||||
|
tableId: string;
|
||||||
|
fields: TableFieldMetadata<ItemType>[];
|
||||||
|
initialSort?: TableSortValue;
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { OrderBy } from '@/types/OrderBy';
|
||||||
|
|
||||||
|
export type TableSortValue = {
|
||||||
|
fieldName: string;
|
||||||
|
orderBy: OrderBy;
|
||||||
|
};
|
||||||
@ -1,237 +0,0 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes';
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
|
||||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
|
||||||
import { getActiveFieldMetadataItems } from '@/object-metadata/utils/getActiveFieldMetadataItems';
|
|
||||||
import { getDisabledFieldMetadataItems } from '@/object-metadata/utils/getDisabledFieldMetadataItems';
|
|
||||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown';
|
|
||||||
import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown';
|
|
||||||
import {
|
|
||||||
SettingsObjectFieldItemTableRow,
|
|
||||||
StyledObjectFieldTableRow,
|
|
||||||
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
|
|
||||||
import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard';
|
|
||||||
import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType';
|
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
|
||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
|
||||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|
||||||
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
|
||||||
|
|
||||||
const StyledDiv = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsObjectDetail = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { objectSlug = '' } = useParams();
|
|
||||||
const { findActiveObjectMetadataItemBySlug } =
|
|
||||||
useFilteredObjectMetadataItems();
|
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
|
||||||
|
|
||||||
const activeObjectMetadataItem =
|
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
|
||||||
}, [activeObjectMetadataItem, navigate]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
activateMetadataField,
|
|
||||||
deactivateMetadataField,
|
|
||||||
deleteMetadataField,
|
|
||||||
} = useFieldMetadataItem();
|
|
||||||
|
|
||||||
if (!activeObjectMetadataItem) return null;
|
|
||||||
|
|
||||||
const activeMetadataFields = getActiveFieldMetadataItems(
|
|
||||||
activeObjectMetadataItem,
|
|
||||||
);
|
|
||||||
const deactivatedMetadataFields = getDisabledFieldMetadataItems(
|
|
||||||
activeObjectMetadataItem,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDisableObject = async () => {
|
|
||||||
await updateOneObjectMetadataItem({
|
|
||||||
idToUpdate: activeObjectMetadataItem.id,
|
|
||||||
updatePayload: { isActive: false },
|
|
||||||
});
|
|
||||||
navigate(getSettingsPagePath(SettingsPath.Objects));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDisableField = (activeFieldMetadatItem: FieldMetadataItem) => {
|
|
||||||
deactivateMetadataField(activeFieldMetadatItem);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetLabelIdentifierField = (
|
|
||||||
activeFieldMetadatItem: FieldMetadataItem,
|
|
||||||
) =>
|
|
||||||
updateOneObjectMetadataItem({
|
|
||||||
idToUpdate: activeObjectMetadataItem.id,
|
|
||||||
updatePayload: {
|
|
||||||
labelIdentifierFieldMetadataId: activeFieldMetadatItem.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldDisplayAddFieldButton = !activeObjectMetadataItem.isRemote;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
|
||||||
<SettingsPageContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
|
||||||
{ children: activeObjectMetadataItem.labelPlural },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Section>
|
|
||||||
<H2Title title="About" description="Manage your object" />
|
|
||||||
<SettingsObjectSummaryCard
|
|
||||||
iconKey={activeObjectMetadataItem.icon ?? undefined}
|
|
||||||
name={activeObjectMetadataItem.labelPlural || ''}
|
|
||||||
objectMetadataItem={activeObjectMetadataItem}
|
|
||||||
onDeactivate={handleDisableObject}
|
|
||||||
onEdit={() => navigate('./edit')}
|
|
||||||
/>
|
|
||||||
</Section>
|
|
||||||
<Section>
|
|
||||||
<H2Title
|
|
||||||
title="Fields"
|
|
||||||
description={`Customise the fields available in the ${activeObjectMetadataItem.labelSingular} views and their display order in the ${activeObjectMetadataItem.labelSingular} detail view and menus.`}
|
|
||||||
/>
|
|
||||||
<Table>
|
|
||||||
<StyledObjectFieldTableRow>
|
|
||||||
<TableHeader>Name</TableHeader>
|
|
||||||
<TableHeader>
|
|
||||||
{activeObjectMetadataItem.isCustom
|
|
||||||
? 'Identifier'
|
|
||||||
: 'Field type'}
|
|
||||||
</TableHeader>
|
|
||||||
<TableHeader>Data type</TableHeader>
|
|
||||||
<TableHeader></TableHeader>
|
|
||||||
</StyledObjectFieldTableRow>
|
|
||||||
{!!activeMetadataFields.length && (
|
|
||||||
<TableSection title="Active">
|
|
||||||
{activeMetadataFields.map((activeMetadataField) => {
|
|
||||||
const isLabelIdentifier = isLabelIdentifierField({
|
|
||||||
fieldMetadataItem: activeMetadataField,
|
|
||||||
objectMetadataItem: activeObjectMetadataItem,
|
|
||||||
});
|
|
||||||
const canBeSetAsLabelIdentifier =
|
|
||||||
activeObjectMetadataItem.isCustom &&
|
|
||||||
!isLabelIdentifier &&
|
|
||||||
LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(
|
|
||||||
activeMetadataField.type,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsObjectFieldItemTableRow
|
|
||||||
key={activeMetadataField.id}
|
|
||||||
identifierType={getFieldIdentifierType(
|
|
||||||
activeMetadataField,
|
|
||||||
activeObjectMetadataItem,
|
|
||||||
)}
|
|
||||||
variant={
|
|
||||||
activeObjectMetadataItem.isCustom
|
|
||||||
? 'identifier'
|
|
||||||
: 'field-type'
|
|
||||||
}
|
|
||||||
fieldMetadataItem={activeMetadataField}
|
|
||||||
isRemoteObjectField={activeObjectMetadataItem.isRemote}
|
|
||||||
// to={`./${getFieldSlug(activeMetadataField)}`}
|
|
||||||
ActionIcon={
|
|
||||||
<SettingsObjectFieldActiveActionDropdown
|
|
||||||
isCustomField={!!activeMetadataField.isCustom}
|
|
||||||
scopeKey={activeMetadataField.id}
|
|
||||||
onEdit={() =>
|
|
||||||
navigate(`./${getFieldSlug(activeMetadataField)}`)
|
|
||||||
}
|
|
||||||
onSetAsLabelIdentifier={
|
|
||||||
canBeSetAsLabelIdentifier
|
|
||||||
? () =>
|
|
||||||
handleSetLabelIdentifierField(
|
|
||||||
activeMetadataField,
|
|
||||||
)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onDeactivate={
|
|
||||||
isLabelIdentifier
|
|
||||||
? undefined
|
|
||||||
: () => handleDisableField(activeMetadataField)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableSection>
|
|
||||||
)}
|
|
||||||
{!!deactivatedMetadataFields.length && (
|
|
||||||
<TableSection isInitiallyExpanded={false} title="Inactive">
|
|
||||||
{deactivatedMetadataFields.map((deactivatedMetadataField) => (
|
|
||||||
<SettingsObjectFieldItemTableRow
|
|
||||||
key={deactivatedMetadataField.id}
|
|
||||||
variant={
|
|
||||||
activeObjectMetadataItem.isCustom
|
|
||||||
? 'identifier'
|
|
||||||
: 'field-type'
|
|
||||||
}
|
|
||||||
fieldMetadataItem={deactivatedMetadataField}
|
|
||||||
ActionIcon={
|
|
||||||
<SettingsObjectFieldInactiveActionDropdown
|
|
||||||
isCustomField={!!deactivatedMetadataField.isCustom}
|
|
||||||
scopeKey={deactivatedMetadataField.id}
|
|
||||||
onActivate={() =>
|
|
||||||
activateMetadataField(deactivatedMetadataField)
|
|
||||||
}
|
|
||||||
onDelete={() =>
|
|
||||||
deleteMetadataField(deactivatedMetadataField)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableSection>
|
|
||||||
)}
|
|
||||||
</Table>
|
|
||||||
{shouldDisplayAddFieldButton && (
|
|
||||||
<StyledDiv>
|
|
||||||
<UndecoratedLink
|
|
||||||
to={
|
|
||||||
deactivatedMetadataFields.length
|
|
||||||
? './new-field/step-1'
|
|
||||||
: './new-field/step-2'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
Icon={IconPlus}
|
|
||||||
title="Add Field"
|
|
||||||
size="small"
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</UndecoratedLink>
|
|
||||||
</StyledDiv>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
</SettingsPageContainer>
|
|
||||||
</SubMenuTopBarContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { AppPath } from '@/types/AppPath';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
import { SettingsObjectDetailPageContent } from '~/pages/settings/data-model/SettingsObjectDetailPageContent';
|
||||||
|
|
||||||
|
export const SettingsObjectDetailPage = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { objectSlug = '' } = useParams();
|
||||||
|
const { findActiveObjectMetadataItemBySlug } =
|
||||||
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const activeObjectMetadataItem =
|
||||||
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeObjectMetadataItem) navigate(AppPath.NotFound);
|
||||||
|
}, [activeObjectMetadataItem, navigate]);
|
||||||
|
|
||||||
|
if (!isDefined(activeObjectMetadataItem)) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsObjectDetailPageContent
|
||||||
|
objectMetadataItem={activeObjectMetadataItem}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,101 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
|
||||||
|
import { getDisabledFieldMetadataItems } from '@/object-metadata/utils/getDisabledFieldMetadataItems';
|
||||||
|
import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||||
|
|
||||||
|
const StyledDiv = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type SettingsObjectDetailPageContentProps = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsObjectDetailPageContent = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
}: SettingsObjectDetailPageContentProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
|
||||||
|
const handleDisableObject = async () => {
|
||||||
|
await updateOneObjectMetadataItem({
|
||||||
|
idToUpdate: objectMetadataItem.id,
|
||||||
|
updatePayload: { isActive: false },
|
||||||
|
});
|
||||||
|
navigate(getSettingsPagePath(SettingsPath.Objects));
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledFieldMetadataItems =
|
||||||
|
getDisabledFieldMetadataItems(objectMetadataItem);
|
||||||
|
|
||||||
|
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
|
{ children: objectMetadataItem.labelPlural },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Section>
|
||||||
|
<H2Title title="About" description="Manage your object" />
|
||||||
|
<SettingsObjectSummaryCard
|
||||||
|
iconKey={objectMetadataItem.icon ?? undefined}
|
||||||
|
name={objectMetadataItem.labelPlural || ''}
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
onDeactivate={handleDisableObject}
|
||||||
|
onEdit={() => navigate('./edit')}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Fields"
|
||||||
|
description={`Customise the fields available in the ${objectMetadataItem.labelSingular} views and their display order in the ${objectMetadataItem.labelSingular} detail view and menus.`}
|
||||||
|
/>
|
||||||
|
<SettingsObjectFieldTable
|
||||||
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
mode="view"
|
||||||
|
/>
|
||||||
|
{shouldDisplayAddFieldButton && (
|
||||||
|
<StyledDiv>
|
||||||
|
<UndecoratedLink
|
||||||
|
to={
|
||||||
|
isNonEmptyArray(disabledFieldMetadataItems)
|
||||||
|
? './new-field/step-1'
|
||||||
|
: './new-field/step-2'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Add Field"
|
||||||
|
size="small"
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
|
</StyledDiv>
|
||||||
|
)}
|
||||||
|
</Section>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import {
|
||||||
|
SettingsObjectFieldItemTableRow,
|
||||||
|
StyledObjectFieldTableRow,
|
||||||
|
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
|
||||||
|
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
|
||||||
|
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
|
||||||
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
|
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||||
|
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
||||||
|
import { TableMetadata } from '@/ui/layout/table/types/TableMetadata';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { useMapFieldMetadataItemToSettingsObjectDetailTableItem } from '~/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem';
|
||||||
|
import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
|
||||||
|
|
||||||
|
const SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata<SettingsObjectDetailTableItem> =
|
||||||
|
{
|
||||||
|
tableId: 'settingsObjectDetail',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Name',
|
||||||
|
fieldName: 'label',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Field type',
|
||||||
|
fieldName: 'fieldType',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Data type',
|
||||||
|
fieldName: 'dataType',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialSort: {
|
||||||
|
fieldName: 'label',
|
||||||
|
orderBy: 'AscNullsLast',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM: TableMetadata<SettingsObjectDetailTableItem> =
|
||||||
|
{
|
||||||
|
tableId: 'settingsObjectDetail',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Name',
|
||||||
|
fieldName: 'label',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Identifier',
|
||||||
|
fieldName: 'identifierType',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Data type',
|
||||||
|
fieldName: 'dataType',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialSort: {
|
||||||
|
fieldName: 'label',
|
||||||
|
orderBy: 'AscNullsLast',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SettingsObjectFieldTableProps = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
mode: 'view' | 'new-field';
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: find another way than using mode which feels like it could be replaced by another pattern
|
||||||
|
export const SettingsObjectFieldTable = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
mode,
|
||||||
|
}: SettingsObjectFieldTableProps) => {
|
||||||
|
const tableMetadata = objectMetadataItem.isCustom
|
||||||
|
? SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM
|
||||||
|
: SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD;
|
||||||
|
|
||||||
|
const { mapFieldMetadataItemToSettingsObjectDetailTableItem } =
|
||||||
|
useMapFieldMetadataItemToSettingsObjectDetailTableItem(objectMetadataItem);
|
||||||
|
|
||||||
|
const [settingsObjectFields, setSettingsObjectFields] = useRecoilState(
|
||||||
|
settingsObjectFieldsFamilyState({
|
||||||
|
objectMetadataItemId: objectMetadataItem.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSettingsObjectFields(objectMetadataItem.fields);
|
||||||
|
}, [objectMetadataItem, setSettingsObjectFields]);
|
||||||
|
|
||||||
|
const activeObjectSettingsDetailItems = useMemo(() => {
|
||||||
|
const activeMetadataFields = settingsObjectFields?.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
activeMetadataFields?.map(
|
||||||
|
mapFieldMetadataItemToSettingsObjectDetailTableItem,
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
settingsObjectFields,
|
||||||
|
mapFieldMetadataItemToSettingsObjectDetailTableItem,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const disabledObjectSettingsDetailItems = useMemo(() => {
|
||||||
|
const disabledFieldMetadataItems = settingsObjectFields?.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
!fieldMetadataItem.isActive && !fieldMetadataItem.isSystem,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
disabledFieldMetadataItems?.map(
|
||||||
|
mapFieldMetadataItemToSettingsObjectDetailTableItem,
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
settingsObjectFields,
|
||||||
|
mapFieldMetadataItemToSettingsObjectDetailTableItem,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const sortedActiveObjectSettingsDetailItems = useSortedArray(
|
||||||
|
activeObjectSettingsDetailItems,
|
||||||
|
tableMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedDisabledObjectSettingsDetailItems = useSortedArray(
|
||||||
|
disabledObjectSettingsDetailItems,
|
||||||
|
tableMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<StyledObjectFieldTableRow>
|
||||||
|
{tableMetadata.fields.map((item) => (
|
||||||
|
<SortableTableHeader
|
||||||
|
key={item.fieldName}
|
||||||
|
fieldName={item.fieldName}
|
||||||
|
label={item.fieldLabel}
|
||||||
|
tableId={tableMetadata.tableId}
|
||||||
|
initialSort={tableMetadata.initialSort}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</StyledObjectFieldTableRow>
|
||||||
|
{isNonEmptyArray(sortedActiveObjectSettingsDetailItems) && (
|
||||||
|
<TableSection title="Active">
|
||||||
|
{sortedActiveObjectSettingsDetailItems.map(
|
||||||
|
(objectSettingsDetailItem) => (
|
||||||
|
<SettingsObjectFieldItemTableRow
|
||||||
|
key={objectSettingsDetailItem.fieldMetadataItem.id}
|
||||||
|
settingsObjectDetailTableItem={objectSettingsDetailItem}
|
||||||
|
status="active"
|
||||||
|
mode={mode}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableSection>
|
||||||
|
)}
|
||||||
|
{isNonEmptyArray(sortedDisabledObjectSettingsDetailItems) && (
|
||||||
|
<TableSection
|
||||||
|
isInitiallyExpanded={mode === 'new-field' ? true : false}
|
||||||
|
title="Inactive"
|
||||||
|
>
|
||||||
|
{sortedDisabledObjectSettingsDetailItems.map(
|
||||||
|
(objectSettingsDetailItem) => (
|
||||||
|
<SettingsObjectFieldItemTableRow
|
||||||
|
key={objectSettingsDetailItem.fieldMetadataItem.id}
|
||||||
|
settingsObjectDetailTableItem={objectSettingsDetailItem}
|
||||||
|
status="disabled"
|
||||||
|
mode={mode}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</TableSection>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,27 +1,22 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { H2Title, IconMinus, IconPlus, IconSettings } from 'twenty-ui';
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
||||||
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
|
||||||
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 {
|
|
||||||
SettingsObjectFieldItemTableRow,
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
StyledObjectFieldTableRow,
|
import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
|
||||||
} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
|
||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
|
||||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||||
|
|
||||||
const StyledSection = styled(Section)`
|
const StyledSection = styled(Section)`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -43,62 +38,52 @@ export const SettingsObjectNewFieldStep1 = () => {
|
|||||||
const activeObjectMetadataItem =
|
const activeObjectMetadataItem =
|
||||||
findActiveObjectMetadataItemBySlug(objectSlug);
|
findActiveObjectMetadataItemBySlug(objectSlug);
|
||||||
|
|
||||||
|
const [settingsObjectFields] = useRecoilState(
|
||||||
|
settingsObjectFieldsFamilyState({
|
||||||
|
objectMetadataItemId: activeObjectMetadataItem?.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const { activateMetadataField, deactivateMetadataField } =
|
const { activateMetadataField, deactivateMetadataField } =
|
||||||
useFieldMetadataItem();
|
useFieldMetadataItem();
|
||||||
const [metadataFields, setMetadataFields] = useState(
|
|
||||||
activeObjectMetadataItem?.fields ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeMetadataFields = metadataFields.filter((field) => field.isActive);
|
const canSave = settingsObjectFields?.some(
|
||||||
const deactivatedMetadataFields = metadataFields.filter(
|
|
||||||
(field) => !field.isActive,
|
|
||||||
);
|
|
||||||
|
|
||||||
const canSave = metadataFields.some(
|
|
||||||
(field, index) =>
|
(field, index) =>
|
||||||
field.isActive !== activeObjectMetadataItem?.fields[index].isActive,
|
field.isActive !== activeObjectMetadataItem?.fields[index].isActive,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!activeObjectMetadataItem || !settingsObjectFields) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
settingsObjectFields.map((fieldMetadataItem, index) => {
|
||||||
|
if (
|
||||||
|
fieldMetadataItem.isActive ===
|
||||||
|
activeObjectMetadataItem.fields[index].isActive
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldMetadataItem.isActive
|
||||||
|
? activateMetadataField(fieldMetadataItem)
|
||||||
|
: deactivateMetadataField(fieldMetadataItem);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
navigate(`/settings/objects/${objectSlug}`);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeObjectMetadataItem) {
|
if (!activeObjectMetadataItem) {
|
||||||
navigate(AppPath.NotFound);
|
navigate(AppPath.NotFound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}, [activeObjectMetadataItem, navigate]);
|
||||||
if (!metadataFields.length)
|
|
||||||
setMetadataFields(activeObjectMetadataItem.fields);
|
|
||||||
}, [activeObjectMetadataItem, metadataFields.length, navigate]);
|
|
||||||
|
|
||||||
if (!activeObjectMetadataItem) return null;
|
if (!activeObjectMetadataItem) return null;
|
||||||
|
|
||||||
const handleToggleField = (fieldMetadataId: string) =>
|
|
||||||
setMetadataFields((previousFields) =>
|
|
||||||
previousFields.map((field) =>
|
|
||||||
field.id === fieldMetadataId
|
|
||||||
? { ...field, isActive: !field.isActive }
|
|
||||||
: field,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
await Promise.all(
|
|
||||||
metadataFields.map((metadataField, index) => {
|
|
||||||
if (
|
|
||||||
metadataField.isActive ===
|
|
||||||
activeObjectMetadataItem.fields[index].isActive
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadataField.isActive
|
|
||||||
? activateMetadataField(metadataField)
|
|
||||||
: deactivateMetadataField(metadataField);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
navigate(`/settings/objects/${objectSlug}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
@ -126,58 +111,10 @@ export const SettingsObjectNewFieldStep1 = () => {
|
|||||||
title="Check deactivated fields"
|
title="Check deactivated fields"
|
||||||
description="Before creating a custom field, check if it already exists in the deactivated section."
|
description="Before creating a custom field, check if it already exists in the deactivated section."
|
||||||
/>
|
/>
|
||||||
<Table>
|
<SettingsObjectFieldTable
|
||||||
<StyledObjectFieldTableRow>
|
objectMetadataItem={activeObjectMetadataItem}
|
||||||
<TableHeader>Name</TableHeader>
|
mode="new-field"
|
||||||
<TableHeader>Field type</TableHeader>
|
/>
|
||||||
<TableHeader>Data type</TableHeader>
|
|
||||||
<TableHeader></TableHeader>
|
|
||||||
</StyledObjectFieldTableRow>
|
|
||||||
{!!activeMetadataFields.length && (
|
|
||||||
<TableSection isInitiallyExpanded={false} title="Active">
|
|
||||||
{activeMetadataFields.map((activeMetadataField) => (
|
|
||||||
<SettingsObjectFieldItemTableRow
|
|
||||||
key={activeMetadataField.id}
|
|
||||||
fieldMetadataItem={activeMetadataField}
|
|
||||||
isRemoteObjectField={activeObjectMetadataItem.isRemote}
|
|
||||||
ActionIcon={
|
|
||||||
isLabelIdentifierField({
|
|
||||||
fieldMetadataItem: activeMetadataField,
|
|
||||||
objectMetadataItem: activeObjectMetadataItem,
|
|
||||||
}) ? undefined : (
|
|
||||||
<LightIconButton
|
|
||||||
Icon={IconMinus}
|
|
||||||
accent="tertiary"
|
|
||||||
onClick={() =>
|
|
||||||
handleToggleField(activeMetadataField.id)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableSection>
|
|
||||||
)}
|
|
||||||
{!!deactivatedMetadataFields.length && (
|
|
||||||
<TableSection title="Disabled">
|
|
||||||
{deactivatedMetadataFields.map((deactivatedMetadataField) => (
|
|
||||||
<SettingsObjectFieldItemTableRow
|
|
||||||
key={deactivatedMetadataField.name}
|
|
||||||
fieldMetadataItem={deactivatedMetadataField}
|
|
||||||
ActionIcon={
|
|
||||||
<LightIconButton
|
|
||||||
Icon={IconPlus}
|
|
||||||
accent="tertiary"
|
|
||||||
onClick={() =>
|
|
||||||
handleToggleField(deactivatedMetadataField.id)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableSection>
|
|
||||||
)}
|
|
||||||
</Table>
|
|
||||||
<StyledAddCustomFieldButton
|
<StyledAddCustomFieldButton
|
||||||
Icon={IconPlus}
|
Icon={IconPlus}
|
||||||
title="Add Custom Field"
|
title="Add Custom Field"
|
||||||
|
|||||||
@ -12,23 +12,31 @@ import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDelet
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
|
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
|
||||||
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 {
|
import {
|
||||||
SettingsObjectItemTableRow,
|
SettingsObjectMetadataItemTableRow,
|
||||||
StyledObjectTableRow,
|
StyledObjectTableRow,
|
||||||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
||||||
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage';
|
import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage';
|
||||||
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown';
|
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown';
|
||||||
|
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
|
||||||
import { Table } from '@/ui/layout/table/components/Table';
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||||
|
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
||||||
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata';
|
||||||
|
import { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
|
||||||
|
|
||||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
@ -41,11 +49,71 @@ const StyledH1Title = styled(H1Title)`
|
|||||||
export const SettingsObjects = () => {
|
export const SettingsObjects = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
|
|
||||||
useFilteredObjectMetadataItems();
|
|
||||||
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
|
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
|
||||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
|
||||||
|
const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
|
||||||
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const { totalCountByObjectMetadataItemNamePlural } = useCombinedGetTotalCount(
|
||||||
|
{
|
||||||
|
objectMetadataItems: [
|
||||||
|
...activeObjectMetadataItems,
|
||||||
|
...inactiveObjectMetadataItems,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeObjectSettingsArray = useMemo(
|
||||||
|
() =>
|
||||||
|
activeObjectMetadataItems.map(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
({
|
||||||
|
objectMetadataItem,
|
||||||
|
labelPlural: objectMetadataItem.labelPlural,
|
||||||
|
objectTypeLabel: getObjectTypeLabel(objectMetadataItem).labelText,
|
||||||
|
fieldsCount: objectMetadataItem.fields.filter(
|
||||||
|
(field) => !field.isSystem,
|
||||||
|
).length,
|
||||||
|
totalObjectCount:
|
||||||
|
totalCountByObjectMetadataItemNamePlural[
|
||||||
|
objectMetadataItem.namePlural
|
||||||
|
] ?? 0,
|
||||||
|
}) satisfies SettingsObjectTableItem,
|
||||||
|
),
|
||||||
|
[activeObjectMetadataItems, totalCountByObjectMetadataItemNamePlural],
|
||||||
|
);
|
||||||
|
|
||||||
|
const inactiveObjectSettingsArray = useMemo(
|
||||||
|
() =>
|
||||||
|
inactiveObjectMetadataItems.map(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
({
|
||||||
|
objectMetadataItem,
|
||||||
|
labelPlural: objectMetadataItem.labelPlural,
|
||||||
|
objectTypeLabel: getObjectTypeLabel(objectMetadataItem).labelText,
|
||||||
|
fieldsCount: objectMetadataItem.fields.filter(
|
||||||
|
(field) => !field.isSystem,
|
||||||
|
).length,
|
||||||
|
totalObjectCount:
|
||||||
|
totalCountByObjectMetadataItemNamePlural[
|
||||||
|
objectMetadataItem.namePlural
|
||||||
|
] ?? 0,
|
||||||
|
}) satisfies SettingsObjectTableItem,
|
||||||
|
),
|
||||||
|
[inactiveObjectMetadataItems, totalCountByObjectMetadataItemNamePlural],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedActiveObjectSettingsItems = useSortedArray(
|
||||||
|
activeObjectSettingsArray,
|
||||||
|
SETTINGS_OBJECT_TABLE_METADATA,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedInactiveObjectSettingsItems = useSortedArray(
|
||||||
|
inactiveObjectSettingsArray,
|
||||||
|
SETTINGS_OBJECT_TABLE_METADATA,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
@ -66,51 +134,67 @@ export const SettingsObjects = () => {
|
|||||||
<H2Title title="Existing objects" />
|
<H2Title title="Existing objects" />
|
||||||
<Table>
|
<Table>
|
||||||
<StyledObjectTableRow>
|
<StyledObjectTableRow>
|
||||||
<TableHeader>Name</TableHeader>
|
{SETTINGS_OBJECT_TABLE_METADATA.fields.map(
|
||||||
<TableHeader>Type</TableHeader>
|
(settingsObjectsTableMetadataField) => (
|
||||||
<TableHeader align="right">Fields</TableHeader>
|
<SortableTableHeader
|
||||||
<TableHeader align="right">Instances</TableHeader>
|
fieldName={settingsObjectsTableMetadataField.fieldName}
|
||||||
|
label={settingsObjectsTableMetadataField.fieldLabel}
|
||||||
|
tableId={SETTINGS_OBJECT_TABLE_METADATA.tableId}
|
||||||
|
align={settingsObjectsTableMetadataField.align}
|
||||||
|
initialSort={SETTINGS_OBJECT_TABLE_METADATA.initialSort}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
<TableHeader></TableHeader>
|
<TableHeader></TableHeader>
|
||||||
</StyledObjectTableRow>
|
</StyledObjectTableRow>
|
||||||
{!!activeObjectMetadataItems.length && (
|
{isNonEmptyArray(sortedActiveObjectSettingsItems) && (
|
||||||
<TableSection title="Active">
|
<TableSection title="Active">
|
||||||
{activeObjectMetadataItems.map((activeObjectMetadataItem) => (
|
{sortedActiveObjectSettingsItems.map((objectSettingsItem) => (
|
||||||
<SettingsObjectItemTableRow
|
<SettingsObjectMetadataItemTableRow
|
||||||
key={activeObjectMetadataItem.namePlural}
|
key={objectSettingsItem.objectMetadataItem.namePlural}
|
||||||
objectItem={activeObjectMetadataItem}
|
objectMetadataItem={objectSettingsItem.objectMetadataItem}
|
||||||
|
totalObjectCount={objectSettingsItem.totalObjectCount}
|
||||||
action={
|
action={
|
||||||
<StyledIconChevronRight
|
<StyledIconChevronRight
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
stroke={theme.icon.stroke.sm}
|
stroke={theme.icon.stroke.sm}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
to={`/settings/objects/${getObjectSlug(
|
link={`/settings/objects/${getObjectSlug(
|
||||||
activeObjectMetadataItem,
|
objectSettingsItem.objectMetadataItem,
|
||||||
)}`}
|
)}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableSection>
|
</TableSection>
|
||||||
)}
|
)}
|
||||||
{!!inactiveObjectMetadataItems.length && (
|
{isNonEmptyArray(inactiveObjectMetadataItems) && (
|
||||||
<TableSection title="Inactive">
|
<TableSection title="Inactive">
|
||||||
{inactiveObjectMetadataItems.map(
|
{sortedInactiveObjectSettingsItems.map(
|
||||||
(inactiveObjectMetadataItem) => (
|
(objectSettingsItem) => (
|
||||||
<SettingsObjectItemTableRow
|
<SettingsObjectMetadataItemTableRow
|
||||||
key={inactiveObjectMetadataItem.namePlural}
|
key={objectSettingsItem.objectMetadataItem.namePlural}
|
||||||
objectItem={inactiveObjectMetadataItem}
|
objectMetadataItem={
|
||||||
|
objectSettingsItem.objectMetadataItem
|
||||||
|
}
|
||||||
|
totalObjectCount={objectSettingsItem.totalObjectCount}
|
||||||
action={
|
action={
|
||||||
<SettingsObjectInactiveMenuDropDown
|
<SettingsObjectInactiveMenuDropDown
|
||||||
isCustomObject={inactiveObjectMetadataItem.isCustom}
|
isCustomObject={
|
||||||
scopeKey={inactiveObjectMetadataItem.namePlural}
|
objectSettingsItem.objectMetadataItem.isCustom
|
||||||
|
}
|
||||||
|
scopeKey={
|
||||||
|
objectSettingsItem.objectMetadataItem.namePlural
|
||||||
|
}
|
||||||
onActivate={() =>
|
onActivate={() =>
|
||||||
updateOneObjectMetadataItem({
|
updateOneObjectMetadataItem({
|
||||||
idToUpdate: inactiveObjectMetadataItem.id,
|
idToUpdate:
|
||||||
|
objectSettingsItem.objectMetadataItem.id,
|
||||||
updatePayload: { isActive: true },
|
updatePayload: { isActive: true },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onDelete={() =>
|
onDelete={() =>
|
||||||
deleteOneObjectMetadataItem(
|
deleteOneObjectMetadataItem(
|
||||||
inactiveObjectMetadataItem.id,
|
objectSettingsItem.objectMetadataItem.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -8,11 +8,11 @@ import {
|
|||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { sleep } from '~/utils/sleep';
|
import { sleep } from '~/utils/sleep';
|
||||||
|
|
||||||
import { SettingsObjectDetail } from '../SettingsObjectDetail';
|
import { SettingsObjectDetailPage } from '../SettingsObjectDetailPage';
|
||||||
|
|
||||||
const meta: Meta<PageDecoratorArgs> = {
|
const meta: Meta<PageDecoratorArgs> = {
|
||||||
title: 'Pages/Settings/DataModel/SettingsObjectDetail',
|
title: 'Pages/Settings/DataModel/SettingsObjectDetail',
|
||||||
component: SettingsObjectDetail,
|
component: SettingsObjectDetailPage,
|
||||||
decorators: [PageDecorator],
|
decorators: [PageDecorator],
|
||||||
args: {
|
args: {
|
||||||
routePath: '/settings/objects/:objectSlug',
|
routePath: '/settings/objects/:objectSlug',
|
||||||
@ -25,7 +25,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
|||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
export type Story = StoryObj<typeof SettingsObjectDetail>;
|
export type Story = StoryObj<typeof SettingsObjectDetailPage>;
|
||||||
|
|
||||||
export const StandardObject: Story = {
|
export const StandardObject: Story = {
|
||||||
play: async () => {
|
play: async () => {
|
||||||
|
|||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { TableMetadata } from '@/ui/layout/table/types/TableMetadata';
|
||||||
|
import { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
|
||||||
|
|
||||||
|
export const SETTINGS_OBJECT_TABLE_METADATA: TableMetadata<SettingsObjectTableItem> =
|
||||||
|
{
|
||||||
|
tableId: 'settingsObject',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldLabel: 'Name',
|
||||||
|
fieldName: 'labelPlural',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Type',
|
||||||
|
fieldName: 'objectTypeLabel',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Fields',
|
||||||
|
fieldName: 'fieldsCount',
|
||||||
|
fieldType: 'number',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldLabel: 'Instances',
|
||||||
|
fieldName: 'totalObjectCount',
|
||||||
|
fieldType: 'number',
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initialSort: {
|
||||||
|
fieldName: 'labelPlural',
|
||||||
|
orderBy: 'AscNullsLast',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType';
|
||||||
|
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
|
||||||
|
import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
|
||||||
|
import { getSettingsObjectFieldType } from '~/pages/settings/data-model/utils/getSettingsObjectFieldType';
|
||||||
|
|
||||||
|
export const useMapFieldMetadataItemToSettingsObjectDetailTableItem = (
|
||||||
|
objectMetadataItem: ObjectMetadataItem,
|
||||||
|
) => {
|
||||||
|
const getRelationMetadata = useGetRelationMetadata();
|
||||||
|
|
||||||
|
const mapFieldMetadataItemToSettingsObjectDetailTableItem = (
|
||||||
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
|
): SettingsObjectDetailTableItem => {
|
||||||
|
const fieldType = getSettingsObjectFieldType(
|
||||||
|
objectMetadataItem,
|
||||||
|
fieldMetadataItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { relationObjectMetadataItem } =
|
||||||
|
getRelationMetadata({
|
||||||
|
fieldMetadataItem,
|
||||||
|
}) ?? {};
|
||||||
|
|
||||||
|
const identifierType = getFieldIdentifierType(
|
||||||
|
fieldMetadataItem,
|
||||||
|
objectMetadataItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldMetadataItem,
|
||||||
|
fieldType: fieldType ?? '',
|
||||||
|
dataType:
|
||||||
|
relationObjectMetadataItem?.labelPlural ??
|
||||||
|
getSettingsFieldTypeConfig(fieldMetadataItem.type)?.label ??
|
||||||
|
'',
|
||||||
|
label: fieldMetadataItem.label,
|
||||||
|
identifierType: identifierType,
|
||||||
|
objectMetadataItem,
|
||||||
|
} satisfies SettingsObjectDetailTableItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mapFieldMetadataItemToSettingsObjectDetailTableItem };
|
||||||
|
};
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { FieldIdentifierType } from '@/settings/data-model/types/FieldIdentifierType';
|
||||||
|
|
||||||
|
export type SettingsObjectDetailTableItem = {
|
||||||
|
fieldMetadataItem: FieldMetadataItem;
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
fieldType: string | boolean;
|
||||||
|
label: string;
|
||||||
|
dataType: string;
|
||||||
|
identifierType?: FieldIdentifierType;
|
||||||
|
};
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
|
export type SettingsObjectTableItem = {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
totalObjectCount: number;
|
||||||
|
fieldsCount: number;
|
||||||
|
objectTypeLabel: string;
|
||||||
|
labelPlural: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType';
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const getSettingsObjectFieldType = (
|
||||||
|
objectMetadataItem: ObjectMetadataItem,
|
||||||
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
|
) => {
|
||||||
|
const variant = objectMetadataItem.isCustom ? 'identifier' : 'field-type';
|
||||||
|
|
||||||
|
const identifierType = getFieldIdentifierType(
|
||||||
|
fieldMetadataItem,
|
||||||
|
objectMetadataItem,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variant === 'field-type') {
|
||||||
|
return objectMetadataItem.isRemote
|
||||||
|
? 'Remote'
|
||||||
|
: fieldMetadataItem.isCustom
|
||||||
|
? 'Custom'
|
||||||
|
: 'Standard';
|
||||||
|
} else {
|
||||||
|
return isDefined(identifierType)
|
||||||
|
? identifierType === 'label'
|
||||||
|
? 'Record text'
|
||||||
|
: 'Record image'
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
};
|
||||||
71
packages/twenty-front/src/testing/mock-data/tableData.ts
Normal file
71
packages/twenty-front/src/testing/mock-data/tableData.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { TableMetadata } from '@/ui/layout/table/types/TableMetadata';
|
||||||
|
|
||||||
|
export type MockedTableType = {
|
||||||
|
labelPlural: string;
|
||||||
|
fieldsCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockedTableMetadata: TableMetadata<MockedTableType> = {
|
||||||
|
tableId: 'SettingsObjectDetail',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldName: 'labelPlural',
|
||||||
|
fieldType: 'string',
|
||||||
|
align: 'left',
|
||||||
|
fieldLabel: 'Name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'fieldsCount',
|
||||||
|
fieldType: 'number',
|
||||||
|
align: 'right',
|
||||||
|
fieldLabel: 'Fields Count',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockedTableData = [
|
||||||
|
{
|
||||||
|
labelPlural: 'Opportunities',
|
||||||
|
fieldsCount: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelPlural: 'Contact',
|
||||||
|
fieldsCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelPlural: 'Leads',
|
||||||
|
fieldsCount: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
labelPlural: 'Tasks',
|
||||||
|
fieldsCount: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableDataSortedBylabelInAscendingOrder = [
|
||||||
|
{ labelPlural: 'Contact', fieldsCount: 3 },
|
||||||
|
{ labelPlural: 'Leads', fieldsCount: 4 },
|
||||||
|
{ labelPlural: 'Opportunities', fieldsCount: 11 },
|
||||||
|
{ labelPlural: 'Tasks', fieldsCount: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableDataSortedBylabelInDescendingOrder = [
|
||||||
|
{ labelPlural: 'Tasks', fieldsCount: 5 },
|
||||||
|
{ labelPlural: 'Opportunities', fieldsCount: 11 },
|
||||||
|
{ labelPlural: 'Leads', fieldsCount: 4 },
|
||||||
|
{ labelPlural: 'Contact', fieldsCount: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableDataSortedByFieldsCountInAscendingOrder = [
|
||||||
|
{ labelPlural: 'Contact', fieldsCount: 3 },
|
||||||
|
{ labelPlural: 'Leads', fieldsCount: 4 },
|
||||||
|
{ labelPlural: 'Tasks', fieldsCount: 5 },
|
||||||
|
{ labelPlural: 'Opportunities', fieldsCount: 11 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableDataSortedByFieldsCountInDescendingOrder = [
|
||||||
|
{ labelPlural: 'Opportunities', fieldsCount: 11 },
|
||||||
|
{ labelPlural: 'Tasks', fieldsCount: 5 },
|
||||||
|
{ labelPlural: 'Leads', fieldsCount: 4 },
|
||||||
|
{ labelPlural: 'Contact', fieldsCount: 3 },
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user