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:
martmull
2025-10-15 17:13:29 +02:00
committed by GitHub
parent c0ed246a03
commit b16ab1b7c9
109 changed files with 2464 additions and 1357 deletions
@@ -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>
</>
);
};