bc28e1557c
## Summary Introduces a dedicated **metadata** mutation to update **standard (non-custom)** workspace member settings, moves profile-related UI to use it, and aligns **workspace member** record permissions with the rest of the CRM so users cannot escalate visibility via RLS by editing their own member record. ## Product behaviour ### Profile and appearance (standard fields) - Users can still update **their own** standard workspace member fields that the product exposes in **Settings / Profile** (e.g. name, locale, color scheme, avatar flow) via the new **`updateWorkspaceMemberSettings`** mutation. - The mutation returns a **boolean**; the app **merges** the updated fields into local state so the UI stays in sync without refetching the full workspace member record. - **Locale** changes also keep **`userWorkspace`** in sync when a locale is present in the payload (including from the workspace `updateOne` path when applicable). ### Custom fields on workspace members - The dedicated metadata mutation **rejects** any **custom** workspace member field (and unknown keys). Those updates must go through the normal **object** `updateOne` pipeline, which is subject to **object- and field-level** permissions like other records. But since we don't have object- and field-level permission configuration for system objects yet, this permission is derived from Workspace member settings permission. - **Workspace member** is no longer exempt from ORM permission validation for updates merely because it is a **system** object. Users who **do not** have workspace member access (e.g. no **Workspace members** settings permission and no equivalent broad settings access on the role) **cannot** use `updateOne` on `workspaceMember` to change **custom** (or other) fields on their own row—even though that row is used for RLS predicates. - This closes a path where someone could widen what they can see by writing to fields that drive row-level rules. ### Who can change another member - Updating **another** user’s workspace member still requires **Workspace members** (or equivalent) settings permission, consistent with admin tooling.
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
import { useFieldMetadataItemById } from '@/object-metadata/hooks/useFieldMetadataItemById';
|
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
|
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
|
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
|
|
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
|
|
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
|
import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId';
|
|
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
|
import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
|
|
import { type WidgetAction } from '@/page-layout/widgets/types/WidgetAction';
|
|
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
|
|
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { RelationType } from '~/generated-metadata/graphql';
|
|
|
|
type UseWidgetActionsParams = {
|
|
widget: PageLayoutWidget;
|
|
};
|
|
|
|
export const useWidgetActions = ({
|
|
widget,
|
|
}: UseWidgetActionsParams): WidgetAction[] => {
|
|
const targetRecord = useTargetRecord();
|
|
|
|
const { objectMetadataItem } = useObjectMetadataItem({
|
|
objectNameSingular: targetRecord.targetObjectNameSingular,
|
|
});
|
|
|
|
const fieldMetadataId = isFieldWidget(widget)
|
|
? widget.configuration.fieldMetadataId
|
|
: undefined;
|
|
|
|
const resolvedFieldMetadataId = useResolveFieldMetadataIdFromNameOrId(
|
|
fieldMetadataId ?? '',
|
|
);
|
|
|
|
const { fieldMetadataItem } = useFieldMetadataItemById(
|
|
resolvedFieldMetadataId ?? '',
|
|
);
|
|
|
|
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
|
|
|
const isRecordReadOnly = useIsRecordReadOnly({
|
|
recordId: targetRecord.id,
|
|
objectMetadataId: objectMetadataItem.id,
|
|
});
|
|
|
|
const actions: WidgetAction[] = [];
|
|
|
|
if (
|
|
!isFieldWidget(widget) ||
|
|
!isDefined(fieldMetadataItem) ||
|
|
!fieldMetadataItem.isActive
|
|
) {
|
|
return actions;
|
|
}
|
|
|
|
const fieldDefinition = formatFieldMetadataItemAsColumnDefinition({
|
|
field: fieldMetadataItem,
|
|
position: 0,
|
|
objectMetadataItem,
|
|
showLabel: true,
|
|
labelWidth: 90,
|
|
});
|
|
|
|
const isOneToManyRelation =
|
|
isFieldRelation(fieldDefinition) &&
|
|
fieldDefinition.metadata.relationType === RelationType.ONE_TO_MANY;
|
|
|
|
if (isOneToManyRelation) {
|
|
actions.push({
|
|
id: 'see-all',
|
|
position: 0,
|
|
});
|
|
}
|
|
|
|
const isFieldReadOnly = isRecordFieldReadOnly({
|
|
isRecordReadOnly,
|
|
isSystemObject: objectMetadataItem.isSystem,
|
|
objectPermissions: getObjectPermissionsFromMapByObjectMetadataId({
|
|
objectPermissionsByObjectMetadataId,
|
|
objectMetadataId: objectMetadataItem.id,
|
|
}),
|
|
fieldMetadataItem: {
|
|
id: fieldMetadataItem.id,
|
|
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
|
|
isCustom: fieldMetadataItem.isCustom ?? false,
|
|
},
|
|
fieldDefinition,
|
|
objectPermissionsByObjectMetadataId,
|
|
});
|
|
|
|
if (!isFieldReadOnly) {
|
|
actions.push({
|
|
id: 'edit',
|
|
position: 1,
|
|
});
|
|
}
|
|
|
|
return actions.sort((a, b) => a.position - b.position);
|
|
};
|