Introduce updateWorkspaceMemberSettings and clarify product (#19441)

## 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.
This commit is contained in:
Marie
2026-04-14 18:29:00 +02:00
committed by GitHub
parent 42f452311b
commit bc28e1557c
58 changed files with 1986 additions and 478 deletions
@@ -5,11 +5,13 @@ import { useCallback, useState } from 'react';
import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatus';
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
import {
@@ -19,9 +21,9 @@ import {
} from '@/object-record/record-field/ui/contexts/FieldContext';
import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { useIsRecordReadOnly } from '@/object-record/read-only/hooks/useIsRecordReadOnly';
import { isRecordFieldReadOnly } from '@/object-record/read-only/utils/isRecordFieldReadOnly';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { getRecordFieldInputInstanceId } from '@/object-record/utils/getRecordFieldInputId';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
AvatarOrIcon,
@@ -32,7 +34,6 @@ import {
} from 'twenty-ui/components';
import { IconCalendarEvent } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
type CalendarEventDetailsProps = {
@@ -70,7 +71,6 @@ const StyledTitle = styled.h2<{ canceled?: boolean }>`
font-weight: ${themeCssVariables.font.weight.semiBold};
margin: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[0]}
${themeCssVariables.spacing[2]};
text-decoration: ${({ canceled }) => (canceled ? 'line-through' : 'none')};
`;
@@ -149,12 +149,21 @@ export const CalendarEventDetails = ({
const objectPermissions = useObjectPermissionsForObject(
objectMetadataItem.id,
);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const isRecordReadOnly = useIsRecordReadOnly({
recordId: calendarEvent.id,
objectMetadataId: objectMetadataItem.id,
});
const renderField = (fieldMetadataItem: FieldMetadataItem) => {
const fieldDefinition = formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem,
showLabel: true,
labelWidth: 72,
});
const isReadOnly = isRecordFieldReadOnly({
isRecordReadOnly,
isSystemObject: objectMetadataItem.isSystem,
@@ -164,6 +173,8 @@ export const CalendarEventDetails = ({
isUIReadOnly: fieldMetadataItem.isUIReadOnly ?? false,
isCustom: fieldMetadataItem.isCustom ?? false,
},
fieldDefinition,
objectPermissionsByObjectMetadataId,
});
return (
@@ -173,12 +184,7 @@ export const CalendarEventDetails = ({
value={{
recordId: calendarEvent.id,
isLabelIdentifier: false,
fieldDefinition: formatFieldMetadataItemAsFieldDefinition({
field: fieldMetadataItem,
objectMetadataItem,
showLabel: true,
labelWidth: 72,
}),
fieldDefinition,
useUpdateRecord: useUpdateOneCalendarEventRecordMutation,
maxWidth: 300,
isRecordFieldReadOnly: isReadOnly,