1518 extensibility front add an application section in settings (#15056)
Protected by IS_APPLICATION_ENABLED featureFlag Add `Application` section in settings <img width="301" height="137" alt="image" src="https://github.com/user-attachments/assets/ee53bdd2-36f6-45c6-8646-17b1e08abf00" /> A `settings/applications` route listing all installed applications <img width="661" height="428" alt="image" src="https://github.com/user-attachments/assets/69d534c4-4e9e-452a-a3d9-ded0223bb457" /> Introduce a new Tag for application managed items <img width="885" height="759" alt="image" src="https://github.com/user-attachments/assets/19767be5-61e5-4bd2-a51d-54ed9bfb1923" /> A `settings/applications/<application_id>` details setting page listing all objects, serverlessFunctions and agents created by the application: <img width="917" height="778" alt="image" src="https://github.com/user-attachments/assets/7fc056a6-1d73-4242-b2eb-6f8955d8597d" /> A `settings/applications/<application_id>/<serverless_function_id>` <img width="905" height="652" alt="image" src="https://github.com/user-attachments/assets/56ca0021-26bf-42cb-9abf-34879f16050a" /> Add trigger tab in serverless function details (readonly for now) <img width="899" height="724" alt="image" src="https://github.com/user-attachments/assets/5eeefa35-f2a4-4fd8-a640-7b5c5891f226" /> Set object, serverless and agent setting detail pages readonly for managed items <img width="1075" height="859" alt="image" src="https://github.com/user-attachments/assets/57c73d69-4980-47a2-b752-8dc5ab494530" /> <img width="648" height="582" alt="image" src="https://github.com/user-attachments/assets/5ad5f3f7-3bc3-4e40-870a-4981c6492524" /> <img width="982" height="692" alt="image" src="https://github.com/user-attachments/assets/7ad756c4-5d33-4a0a-9eb8-416c040362b9" /> <img width="1077" height="647" alt="image" src="https://github.com/user-attachments/assets/e086b9f5-4062-4d10-82a9-4023de3cad3f" />
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
SettingsObjectMetadataItemTableRow,
|
||||
StyledObjectTableRow,
|
||||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
||||
import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata';
|
||||
import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
||||
import { IconChevronRight, IconSearch } from 'twenty-ui/display';
|
||||
import styled from '@emotion/styled';
|
||||
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
|
||||
import type { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
|
||||
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsObjectTable = ({
|
||||
activeObjects,
|
||||
inactiveObjects,
|
||||
withSearchBar = true,
|
||||
}: {
|
||||
activeObjects: ObjectMetadataItem[];
|
||||
inactiveObjects: ObjectMetadataItem[];
|
||||
withSearchBar?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
|
||||
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const { totalCountByObjectMetadataItemNamePlural } = useCombinedGetTotalCount(
|
||||
{
|
||||
objectMetadataItems: [...activeObjects, ...inactiveObjects],
|
||||
},
|
||||
);
|
||||
|
||||
const activeObjectSettingsArray = useMemo(
|
||||
() =>
|
||||
activeObjects.map(
|
||||
(objectMetadataItem) =>
|
||||
({
|
||||
objectMetadataItem,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
objectTypeLabel: getItemTagInfo(objectMetadataItem).labelText,
|
||||
fieldsCount: objectMetadataItem.fields.filter(
|
||||
(field) => !field.isSystem,
|
||||
).length,
|
||||
totalObjectCount:
|
||||
totalCountByObjectMetadataItemNamePlural[
|
||||
objectMetadataItem.namePlural
|
||||
] ?? 0,
|
||||
}) satisfies SettingsObjectTableItem,
|
||||
),
|
||||
[activeObjects, totalCountByObjectMetadataItemNamePlural],
|
||||
);
|
||||
|
||||
const inactiveObjectSettingsArray = useMemo(
|
||||
() =>
|
||||
inactiveObjects.map(
|
||||
(objectMetadataItem) =>
|
||||
({
|
||||
objectMetadataItem,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
objectTypeLabel: getItemTagInfo({
|
||||
isCustom: objectMetadataItem.isCustom,
|
||||
isRemote: objectMetadataItem.isRemote,
|
||||
}).labelText,
|
||||
fieldsCount: objectMetadataItem.fields.filter(
|
||||
(field) => !field.isSystem,
|
||||
).length,
|
||||
totalObjectCount:
|
||||
totalCountByObjectMetadataItemNamePlural[
|
||||
objectMetadataItem.namePlural
|
||||
] ?? 0,
|
||||
}) satisfies SettingsObjectTableItem,
|
||||
),
|
||||
[inactiveObjects, totalCountByObjectMetadataItemNamePlural],
|
||||
);
|
||||
|
||||
const sortedActiveObjectSettingsItems = useSortedArray(
|
||||
activeObjectSettingsArray,
|
||||
GET_SETTINGS_OBJECT_TABLE_METADATA,
|
||||
);
|
||||
|
||||
const sortedInactiveObjectSettingsItems = useSortedArray(
|
||||
inactiveObjectSettingsArray,
|
||||
GET_SETTINGS_OBJECT_TABLE_METADATA,
|
||||
);
|
||||
|
||||
const filteredActiveObjectSettingsItems = useMemo(
|
||||
() =>
|
||||
sortedActiveObjectSettingsItems.filter((item) => {
|
||||
const searchNormalized = normalizeSearchText(searchTerm);
|
||||
return (
|
||||
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
|
||||
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized)
|
||||
);
|
||||
}),
|
||||
[sortedActiveObjectSettingsItems, searchTerm],
|
||||
);
|
||||
|
||||
const filteredInactiveObjectSettingsItems = useMemo(
|
||||
() =>
|
||||
sortedInactiveObjectSettingsItems.filter((item) => {
|
||||
const searchNormalized = normalizeSearchText(searchTerm);
|
||||
return (
|
||||
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
|
||||
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized)
|
||||
);
|
||||
}),
|
||||
[sortedInactiveObjectSettingsItems, searchTerm],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{withSearchBar && (
|
||||
<StyledSearchInput
|
||||
instanceId="settings-objects-search"
|
||||
LeftIcon={IconSearch}
|
||||
placeholder={t`Search for an object...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Table>
|
||||
<StyledObjectTableRow>
|
||||
{GET_SETTINGS_OBJECT_TABLE_METADATA.fields.map(
|
||||
(settingsObjectsTableMetadataField) => (
|
||||
<SortableTableHeader
|
||||
key={settingsObjectsTableMetadataField.fieldName}
|
||||
fieldName={settingsObjectsTableMetadataField.fieldName}
|
||||
label={t(settingsObjectsTableMetadataField.fieldLabel)}
|
||||
tableId={GET_SETTINGS_OBJECT_TABLE_METADATA.tableId}
|
||||
align={settingsObjectsTableMetadataField.align}
|
||||
initialSort={GET_SETTINGS_OBJECT_TABLE_METADATA.initialSort}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
<TableHeader></TableHeader>
|
||||
</StyledObjectTableRow>
|
||||
{isNonEmptyArray(sortedActiveObjectSettingsItems) && (
|
||||
<TableSection title={t`Active`}>
|
||||
{filteredActiveObjectSettingsItems.map((objectSettingsItem) => (
|
||||
<SettingsObjectMetadataItemTableRow
|
||||
key={objectSettingsItem.objectMetadataItem.namePlural}
|
||||
objectMetadataItem={objectSettingsItem.objectMetadataItem}
|
||||
totalObjectCount={objectSettingsItem.totalObjectCount}
|
||||
action={
|
||||
<StyledIconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
link={getSettingsPath(SettingsPath.ObjectDetail, {
|
||||
objectNamePlural:
|
||||
objectSettingsItem.objectMetadataItem.namePlural,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</TableSection>
|
||||
)}
|
||||
{isNonEmptyArray(sortedInactiveObjectSettingsItems) && (
|
||||
<TableSection title={t`Inactive`}>
|
||||
{filteredInactiveObjectSettingsItems.map((objectSettingsItem) => (
|
||||
<SettingsObjectMetadataItemTableRow
|
||||
key={objectSettingsItem.objectMetadataItem.namePlural}
|
||||
objectMetadataItem={objectSettingsItem.objectMetadataItem}
|
||||
totalObjectCount={objectSettingsItem.totalObjectCount}
|
||||
action={
|
||||
<SettingsObjectInactiveMenuDropDown
|
||||
isCustomObject={
|
||||
objectSettingsItem.objectMetadataItem.isCustom
|
||||
}
|
||||
objectMetadataItemNamePlural={
|
||||
objectSettingsItem.objectMetadataItem.namePlural
|
||||
}
|
||||
onActivate={() =>
|
||||
updateOneObjectMetadataItem({
|
||||
idToUpdate: objectSettingsItem.objectMetadataItem.id,
|
||||
updatePayload: { isActive: true },
|
||||
})
|
||||
}
|
||||
onDelete={() =>
|
||||
deleteOneObjectMetadataItem(
|
||||
objectSettingsItem.objectMetadataItem.id,
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</TableSection>
|
||||
)}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user