783fe8d387
## 🐛 Issues Fixed ### 1. Role Editing Not Working for New Agents When creating a new agent and then creating a role for it, the role permissions appeared as non-editable. This was caused by: - In create mode, `agentId = ''` (empty string from `useParams`) - Empty string is **falsy** but `isDefined('')` returns **true** - This caused incorrect behavior in role exclusivity checks and API calls ### 2. Missing Role Data Loading The agent form wasn't loading role data needed for permission editing because it was missing the `SettingsRolesQueryEffect` component. ### 3. Navigation Conflicts The `useSaveDraftRoleToDB` hook contained navigation logic that caused conflicts when used from different contexts (role detail page vs agent form). ### 4. Linting Errors Unused `useIcons` import in `SettingsAgentRoleTab.tsx`. --- ## 🔧 Changes Made ### Agent Role Tab (`SettingsAgentRoleTab.tsx`) - ✅ Use `isNonEmptyString(agentId)` to validate agentId (follows codebase patterns) - ✅ Improved role exclusivity logic to handle both create and edit modes: - **Edit mode**: Role must be assigned exclusively to this agent - **Create mode**: Role must not be assigned to anyone yet - ✅ Only call `assignRoleToAgent` when a valid agentId exists - ✅ Pass `undefined` instead of empty string for `fromAgentId` prop ### Role Hook (`useSaveDraftRoleToDB.ts`) - ✅ Removed navigation logic from the hook (better separation of concerns) - ✅ Removed `useNavigateSettings` and `SettingsPath` dependencies - ✅ Hook now only handles data persistence, not navigation ### Role Component (`SettingsRole.tsx`) - ✅ Added navigation after successful role creation in create mode - ✅ Navigation now handled by the component using the hook ### Agent Form (`SettingsAgentForm.tsx`) - ✅ Added `SettingsRolesQueryEffect` to ensure role data is loaded - ✅ Agent form handles its own navigation after save --- ## ✅ Testing Scenarios All these scenarios now work correctly: 1. ✅ Create new agent → Create role → Edit permissions → Save agent 2. ✅ Edit existing agent → Create role → Edit permissions → Save 3. ✅ Edit existing agent → Try to edit shared role (shows warning message) 4. ✅ Navigate to object-level permissions from agent form with proper breadcrumbs --- ## 📝 Technical Details ### Root Cause Analysis The main issue was that empty string was being treated differently in different checks: - `agentId &&` → evaluates to `false` (empty string is falsy) - `isDefined(agentId)` → returns `true` (empty string is defined) This inconsistency caused the role to appear as non-editable and attempted invalid API calls. ### Solution Used `isNonEmptyString()` from `@sniptt/guards` which properly checks both that the value is defined AND not empty, following codebase conventions. --- ## 🎯 Impact - Fixes critical bug preventing role permission configuration for new agents - Improves code quality with better separation of concerns - Makes navigation logic more predictable and maintainable - Follows codebase patterns and guidelines
232 lines
7.6 KiB
TypeScript
232 lines
7.6 KiB
TypeScript
import { type 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 { 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 { type TableMetadata } from '@/ui/layout/table/types/TableMetadata';
|
|
import styled from '@emotion/styled';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useRecoilState } from 'recoil';
|
|
import { IconArchive, IconFilter, IconSearch } from 'twenty-ui/display';
|
|
import { Button } from 'twenty-ui/input';
|
|
import { MenuItemToggle } from 'twenty-ui/navigation';
|
|
import { useMapFieldMetadataItemToSettingsObjectDetailTableItem } from '~/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem';
|
|
import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
|
|
import { normalizeSearchText } from '~/utils/normalizeSearchText';
|
|
|
|
const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_STANDARD: TableMetadata<SettingsObjectDetailTableItem> =
|
|
{
|
|
tableId: 'settingsObjectDetail',
|
|
fields: [
|
|
{
|
|
fieldLabel: msg`Name`,
|
|
fieldName: 'label',
|
|
fieldType: 'string',
|
|
align: 'left',
|
|
},
|
|
{
|
|
fieldLabel: msg`Field type`,
|
|
fieldName: 'fieldType',
|
|
fieldType: 'string',
|
|
align: 'left',
|
|
},
|
|
{
|
|
fieldLabel: msg`Data type`,
|
|
fieldName: 'dataType',
|
|
fieldType: 'string',
|
|
align: 'left',
|
|
},
|
|
],
|
|
initialSort: {
|
|
fieldName: 'label',
|
|
orderBy: 'AscNullsLast',
|
|
},
|
|
};
|
|
|
|
const GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM: TableMetadata<SettingsObjectDetailTableItem> =
|
|
{
|
|
tableId: 'settingsObjectDetail',
|
|
fields: [
|
|
{
|
|
fieldLabel: msg`Name`,
|
|
fieldName: 'label',
|
|
fieldType: 'string',
|
|
align: 'left',
|
|
},
|
|
{
|
|
fieldLabel: msg`Identifier`,
|
|
fieldName: 'identifierType',
|
|
fieldType: 'string',
|
|
align: 'left',
|
|
},
|
|
{
|
|
fieldLabel: msg`Data type`,
|
|
fieldName: 'dataType',
|
|
fieldType: 'string',
|
|
align: 'left',
|
|
},
|
|
],
|
|
initialSort: {
|
|
fieldName: 'label',
|
|
orderBy: 'AscNullsLast',
|
|
},
|
|
};
|
|
|
|
const StyledSearchAndFilterContainer = styled.div`
|
|
display: flex;
|
|
gap: ${({ theme }) => theme.spacing(2)};
|
|
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
|
width: 100%;
|
|
`;
|
|
|
|
const StyledSearchInput = styled(SettingsTextInput)`
|
|
flex: 1;
|
|
`;
|
|
|
|
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 { t } = useLingui();
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [showInactive, setShowInactive] = useState(mode === 'new-field');
|
|
|
|
const tableMetadata = objectMetadataItem.isCustom
|
|
? GET_SETTINGS_OBJECT_DETAIL_TABLE_METADATA_CUSTOM
|
|
: GET_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 allObjectSettingsDetailItems = useMemo(() => {
|
|
const nonSystemFields = settingsObjectFields?.filter(
|
|
(fieldMetadataItem) => !fieldMetadataItem.isSystem,
|
|
);
|
|
|
|
return (
|
|
nonSystemFields?.map(
|
|
mapFieldMetadataItemToSettingsObjectDetailTableItem,
|
|
) ?? []
|
|
);
|
|
}, [
|
|
settingsObjectFields,
|
|
mapFieldMetadataItemToSettingsObjectDetailTableItem,
|
|
]);
|
|
|
|
const sortedAllObjectSettingsDetailItems = useSortedArray(
|
|
allObjectSettingsDetailItems,
|
|
tableMetadata,
|
|
);
|
|
|
|
const filteredItems = useMemo(() => {
|
|
const searchNormalized = normalizeSearchText(searchTerm);
|
|
|
|
return sortedAllObjectSettingsDetailItems.filter((item) => {
|
|
const matchesActiveFilter =
|
|
showInactive || item.fieldMetadataItem.isActive;
|
|
|
|
const matchesSearch =
|
|
normalizeSearchText(item.label).includes(searchNormalized) ||
|
|
normalizeSearchText(item.dataType).includes(searchNormalized);
|
|
|
|
return matchesActiveFilter && matchesSearch;
|
|
});
|
|
}, [sortedAllObjectSettingsDetailItems, searchTerm, showInactive]);
|
|
|
|
return (
|
|
<>
|
|
<StyledSearchAndFilterContainer>
|
|
<StyledSearchInput
|
|
instanceId="object-field-table-search"
|
|
LeftIcon={IconSearch}
|
|
placeholder={t`Search a field...`}
|
|
value={searchTerm}
|
|
onChange={setSearchTerm}
|
|
/>
|
|
<Dropdown
|
|
dropdownId="settings-fields-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={() => setShowInactive(!showInactive)}
|
|
toggled={showInactive}
|
|
text={t`Inactive`}
|
|
toggleSize="small"
|
|
/>
|
|
</DropdownMenuItemsContainer>
|
|
</DropdownContent>
|
|
}
|
|
/>
|
|
</StyledSearchAndFilterContainer>
|
|
<Table>
|
|
<StyledObjectFieldTableRow>
|
|
{tableMetadata.fields.map((item) => (
|
|
<SortableTableHeader
|
|
key={item.fieldName}
|
|
fieldName={item.fieldName}
|
|
label={t(item.fieldLabel)}
|
|
tableId={tableMetadata.tableId}
|
|
initialSort={tableMetadata.initialSort}
|
|
/>
|
|
))}
|
|
<TableHeader></TableHeader>
|
|
</StyledObjectFieldTableRow>
|
|
{filteredItems.map((objectSettingsDetailItem) => {
|
|
const status = objectSettingsDetailItem.fieldMetadataItem.isActive
|
|
? 'active'
|
|
: 'disabled';
|
|
|
|
return (
|
|
<SettingsObjectFieldItemTableRow
|
|
key={objectSettingsDetailItem.fieldMetadataItem.id}
|
|
settingsObjectDetailTableItem={objectSettingsDetailItem}
|
|
status={status}
|
|
mode={mode}
|
|
/>
|
|
);
|
|
})}
|
|
</Table>
|
|
</>
|
|
);
|
|
};
|