Skip to content

feat: manage permission set assignments from the permissions manager - #1898

Merged
paustint merged 1 commit into
mainfrom
feat/permission-set-assignment
Aug 2, 2026
Merged

feat: manage permission set assignments from the permissions manager#1898
paustint merged 1 commit into
mainfrom
feat/permission-set-assignment

Conversation

@paustint

Copy link
Copy Markdown
Contributor

Users could edit object/field/tab/system permissions but had no way to control who a permission set was assigned to. The only assignment affordance was a read-only popover on the selection page.

Adds a "Manage Assignments" modal that assigns and unassigns users via PermissionSetAssignment records, reachable from two places:

  • the existing popover on the selection page list
  • the permissions editor table, via a new popover trigger in the column group header that spans each profile / permission set

Permission sets only. Profiles stay read-only — assigning a profile means mutating User.ProfileId, which is out of scope and far more destructive. Gated three ways: the popover's canManageAssignments, the header's allowManageAssignments, and the modal's own IsOwnedByProfile check.

The modal stages changes locally (left panel unassigns, right panel is a type-ahead over User that never loads the full org roster) and applies the diff on save — deletes before inserts so a remove-then-re-add can't hit DUPLICATE_VALUE, chunked at 200 with allOrNone: false. Partial failures keep the modal open with the Salesforce message inline and reload from the server. Nothing on the editor page refetches; only the popover's own list refreshes.

Copilot AI review requested due to automatic review settings July 31, 2026 00:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end support for managing Permission Set assignments (who a permission set is assigned to) directly from the permissions manager UI, without needing to jump to Salesforce Setup.

Changes:

  • Introduces a new Manage Assignments modal that stages add/remove changes locally and applies them via PermissionSetAssignment create/delete (chunked, allOrNone: false, partial failure handling).
  • Extends the existing Profile/Permission Set popover to optionally expose a “Manage Assignments” action for standalone Permission Sets (not profile-owned).
  • Adds a column-group header popover trigger in the permissions editor table so assignments can be managed from within the editor experience.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
libs/ui/src/lib/widgets/ProfileOrPermSetPopover.tsx Adds optional “Manage Assignments” footer action and opens the new modal.
libs/ui/src/lib/widgets/permission-set-assignment-utils.ts New shared SOQL builders + ID guard/escaping utilities to avoid cycles.
libs/ui/src/lib/widgets/ManagePermissionSetAssignmentsModal.tsx New modal UI and save logic for PermissionSetAssignment diffs (delete then create).
libs/ui/src/lib/widgets/tests/permission-set-assignment-utils.spec.ts Unit tests for SOQL builders and escaping.
libs/ui/src/lib/widgets/tests/ManagePermissionSetAssignmentsModal.spec.tsx Component tests for staging/saving behavior and failure handling.
libs/ui/src/index.ts Exports the new modal and shared utilities from @jetstream/ui.
libs/types/src/lib/salesforce/record.types.ts Adds a PermissionSetAssignmentRecord type used by the modal’s query.
libs/shared/constants/src/lib/shared-constants.ts Adds Amplitude analytics keys for opened/saved assignment flows.
libs/features/manage-permissions/src/utils/permission-manager-table-utils.tsx Adds a group header renderer (non-sortable) for the first column of each profile/permset group.
libs/features/manage-permissions/src/utils/tests/permission-manager-columns.spec.ts Tests for the group-header column behavior (renderHeaderCell/sortable/filters/name).
libs/features/manage-permissions/src/PermissionColumnGroupHeader.tsx New header component hosting the popover trigger and wiring org/state + analytics.
libs/features/manage-permissions/src/ManagePermissionsSelection.tsx Enables Manage Assignments action from the existing selection-page popover + wires analytics.

Comment thread libs/ui/src/lib/widgets/ManagePermissionSetAssignmentsModal.tsx
Copilot AI review requested due to automatic review settings July 31, 2026 02:19
@paustint
paustint force-pushed the feat/permission-set-assignment branch from 16fb4a9 to 65dcec9 Compare July 31, 2026 02:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

libs/ui/src/lib/widgets/ProfileOrPermSetPopover.tsx:210

  • canManageAssignments currently allows opening the assignments modal when meta is missing/undefined because it checks meta?.IsOwnedByProfile !== true. That bypasses the intended "IsOwnedByProfile" gate (and could allow attempts to assign profile-owned permission sets, depending on call site). Make the guard require an explicit false value.
  // Profile-owned permission sets are granted through the profile, so they are never directly assignable.
  const canManageAssignments =
    allowManageAssignments && recordType === 'PermissionSet' && meta?.IsOwnedByProfile !== true && ID_ALLOWED.test(effectiveRecordId);

libs/types/src/lib/salesforce/record.types.ts:197

  • PermissionSetAssignmentRecord.Assignee is typed as always-present, but relationship sub-objects are often omitted (or can be null) depending on the SOQL SELECT and field-level access. The modal code already treats Assignee as optional via optional chaining, so the type should reflect that to prevent unsafe assumptions in other callers.
  Id: string;
  PermissionSetId: string;
  AssigneeId: string;
  Assignee: { Id: string; Name: string; Username: string; IsActive: boolean };
}

