Skip to content

feat: Workspaces#5641

Open
kof wants to merge 65 commits intomainfrom
feat/workspaces
Open

feat: Workspaces#5641
kof wants to merge 65 commits intomainfrom
feat/workspaces

Conversation

@kof
Copy link
Member

@kof kof commented Mar 12, 2026

No description provided.

@kof kof force-pushed the feat/workspaces branch from f33d72c to 4438b76 Compare March 12, 2026 19:41
kof added 26 commits March 12, 2026 19:50
…d Workspace model (id, name, isDefault, createdAt, userId)\n- Add WorkspaceMember model (workspaceId, userId, relation) with CASCADE delete\n- Add optional workspaceId FK on Project (ON DELETE SET NULL)\n- Drop unused Team model and User.teamId column\n- Recreate DashboardProject view with workspaceId column\n- Recreate latestBuildVirtual wrapper using LATERAL join\n- Remove teamId from dashboard storybook mock\n- Partial unique index enforces one default workspace per user
- Create default "Personal" workspace at user signup in genericCreateAccount
- Add data migration to backfill existing users with default workspaces
- Assign unassigned projects to their owner's default workspace
- Add Workspace type export from @webstudio-is/project
- Regenerate PostgREST types with Workspace/WorkspaceMember tables
- Add workspaceId to storybook/test mock data
- Add @webstudio-is/postgrest dependency to project package
Add create, rename, remove, and findMany functions for workspaces.
- create: non-default workspace with name validation (min 2 chars)
- rename: owner-only, with name validation
- remove: owner-only, rejects default workspace and non-empty workspaces
- findMany: returns owned + member-of workspaces, default first

Wire workspace tRPC router into the app router with matching
procedures: workspace.create, workspace.rename, workspace.delete,
workspace.list.
Projects are created inside a workspace and listed per workspace.

- project.create accepts optional workspaceId, sets it on the new project
- dashboard findMany accepts optional workspaceId filter
- Dashboard route loader loads workspaces list and reads selected
  workspace from cookie when the workspaces flag is on
- DashboardData type extended with workspaces and currentWorkspaceId

When the flag is off, all behavior is unchanged.
…selection

Replace cookie-based workspace selection with ?workspaceId= URL param.
No param means the default workspace. Simpler, shareable, no cookie
infrastructure needed.
Add/remove/list workspace members by email.

- addMember: owner-only, looks up user by email, inserts as
  administrator, rejects duplicates and self-add
- removeMember: owner-only, prevents removing the owner
- listMembers: visible to owner and members, returns user email/username

tRPC procedures: workspace.addMember, workspace.removeMember,
workspace.listMembers.
…kspaceProjectAuthorization SQL view (workspace owner = own, members = their relation)\n- Update authorization check() to query workspace view when flag is on\n- Dashboard findMany filters by workspaceId instead of userId for workspace members\n- Builder resolves plan features from workspace owner when project belongs to a workspace\n- Add @webstudio-is/feature-flags dependency to trpc-interface and dashboard packages
…ector component with design system Select\n- Wire workspace selector into dashboard sidebar (feature-flagged)\n- Update shouldRevalidate to reload data on workspace switch\n- Pass currentWorkspaceId through to CreateProject so new projects\n are created in the currently viewed workspace\n- Rename sidebar section from \"Workspace\" to \"Navigation\" when\n workspace selector is visible
…n\n\n- Add color='ghost' variant to SelectButton (transparent bg, hover highlight)\n- Pass color prop through Select to SelectButton\n- Rewrite workspace selector to use Select with color='ghost'\n- Render workspace selector directly in sidebar nav (no CollapsibleSection)
- Add workspace dialogs: create, rename, manage members, delete
- Rewrite workspace selector as dropdown menu with action items
- Support comma-separated multi-email member invites
- Create placeholder users for non-existing email invites
- Add ScrollAreaNative component with native thin scrollbar
- Show newest members first in member list
- Preserve workspaceId query param in navigation links
- Fix DashboardSetup to use useEffect for data sync
- Pass userId to WorkspaceSelector for ownership checks
- Disable Rename/Delete menu items for non-owners
- Hide invite input and remove buttons for non-owners in ManageMembersDialog
- Add empty state for member list
- Add Avatar with small size variant (data-size attribute) to workspace selector
- Use userId prop instead of isOwner boolean for future role extensibility
- Backend: accept deleteProjects flag to soft-delete all projects before removing workspace
- Frontend: warning text mentions all projects will be deleted, with 'All projects' in red
- Dashboard: detect stale workspaceId in URL and redirect to default workspace
- Extract softDeleteProject helper so unique domain invariant lives in one place
- Projects created in a workspace are owned by workspace owner, not creator
- Remove unnecessary useEffect around $data.set
- Remove unused parseTrpcError helper
- Remove unsafe `as TokenAuthPermit` cast
- Add sortWorkspaces tests
- Document idempotent retry safety in workspace remove()
…ck when creating projects in a workspace\n- Remove placeholder user creation on invite (prevent DB pollution)\n- Show invited emails client-side to prevent email enumeration\n- Show only email in member rows (no username leak)\n- Throw on workspace plan resolution error instead of silent fallback\n- DRY navigation items in dashboard sidebar\n- Replace null with empty string in listMembers return\n- Restore useEffect in DashboardSetup to fix React render warning
- Add unified getPermissions() combining role + plan permissions
- Gate project actions (create, delete, publish, share, etc.) by workspace role
- Introduce MemberRelation schema (owners/admins/builders/viewers)
- Compute $permissions from $userPlanFeatures + $authPermit in nano-states
- Set $userPlanFeatures globally in DashboardSetup (matching builder pattern)
- Add workspace dialogs for managing members and invitations
- Enforce permissions across dashboard, builder, and settings UI
…ns prop-drilling, unify shared stores

