{
- toggleDropdown();
- onClickOutside?.();
- }}
+ onClick={handleClickableComponentClick}
className={className}
>
{clickableComponent}
diff --git a/packages/twenty-front/src/modules/ui/layout/table/components/SortableTableHeader.tsx b/packages/twenty-front/src/modules/ui/layout/table/components/SortableTableHeader.tsx
new file mode 100644
index 000000000..fcdb02ebf
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/components/SortableTableHeader.tsx
@@ -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 (
+
+ {isSortActive && align === 'right' ? (
+ isAsc ? (
+
+ ) : (
+
+ )
+ ) : null}
+ {label}
+ {isSortActive && align === 'left' ? (
+ isAsc ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/table/components/TableHeader.tsx b/packages/twenty-front/src/modules/ui/layout/table/components/TableHeader.tsx
index 39e3a9ccc..c00a80c0e 100644
--- a/packages/twenty-front/src/modules/ui/layout/table/components/TableHeader.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/table/components/TableHeader.tsx
@@ -1,6 +1,9 @@
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;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
@@ -15,6 +18,7 @@ const StyledTableHeader = styled.div<{ align?: 'left' | 'center' | 'right' }>`
: 'flex-start'};
padding: 0 ${({ theme }) => theme.spacing(2)};
text-align: ${({ align }) => align ?? 'left'};
+ cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
`;
export { StyledTableHeader as TableHeader };
diff --git a/packages/twenty-front/src/modules/ui/layout/table/hooks/__tests__/useSortedArray.test.tsx b/packages/twenty-front/src/modules/ui/layout/table/hooks/__tests__/useSortedArray.test.tsx
new file mode 100644
index 000000000..05298000e
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/hooks/__tests__/useSortedArray.test.tsx
@@ -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
= ({ children, initializeState }) => (
+ {children}
+);
+
+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 }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ 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 }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ 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 }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ 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 }) => (
+
+ {children}
+
+ ),
+ },
+ );
+
+ const sortedData = result.current;
+
+ expect(sortedData).toEqual(tableDataSortedByFieldsCountInDescendingOrder);
+ });
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/table/hooks/useSortedArray.ts b/packages/twenty-front/src/modules/ui/layout/table/hooks/useSortedArray.ts
new file mode 100644
index 000000000..bc582345f
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/hooks/useSortedArray.ts
@@ -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 = (
+ arrayToSort: T[],
+ tableMetadata: TableMetadata,
+): 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;
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/table/states/sortedFieldByTableFamilyState.ts b/packages/twenty-front/src/modules/ui/layout/table/states/sortedFieldByTableFamilyState.ts
new file mode 100644
index 000000000..7e088dab7
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/states/sortedFieldByTableFamilyState.ts
@@ -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,
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/table/types/TableFieldMetadata.ts b/packages/twenty-front/src/modules/ui/layout/table/types/TableFieldMetadata.ts
new file mode 100644
index 000000000..4e807bda6
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/types/TableFieldMetadata.ts
@@ -0,0 +1,6 @@
+export type TableFieldMetadata = {
+ fieldLabel: string;
+ fieldName: keyof ItemType;
+ fieldType: 'string' | 'number';
+ align: 'left' | 'right';
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/table/types/TableMetadata.ts b/packages/twenty-front/src/modules/ui/layout/table/types/TableMetadata.ts
new file mode 100644
index 000000000..b4ba856f1
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/types/TableMetadata.ts
@@ -0,0 +1,8 @@
+import { TableFieldMetadata } from '@/ui/layout/table/types/TableFieldMetadata';
+import { TableSortValue } from '@/ui/layout/table/types/TableSortValue';
+
+export type TableMetadata = {
+ tableId: string;
+ fields: TableFieldMetadata[];
+ initialSort?: TableSortValue;
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/table/types/TableSortValue.ts b/packages/twenty-front/src/modules/ui/layout/table/types/TableSortValue.ts
new file mode 100644
index 000000000..f72ba0b5b
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/table/types/TableSortValue.ts
@@ -0,0 +1,6 @@
+import { OrderBy } from '@/types/OrderBy';
+
+export type TableSortValue = {
+ fieldName: string;
+ orderBy: OrderBy;
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx
deleted file mode 100644
index 3f551f772..000000000
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetail.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- navigate('./edit')}
- />
-
-
-
-
-
- Name
-
- {activeObjectMetadataItem.isCustom
- ? 'Identifier'
- : 'Field type'}
-
- Data type
-
-
- {!!activeMetadataFields.length && (
-
- {activeMetadataFields.map((activeMetadataField) => {
- const isLabelIdentifier = isLabelIdentifierField({
- fieldMetadataItem: activeMetadataField,
- objectMetadataItem: activeObjectMetadataItem,
- });
- const canBeSetAsLabelIdentifier =
- activeObjectMetadataItem.isCustom &&
- !isLabelIdentifier &&
- LABEL_IDENTIFIER_FIELD_METADATA_TYPES.includes(
- activeMetadataField.type,
- );
-
- return (
-
- navigate(`./${getFieldSlug(activeMetadataField)}`)
- }
- onSetAsLabelIdentifier={
- canBeSetAsLabelIdentifier
- ? () =>
- handleSetLabelIdentifierField(
- activeMetadataField,
- )
- : undefined
- }
- onDeactivate={
- isLabelIdentifier
- ? undefined
- : () => handleDisableField(activeMetadataField)
- }
- />
- }
- />
- );
- })}
-
- )}
- {!!deactivatedMetadataFields.length && (
-
- {deactivatedMetadataFields.map((deactivatedMetadataField) => (
-
- activateMetadataField(deactivatedMetadataField)
- }
- onDelete={() =>
- deleteMetadataField(deactivatedMetadataField)
- }
- />
- }
- />
- ))}
-
- )}
-
- {shouldDisplayAddFieldButton && (
-
-
-
-
-
- )}
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx
new file mode 100644
index 000000000..899c36909
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPage.tsx
@@ -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 (
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx
new file mode 100644
index 000000000..9f51f890c
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx
@@ -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 (
+
+
+
+
+
+ navigate('./edit')}
+ />
+
+
+
+
+ {shouldDisplayAddFieldButton && (
+
+
+
+
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx
new file mode 100644
index 000000000..fffa5f276
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldTable.tsx
@@ -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 =
+ {
+ 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 =
+ {
+ 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 (
+
+
+ {tableMetadata.fields.map((item) => (
+
+ ))}
+
+
+ {isNonEmptyArray(sortedActiveObjectSettingsDetailItems) && (
+
+ {sortedActiveObjectSettingsDetailItems.map(
+ (objectSettingsDetailItem) => (
+
+ ),
+ )}
+
+ )}
+ {isNonEmptyArray(sortedDisabledObjectSettingsDetailItems) && (
+
+ {sortedDisabledObjectSettingsDetailItems.map(
+ (objectSettingsDetailItem) => (
+
+ ),
+ )}
+
+ )}
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx
index 98d5d0859..6fbc2d1e6 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx
@@ -1,27 +1,22 @@
-import { useEffect, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
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 { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
-import {
- SettingsObjectFieldItemTableRow,
- StyledObjectFieldTableRow,
-} from '@/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow';
+
+import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
+import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState';
import { AppPath } from '@/types/AppPath';
import { Button } from '@/ui/input/button/components/Button';
-import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
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 { useRecoilState } from 'recoil';
+import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
const StyledSection = styled(Section)`
display: flex;
@@ -43,62 +38,52 @@ export const SettingsObjectNewFieldStep1 = () => {
const activeObjectMetadataItem =
findActiveObjectMetadataItemBySlug(objectSlug);
+ const [settingsObjectFields] = useRecoilState(
+ settingsObjectFieldsFamilyState({
+ objectMetadataItemId: activeObjectMetadataItem?.id,
+ }),
+ );
+
const { activateMetadataField, deactivateMetadataField } =
useFieldMetadataItem();
- const [metadataFields, setMetadataFields] = useState(
- activeObjectMetadataItem?.fields ?? [],
- );
- const activeMetadataFields = metadataFields.filter((field) => field.isActive);
- const deactivatedMetadataFields = metadataFields.filter(
- (field) => !field.isActive,
- );
-
- const canSave = metadataFields.some(
+ const canSave = settingsObjectFields?.some(
(field, index) =>
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(() => {
if (!activeObjectMetadataItem) {
navigate(AppPath.NotFound);
return;
}
-
- if (!metadataFields.length)
- setMetadataFields(activeObjectMetadataItem.fields);
- }, [activeObjectMetadataItem, metadataFields.length, navigate]);
+ }, [activeObjectMetadataItem, navigate]);
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 (
@@ -126,58 +111,10 @@ export const SettingsObjectNewFieldStep1 = () => {
title="Check deactivated fields"
description="Before creating a custom field, check if it already exists in the deactivated section."
/>
-
-
- Name
- Field type
- Data type
-
-
- {!!activeMetadataFields.length && (
-
- {activeMetadataFields.map((activeMetadataField) => (
-
- handleToggleField(activeMetadataField.id)
- }
- />
- )
- }
- />
- ))}
-
- )}
- {!!deactivatedMetadataFields.length && (
-
- {deactivatedMetadataFields.map((deactivatedMetadataField) => (
-
- handleToggleField(deactivatedMetadataField.id)
- }
- />
- }
- />
- ))}
-
- )}
-
+
theme.font.color.tertiary};
@@ -41,11 +49,71 @@ const StyledH1Title = styled(H1Title)`
export const SettingsObjects = () => {
const theme = useTheme();
- const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
- useFilteredObjectMetadataItems();
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
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 (
@@ -66,51 +134,67 @@ export const SettingsObjects = () => {
- Name
- Type
- Fields
- Instances
+ {SETTINGS_OBJECT_TABLE_METADATA.fields.map(
+ (settingsObjectsTableMetadataField) => (
+
+ ),
+ )}
- {!!activeObjectMetadataItems.length && (
+ {isNonEmptyArray(sortedActiveObjectSettingsItems) && (
- {activeObjectMetadataItems.map((activeObjectMetadataItem) => (
- (
+
}
- to={`/settings/objects/${getObjectSlug(
- activeObjectMetadataItem,
+ link={`/settings/objects/${getObjectSlug(
+ objectSettingsItem.objectMetadataItem,
)}`}
/>
))}
)}
- {!!inactiveObjectMetadataItems.length && (
+ {isNonEmptyArray(inactiveObjectMetadataItems) && (
- {inactiveObjectMetadataItems.map(
- (inactiveObjectMetadataItem) => (
- (
+
updateOneObjectMetadataItem({
- idToUpdate: inactiveObjectMetadataItem.id,
+ idToUpdate:
+ objectSettingsItem.objectMetadataItem.id,
updatePayload: { isActive: true },
})
}
onDelete={() =>
deleteOneObjectMetadataItem(
- inactiveObjectMetadataItem.id,
+ objectSettingsItem.objectMetadataItem.id,
)
}
/>
diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectDetail.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectDetail.stories.tsx
index 687cf19a2..977480343 100644
--- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectDetail.stories.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectDetail.stories.tsx
@@ -8,11 +8,11 @@ import {
import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/utils/sleep';
-import { SettingsObjectDetail } from '../SettingsObjectDetail';
+import { SettingsObjectDetailPage } from '../SettingsObjectDetailPage';
const meta: Meta = {
title: 'Pages/Settings/DataModel/SettingsObjectDetail',
- component: SettingsObjectDetail,
+ component: SettingsObjectDetailPage,
decorators: [PageDecorator],
args: {
routePath: '/settings/objects/:objectSlug',
@@ -25,7 +25,7 @@ const meta: Meta = {
export default meta;
-export type Story = StoryObj;
+export type Story = StoryObj;
export const StandardObject: Story = {
play: async () => {
diff --git a/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts b/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts
new file mode 100644
index 000000000..7ad50ba37
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/constants/SettingsObjectTableMetadata.ts
@@ -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 =
+ {
+ 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',
+ },
+ };
diff --git a/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts b/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts
new file mode 100644
index 000000000..2a965c019
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts
@@ -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 };
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/types/SettingsObjectDetailTableItem.ts b/packages/twenty-front/src/pages/settings/data-model/types/SettingsObjectDetailTableItem.ts
new file mode 100644
index 000000000..642d39203
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/types/SettingsObjectDetailTableItem.ts
@@ -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;
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/types/SettingsObjectTableItem.ts b/packages/twenty-front/src/pages/settings/data-model/types/SettingsObjectTableItem.ts
new file mode 100644
index 000000000..62b1a0ec0
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/types/SettingsObjectTableItem.ts
@@ -0,0 +1,9 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+
+export type SettingsObjectTableItem = {
+ objectMetadataItem: ObjectMetadataItem;
+ totalObjectCount: number;
+ fieldsCount: number;
+ objectTypeLabel: string;
+ labelPlural: string;
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/utils/getSettingsObjectFieldType.ts b/packages/twenty-front/src/pages/settings/data-model/utils/getSettingsObjectFieldType.ts
new file mode 100644
index 000000000..1386d810b
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/data-model/utils/getSettingsObjectFieldType.ts
@@ -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;
+ }
+};
diff --git a/packages/twenty-front/src/testing/mock-data/tableData.ts b/packages/twenty-front/src/testing/mock-data/tableData.ts
new file mode 100644
index 000000000..9ebe0f306
--- /dev/null
+++ b/packages/twenty-front/src/testing/mock-data/tableData.ts
@@ -0,0 +1,71 @@
+import { TableMetadata } from '@/ui/layout/table/types/TableMetadata';
+
+export type MockedTableType = {
+ labelPlural: string;
+ fieldsCount: number;
+};
+
+export const mockedTableMetadata: TableMetadata = {
+ 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 },
+];