@paustint
paustint force-pushed the feat/permission-set-assignment branch from 65dcec9 to 1c07202 Compare July 31, 2026 15:16
Copilot AI review requested due to automatic review settings July 31, 2026 15:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (2)

libs/ui/src/lib/widgets/permission-set-assignment-utils.ts:8

  • ID_ALLOWED currently allows 16- and 17-character strings ({15,18}), but the comment (and Salesforce ID format) is specifically 15 or 18 characters. This can cause invalid IDs to be treated as safe/valid and end up in SOQL builders (and in getUserSearchQuery it can also incorrectly switch into the “ID” search path). Tighten the regex to only match 15 or 18 characters.
/** Salesforce ids are 15 or 18 alphanumeric characters. Anything else must never reach a SOQL string. */
export const ID_ALLOWED = /^[a-zA-Z0-9]{15,18}$/;

libs/features/manage-permissions/src/utils/permission-manager-table-utils.tsx:648

  • Importing PermissionColumnGroupHeader into permission-manager-table-utils makes any unit test that imports the table utils also evaluate PermissionColumnGroupHeader’s module graph. That header imports @jetstream/ui/app-state, which initializes userProfileState by calling fetchUserProfile() at module-evaluation time (see libs/shared/ui-app-state/src/lib/ui-app-state.ts:236) — and other tests already have to mock @jetstream/ui/app-state to avoid unhandled rejections in jsdom (see libs/ui/src/lib/popover/tests/Popover.spec.tsx:1-8). As a result, the existing manage-permissions util tests may start failing unless this project’s Vitest setup mocks @jetstream/ui/app-state (with the named exports that PermissionColumnGroupHeader imports) or the header import is otherwise isolated from test-only code paths.
    // Only the first column of each group renders a header cell — the rest are covered by its colSpan.
    // It must be non-sortable because a sortable header wraps its label in a <button>, and the group header
    // hosts a popover trigger (also a button). Nothing is lost: sorting reads row["<id>-<action>"], which
    // never exists on these rows, so it was already a no-op. Filtering uses `getValue` and is unaffected.
    ...(isFirstItem
      ? {
          sortable: false,
          renderHeaderCell: () => <PermissionColumnGroupHeader id={id} label={label} type={type} />,
        }

Copilot AI review requested due to automatic review settings August 1, 2026 22:11
@paustint
paustint force-pushed the feat/permission-set-assignment branch from 1c07202 to f6968fe Compare August 1, 2026 22:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Users could edit object/field/tab/system permissions but had no way to
control who a permission set was assigned to. The only assignment
affordance was a read-only popover on the selection page.

Adds a "Manage Assignments" modal that assigns and unassigns users via
PermissionSetAssignment records, reachable from two places:

- the existing popover on the selection page list
- the permissions editor table, via a new popover trigger in the column
  group header that spans each profile / permission set

Permission sets only. Profiles stay read-only — assigning a profile means
mutating User.ProfileId, which is out of scope and far more destructive.
Gated three ways: the popover's canManageAssignments, the header's
allowManageAssignments, and the modal's own IsOwnedByProfile check.

The modal stages changes locally (left panel unassigns, right panel is a
type-ahead over User that never loads the full org roster) and applies the
diff on save — deletes before inserts so a remove-then-re-add can't hit
DUPLICATE_VALUE, chunked at 200 with allOrNone: false. Partial failures
keep the modal open with the Salesforce message inline and reload from the
server. Nothing on the editor page refetches; only the popover's own list
refreshes.
Copilot AI review requested due to automatic review settings August 2, 2026 18:23
@paustint
paustint force-pushed the feat/permission-set-assignment branch from f6968fe to ba4833f Compare August 2, 2026 18:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Suppressed comments (1)

libs/ui/src/lib/widgets/ManagePermissionSetAssignmentsModal.tsx:126

  • isAssignable currently treats isOwnedByProfile: undefined as assignable (!undefined === true). That means a caller who omits isOwnedByProfile can manage assignments even though the prop docs (and PR description) frame the modal as self-gating on the profile-owned check. Consider requiring an explicit isOwnedByProfile === false (or making the prop required) so “unknown ownership” fails closed, matching the popover’s explicit meta?.IsOwnedByProfile === false gating.
  const [saving, setSaving] = useState(false);
  const [saveErrorMessage, setSaveErrorMessage] = useState<string | null>(null);
  const [saveFailures, setSaveFailures] = useState<SaveFailure[]>([]);

  const isAssignable = !isOwnedByProfile && ID_ALLOWED.test(permissionSetId);

@paustint
paustint merged commit 1e2f339 into main Aug 2, 2026
9 checks passed
@paustint
paustint deleted the feat/permission-set-assignment branch August 2, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants