8299488f21
# Introduction Two things: - Enforcing non nullable workspace custom application Id for any workspace - Fixing front non editable data models following https://github.com/twentyhq/twenty/pull/15911 that associate any custom entities to an applicationId. The front was putting everything as readonly when under an app ( we will have to handle the twenty standard application in the future too ) ## Fallback ### Migration The non nullable migration will fail when released, that's why it's being swallowed and re-run in an upgrade command post workspace custom application creation for those that miss one. Allowing the migration to pass in the end The typeorm migration still need to exists for any new workspaces ### GetCurrentUser In order to dynamically display isReadOnly in data model settings we're fetching the workspaceCustomApplicationId through the `getCurrentUser` If not fallback this endpoint would throw until we're handling existing workspaces that do not have a custom workspace application The fallback should be removed post release
265 lines
9.6 KiB
TypeScript
265 lines
9.6 KiB
TypeScript
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
|
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
|
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
|
|
import {
|
|
SettingsObjectMetadataItemTableRow,
|
|
StyledObjectTableRow,
|
|
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
|
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
|
|
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
|
|
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
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 { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray';
|
|
import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState';
|
|
import { useTheme } from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useMemo, useState } from 'react';
|
|
import { useRecoilValue } from 'recoil';
|
|
import { SettingsPath } from 'twenty-shared/types';
|
|
import { getSettingsPath } from 'twenty-shared/utils';
|
|
import {
|
|
IconArchive,
|
|
IconChevronRight,
|
|
IconFilter,
|
|
IconSearch,
|
|
IconSettings,
|
|
} from 'twenty-ui/display';
|
|
import { Button } from 'twenty-ui/input';
|
|
import { MenuItemToggle } from 'twenty-ui/navigation';
|
|
import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata';
|
|
import type { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
|
|
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
|
|
|
const StyledIconChevronRight = styled(IconChevronRight)`
|
|
color: ${({ theme }) => theme.font.color.tertiary};
|
|
`;
|
|
|
|
const StyledSearchAndFilterContainer = styled.div`
|
|
display: flex;
|
|
gap: ${({ theme }) => theme.spacing(2)};
|
|
align-items: center;
|
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
|
`;
|
|
|
|
const StyledSearchInput = styled(SettingsTextInput)`
|
|
flex: 1;
|
|
width: 100%;
|
|
`;
|
|
|
|
export const SettingsObjectTable = ({
|
|
objectMetadataItems,
|
|
withSearchBar = true,
|
|
}: {
|
|
objectMetadataItems: ObjectMetadataItem[];
|
|
withSearchBar?: boolean;
|
|
}) => {
|
|
const { t } = useLingui();
|
|
|
|
const theme = useTheme();
|
|
|
|
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [showDeactivated, setShowDeactivated] = useState(false);
|
|
const [showSystemObjects, setShowSystemObjects] = useState(false);
|
|
|
|
const { deleteOneObjectMetadataItem } = useDeleteOneObjectMetadataItem();
|
|
|
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
|
|
|
const { totalCountByObjectMetadataItemNamePlural } = useCombinedGetTotalCount(
|
|
{
|
|
objectMetadataItems,
|
|
},
|
|
);
|
|
|
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
|
|
|
const allObjectSettingsArray = useMemo(
|
|
() =>
|
|
objectMetadataItems.map(
|
|
(objectMetadataItem) =>
|
|
({
|
|
objectMetadataItem,
|
|
labelPlural: objectMetadataItem.labelPlural,
|
|
objectTypeLabel: getItemTagInfo({
|
|
objectMetadataItem,
|
|
workspaceCustomApplicationId:
|
|
currentWorkspace?.workspaceCustomApplication?.id,
|
|
}).labelText,
|
|
fieldsCount: objectMetadataItem.fields.filter(
|
|
(field) => !field.isSystem,
|
|
).length,
|
|
totalObjectCount:
|
|
totalCountByObjectMetadataItemNamePlural[
|
|
objectMetadataItem.namePlural
|
|
] ?? 0,
|
|
}) satisfies SettingsObjectTableItem,
|
|
),
|
|
[
|
|
objectMetadataItems,
|
|
totalCountByObjectMetadataItemNamePlural,
|
|
currentWorkspace,
|
|
],
|
|
);
|
|
|
|
const sortedObjectSettingsItems = useSortedArray(
|
|
allObjectSettingsArray,
|
|
GET_SETTINGS_OBJECT_TABLE_METADATA,
|
|
);
|
|
|
|
const filteredObjectSettingsItems = useMemo(
|
|
() =>
|
|
sortedObjectSettingsItems.filter((item) => {
|
|
const searchNormalized = normalizeSearchText(searchTerm);
|
|
const matchesSearch =
|
|
normalizeSearchText(item.labelPlural).includes(searchNormalized) ||
|
|
normalizeSearchText(item.objectTypeLabel).includes(searchNormalized);
|
|
|
|
if (!matchesSearch) {
|
|
return false;
|
|
}
|
|
|
|
const isActive = item.objectMetadataItem.isActive;
|
|
if (!isActive && !showDeactivated) {
|
|
return false;
|
|
}
|
|
|
|
const isSystem = item.objectMetadataItem.isSystem;
|
|
if (isSystem && !showSystemObjects) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}),
|
|
[sortedObjectSettingsItems, searchTerm, showDeactivated, showSystemObjects],
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{withSearchBar && (
|
|
<StyledSearchAndFilterContainer>
|
|
<StyledSearchInput
|
|
instanceId="settings-objects-search"
|
|
LeftIcon={IconSearch}
|
|
placeholder={t`Search for an object...`}
|
|
value={searchTerm}
|
|
onChange={setSearchTerm}
|
|
/>
|
|
<Dropdown
|
|
dropdownId="settings-objects-filter-dropdown"
|
|
dropdownPlacement="bottom-end"
|
|
dropdownOffset={{ x: 0, y: 8 }}
|
|
clickableComponent={
|
|
<Button
|
|
Icon={IconFilter}
|
|
size="medium"
|
|
variant="secondary"
|
|
accent="default"
|
|
ariaLabel={t`Filter`}
|
|
/>
|
|
}
|
|
dropdownComponents={
|
|
<DropdownContent>
|
|
<DropdownMenuItemsContainer>
|
|
<MenuItemToggle
|
|
LeftIcon={IconArchive}
|
|
onToggleChange={() => setShowDeactivated(!showDeactivated)}
|
|
toggled={showDeactivated}
|
|
text={t`Deactivated`}
|
|
toggleSize="small"
|
|
/>
|
|
{isAdvancedModeEnabled && (
|
|
<MenuItemToggle
|
|
LeftIcon={IconSettings}
|
|
onToggleChange={() =>
|
|
setShowSystemObjects(!showSystemObjects)
|
|
}
|
|
toggled={showSystemObjects}
|
|
text={t`System objects`}
|
|
toggleSize="small"
|
|
/>
|
|
)}
|
|
</DropdownMenuItemsContainer>
|
|
</DropdownContent>
|
|
}
|
|
/>
|
|
</StyledSearchAndFilterContainer>
|
|
)}
|
|
|
|
<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>
|
|
{filteredObjectSettingsItems.map((objectSettingsItem) => {
|
|
const isActive = objectSettingsItem.objectMetadataItem.isActive;
|
|
|
|
return (
|
|
<SettingsObjectMetadataItemTableRow
|
|
key={objectSettingsItem.objectMetadataItem.namePlural}
|
|
objectMetadataItem={objectSettingsItem.objectMetadataItem}
|
|
totalObjectCount={objectSettingsItem.totalObjectCount}
|
|
action={
|
|
isActive ? (
|
|
<StyledIconChevronRight
|
|
size={theme.icon.size.md}
|
|
stroke={theme.icon.stroke.sm}
|
|
/>
|
|
) : (
|
|
<SettingsObjectInactiveMenuDropDown
|
|
isCustomObject={
|
|
objectSettingsItem.objectMetadataItem.isCustom
|
|
}
|
|
objectMetadataItemNamePlural={
|
|
objectSettingsItem.objectMetadataItem.namePlural
|
|
}
|
|
onActivate={() =>
|
|
updateOneObjectMetadataItem({
|
|
idToUpdate: objectSettingsItem.objectMetadataItem.id,
|
|
updatePayload: { isActive: true },
|
|
})
|
|
}
|
|
onDelete={() =>
|
|
deleteOneObjectMetadataItem(
|
|
objectSettingsItem.objectMetadataItem.id,
|
|
)
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
link={
|
|
isActive
|
|
? getSettingsPath(SettingsPath.ObjectDetail, {
|
|
objectNamePlural:
|
|
objectSettingsItem.objectMetadataItem.namePlural,
|
|
})
|
|
: undefined
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
</Table>
|
|
</>
|
|
);
|
|
};
|