- Rename userRelation → workspaceRelation across schema, DB, router, and UI
- Rename MemberRelation type → WorkspaceRelation everywhere
- Create canonical UserPlanFeatures/UserPurchase module in trpc-interface
- Make userPlanFeatures non-optional in AppContext
- Separate purchases from UserPlanFeatures
- Compute workspaceRelation in route loaders instead of consumer sites
- Add $workspaceRelation atom, use real relation in $permissions computed
- Remove permissions prop-drilling from all dashboard components
- Leaf components use useStore($permissions) directly
- Create setSharedStores() for unified store initialization
- Combine workspace + membership into single PostgREST query in builder route
- Replace purchases.length checks with canDownloadAssets/canRestoreBackups
- getUserPlanFeatures() → getUserPlanInfo() returning { userPlanFeatures, purchases }
- Rename dialog title and dropdown item from "Manage members" to "Members"
- Remove icons from workspace selector dropdown menu items
- Remove separator before Delete item
- Set withIndicator={false} to fix indentation without icons
# Conflicts:
#	apps/builder/app/builder/features/menu/menu.tsx
#	apps/builder/app/dashboard/dashboard.stories.tsx
- Show user name next to profile avatar in dashboard sidebar
- Separate welcome view from templates into its own component
- Welcome subtitle with create blank project button (permission-gated)
- Allow PROJECT_TEMPLATES to skip marketplace approval check
- Templates view shows clean "Starter templates" title
- Replace workspaces feature flag with maxWorkspaces-based gating
- Default maxWorkspaces to 0 (controlled via MAX_WORKSPACES env var)
- Skip workspace queries in dashboard loader when maxWorkspaces is 0
- Extract pure functions (resolveCurrentWorkspace, isWorkspaceRelationPermitted, getView) with tests
- Remove workspaces feature flag from all files
kof added 15 commits March 17, 2026 14:51
…space-dropdown.tsx with reusable dropdown (trigger + radio groups + children slot)\n- Refactor workspace-selector to use WorkspaceDropdown with color=ghost and action items as children\n- Refactor transfer-dialog to use WorkspaceDropdown without color prop and no action items\n- Add OR separator between workspace and recipient sections
…r/move guards

- notification.create returns notification ID
- addMember returns notificationId (fake UUID for non-existent users)
- workspace router exposes notificationId in addMember response
- workspace-dialogs: trash button calls notification.cancel with stored ID
- replace $userEmail atom with $user storing full User object
- add maxProjectsAllowedPerUser to UserPlanFeatures + all plan constructors
- plan-limit check on transfer accept
- global dedup: one pending transfer per project
- notification.cancel endpoint
- cross-owner move restriction in moveProject
- self-transfer check in transfer dialog
Add a universal topic-based polling system (polly) that replaces the
old notification-store with a more extensible architecture:

- Backoff, polling-client, polling-manager with full test coverage (86 tests)
- Server-side topic resolvers + tRPC subscription router
- Dashboard subscription wiring with loader-seeded notifications
- In-app toast as primary alert, browser Notification as supplement
- Notification popover reads from shared nanostore atom
- project-queue.ts uses polly backoff, tests migrated to vi.mock()
Only one browser tab actively polls the server. Other tabs receive
data via BroadcastChannel, eliminating duplicate network requests.

- Leader election via heartbeat protocol (lowest tab-id wins)
- Instant failover on leader tab close (abdication broadcast)
- Follower topic sync so leader tracks all tabs' subscriptions
- Drop-in replacement for createPollingManager (same interface)
- Dashboard subscription now uses createCrossTabPollingManager
- 10 new tests (leader election, failover, topic sync, destroy)
- Future topic ideas documented in types.ts registry comment
Move description generation to the server (notification.list) so the
popover renders a pre-built string. Adding a new notification type
now only requires updating describeNotification on the server — the
UI stays generic.

- Remove discriminated union types and WorkspaceInvitePayload import
  from notification-popover.tsx
- NotificationItem is now derived from the server return type
- Move getDescription tests → describeNotification tests
- Remove unused __testing__ export from cross-tab-manager.ts
The story data object was missing the `notifications` field added to
DashboardData, causing seedNotifications to call .filter() on undefined.
…n\n- Delete unused OwnedWorkspaces story from transfer-dialog.stories\n- Add missing projectTitle/workspaceName to notification story items\n- Fix JsonifyObject incompatibility for notifications in dashboard route
@kof kof force-pushed the feat/workspaces branch from b390214 to b7b5da7 Compare March 19, 2026 14:16
kof added 14 commits March 19, 2026 14:22
Changed builder auth check from 'own' to 'edit' permit so workspace
administrators, builders, and editors can access projects.
moveProject previously allowed silently reassigning project ownership
when moving to another user's workspace (e.g. as an admin). This meant
the workspace owner never got a transfer notification — the project
just appeared.

Now moveProject rejects cross-owner moves, forcing the transfer flow
(transferProject) which always creates a notification the recipient
must accept. The transfer dialog no longer shows shared workspaces
in move mode — only own workspaces are listed without an email.
Added a projectCount polling topic so the sender's dashboard
revalidates when a transfer they initiated is accepted. The polled
count changes as soon as the project ownership flips, triggering
a revalidation that removes the transferred project from view.
Changed builder access check from 'edit' to 'view' permit so workspace
viewers can also open projects. Per-action permissions are still
enforced inside the builder.
- Add Animate.css swing keyframes to bell icon when notifications are pending
- Stop animation permanently once the popover has been opened
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.

1 participant