mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-08 16:17:58 +00:00
Promote / demote workspace members ↔ owners in Settings ▸ Members, per [DES-222 / Figma 2993-15512](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=2993-15512) and the [permissions section 3343-22966](https://www.figma.com/design/CkFTD4c20PyRGpNVAJgpfV/Team-Plan---Workspaces?node-id=3343-22966). - Fixes [FE-770](https://linear.app/comfyorg/issue/FE-770/promote-demote-workspace-members-owners-settings-members) - Stacked on #12759 (`jaewon/fe-768-members-invite-ui`) ## Changes - Per-member row (…) menu → **Change role** submenu (Owner / Member, current role check-marked) + existing **Remove member**, replacing the shared PrimeVue `Menu` with the Reka `DropdownMenu`/`DropdownItem` (submenu opens right of parent, flips on collision; scalable for future roles). - **Make [name] an owner?** / **Demote [name] to member?** confirm dialogs (single `ChangeMemberRoleDialogContent`, copy 1:1 from Figma). - `workspaceApi.updateMemberRole` → `PATCH /api/workspace/members/:userId {role}` + `teamWorkspaceStore.changeMemberRole` (local role map update; Role column re-sorts). - **Original-owner guards** (Figma annotations): creator pinned to the top of the list, no row actions for anyone on that row; own row also has no actions. Creator inferred as earliest `joined_at` until BE exposes an explicit flag (tracked as the FE-770 BE blocker — same applies to the endpoint itself, which does not exist yet; UI is wired to the proposed contract). - `DropdownMenu` raised to `z-3000` so the row menu sits above the Settings modal (the Reka popper wrapper copies the content's computed z-index; static `z-1700` lost to dialogs in the `@primeuix` modal sequence). Also drops the always-rendered icon slot in `DropdownItem` so icon-less items (Change role / Remove member) align flush-left. ## User stories verified Viewer = an **owner** (promoted, not the workspace creator), so the creator guard and the self guard are exercised separately. | # | Click → action → expected | | --- | --- | | US1 | Member row (…) → menu shows **Change role ›** + **Remove member** | | US2 | Hover **Change role** → Owner / Member submenu, **current role check-marked** | | US3 | Click the current role (✓) → no dialog, no PATCH (no-op) | | US4 | Member row → **Owner** → "Make {name} an owner?" + "They'll have the same access as you — managing members, billing, and workspace settings." + Cancel / **Make owner** | | US5 | **Cancel** (or ✕) → dialog closes, role unchanged, no PATCH | | US6 | **Make owner** → `PATCH /api/workspace/members/:id {role:'owner'}` → Role column → Owner, row **re-sorts under the creator**, "Role updated" toast, the promoted row keeps its (…) menu | | US7 | Promoted owner row → **Member** → "Demote {name} to member?" + "They'll lose admin access." → **Demote to member** → Role column back to Member | | US8 | **Creator row (earliest joined) has no (…) button** — even for another owner | | US9 | **Own (You) row has no (…) button** — even when not the creator | | US10 | PATCH 500 → "Failed to update role" toast, **dialog stays open**, role unchanged | | US11 | Viewer with `member` role → no row actions anywhere | | US12 | **Remove member** → existing FE-768 "Remove this member?" dialog | ## Tests Each user story is covered by automated tests and confirmed by a manual CDP pass driving the real cloud app (mocked auth + boot + workspace/billing API). | Story | Unit / Component | E2E (Playwright) | CDP (live app) | | --- | :---: | :---: | :---: | | US1 row menu shows Change role + Remove member | ✅ | ✅ | ✅ | | US2 submenu checkmark follows current role | ✅ | ✅ | ✅ | | US3 picking the current role is a no-op | ✅ | ✅ | ✅ | | US4 promote dialog copy (Make owner) | ✅ | ✅ | ✅ | | US5 Cancel leaves role unchanged, no PATCH | ✅ | ✅ | ✅ | | US6 Make owner → PATCH, re-sort under creator, toast, stays demotable | ✅ | ✅ | ✅ | | US7 demote dialog (Demote to member) → role reverts | ✅ | ✅ | ✅ | | US8 creator row has no (…) menu | ✅ | ✅ | ✅ | | US9 own (You) row has no (…) menu | ✅ | ✅ | ✅ | | US10 PATCH 500 → error toast, dialog stays open | ✅ | ✅ | ✅ | | US11 member-role viewer sees no row actions | ✅ | — | — | | US12 Remove member → FE-768 remove dialog | ✅ | ✅ | ✅ | | Layer | File | What it covers | Result | | --- | --- | --- | --- | | E2E (`@cloud`) | `browser_tests/tests/dialogs/memberRoleChange.spec.ts` | 3 tests — guard rows (US1/US8/US9/US12), promote→re-sort→demote round trip (US3–US7), failed PATCH (US10). FE-964 boot pattern: `CloudAuthHelper` + remote-config flag mock + stateful route mocks capturing PATCH args. Reka submenu driven via `ArrowRight` (synthetic hover doesn't open it). | 3 / 3 green | | Component | `ChangeMemberRoleDialogContent.test.ts` | promote/demote copy, confirm → store + success toast + close, error keeps dialog open, cancel | green | | Component | `MembersPanelContent.test.ts` | creator/self rows hide the menu (US8/US9), member-viewer gating (US11) | green | | Composable | `useMembersPanel.test.ts` | menu factory labels/checkmarks/commands, same-role no-op, creator pin in `sortMembers`, `isOriginalOwner` | green | | Store | `teamWorkspaceStore.test.ts` | `changeMemberRole` success/failure, `originalOwnerId` inference | green | | CDP live | full cloud app on `local.comfy.org` (mocked auth + boot) | promote→re-sort→demote round trip with PATCH applied to mock state, guard rows, submenu checkmark, dialog copy, menu/dialog z-index above Settings, forced PATCH 500 → error toast | verified | ⚠️ Merge-gated on the BE role-change endpoint (no `PATCH /workspace/members/:userId` in cloud OpenAPI as of 2026-06-10; see FE-770 BE-blocker comment). ## Screenshots (local dev, workspace/billing API stubbed; vs Figma 2993-15512) | Members (before) | Change role submenu | | --- | --- | | <img alt="members" src="https://github.com/user-attachments/assets/686fec86-fcb5-4942-a745-50f367022ab0" /> | <img alt="submenu" src="https://github.com/user-attachments/assets/d6adeea8-7001-4c8d-91b7-f5bfc47a50d6" /> | | Promote dialog | After promote (Jane → Owner, still demotable) | Demote dialog | | --- | --- | --- | | <img alt="promote" src="https://github.com/user-attachments/assets/af638cde-2fd6-4c37-b203-78801eeb2785" /> | <img alt="after" src="https://github.com/user-attachments/assets/f47dc7af-6b1b-422c-8a9a-5ec889b9af11" /> | <img alt="demote" src="https://github.com/user-attachments/assets/9a861d04-a23b-4cd4-bc54-1ed3a66c6429" /> |
97 lines
2.2 KiB
TypeScript
97 lines
2.2 KiB
TypeScript
import type {
|
|
BillingStatusResponse,
|
|
Member,
|
|
Plan,
|
|
WorkspaceWithRole
|
|
} from '@/platform/workspace/api/workspaceApi'
|
|
import type { RemoteConfig } from '@/platform/remoteConfig/types'
|
|
|
|
// `/api/features` is the remote-config source: production builds resolve the
|
|
// workspaces flag from it (the `ff:` localStorage override is dev-only).
|
|
export const WORKSPACE_FEATURE_FLAG: RemoteConfig = {
|
|
team_workspaces_enabled: true
|
|
}
|
|
|
|
export const TEAM_WORKSPACE: WorkspaceWithRole = {
|
|
id: 'ws-team',
|
|
name: 'Team Comfy',
|
|
type: 'team',
|
|
created_at: '2025-01-01T00:00:00Z',
|
|
joined_at: '2025-01-02T00:00:00Z',
|
|
role: 'owner',
|
|
subscription_tier: 'PRO'
|
|
}
|
|
|
|
export const CREATOR: Member = {
|
|
id: 'u-liz',
|
|
name: 'Liz',
|
|
email: 'liz@test.comfy.org',
|
|
joined_at: '2025-01-01T00:00:00Z',
|
|
role: 'owner',
|
|
is_original_owner: true
|
|
}
|
|
|
|
// Identity must match the CloudAuthHelper mock user so this row counts as
|
|
// "(You)".
|
|
export const VIEWER: Member = {
|
|
id: 'u-me',
|
|
name: 'E2E Test User',
|
|
email: 'e2e@test.comfy.org',
|
|
joined_at: '2025-01-02T00:00:00Z',
|
|
role: 'owner',
|
|
is_original_owner: false
|
|
}
|
|
|
|
export const MEMBER_JANE: Member = {
|
|
id: 'u-jane',
|
|
name: 'Jane',
|
|
email: 'jane@test.comfy.org',
|
|
joined_at: '2025-01-03T00:00:00Z',
|
|
role: 'member',
|
|
is_original_owner: false
|
|
}
|
|
|
|
export const MEMBER_JOHN: Member = {
|
|
id: 'u-john',
|
|
name: 'John',
|
|
email: 'john@test.comfy.org',
|
|
joined_at: '2025-01-04T00:00:00Z',
|
|
role: 'member',
|
|
is_original_owner: false
|
|
}
|
|
|
|
export const DEFAULT_TEAM_MEMBERS: Member[] = [
|
|
CREATOR,
|
|
VIEWER,
|
|
MEMBER_JANE,
|
|
MEMBER_JOHN
|
|
]
|
|
|
|
export const TEAM_BILLING_STATUS: BillingStatusResponse = {
|
|
is_active: true,
|
|
subscription_status: 'active',
|
|
subscription_tier: 'PRO',
|
|
subscription_duration: 'MONTHLY',
|
|
plan_slug: 'pro-monthly',
|
|
billing_status: 'paid',
|
|
has_funds: true,
|
|
renewal_date: '2099-02-20T00:00:00Z'
|
|
}
|
|
|
|
// `max_seats > 1` on the current plan is what flips `isOnTeamPlan`, which gates
|
|
// the whole role-management UI.
|
|
export const TEAM_PRO_PLAN: Plan = {
|
|
slug: 'pro-monthly',
|
|
tier: 'PRO',
|
|
duration: 'MONTHLY',
|
|
price_cents: 10000,
|
|
credits_cents: 21100,
|
|
max_seats: 30,
|
|
availability: { available: true },
|
|
seat_summary: {
|
|
seat_count: 4,
|
|
total_cost_cents: 40000,
|
|
total_credits_cents: 0
|
|
}
|
|
}
|