feat(workspace): auto-add creator to workspace_members + polished first-run UI #23
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_collab!23
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/workspace-creator-membership-and-first-run-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #22.
Summary
Two paired fixes for the operator's first-run experience after a fresh deployment:
workspace.createnow auto-adds the creator toworkspace_membersso per-workspace operations (canvas.create, etc.) actually work for the workspace creator.pickUserwith a polished first-run setup modal asking the operator to name their workspace + first channel.Coupled because either alone leaves the first-run path broken — the modal needs the membership row to work, and the membership fix without the modal still ends up with a workspace named "General" that the operator didn't choose.
Changes
crates/hero_collab_server/src/handlers/workspace.rsINSERT OR IGNORE INTO workspace_members (workspace_id, user_id, data)forcaller_id(when set). Wraps inif let Some(caller)so dev-mode unauth path silently skips (canvas.create's membership check is also dev-mode-bypassed). Failure logged but doesn't failworkspace.create.crates/hero_collab_ui/static/js/chat-app.jsinit()proxy-mode branch awaitsworkspace.list(was fire-and-forget).pickUserzero-workspace branch callsshowFirstRunSetup(user)instead of silent auto-create. NewshowFirstRunSetuppolished modal (~290 LOC) — full description below.Net: +312 lines, -6 lines.
First-run modal — polish details
The modal is forced (no dismiss-without-submit) because the chat is unusable without a workspace. All built via
createElementfor safety; only user-derived value (user.name) set viatextContent, noinnerHTMLon user data.rgba(0,0,0,0.85)+backdrop-filter: blur(4px).user-picker-overlay's 50%-black backdrop, which let the chat sidebar's avatar widget bleed through. Higher opacity + blur fixes the visual artifact.validate_channel_slugregex (^[a-z0-9][a-z0-9_-]*$). CatchesMy Channelbefore the server roundtrip.${user.name}'s Workspacegap: 14px), narrower modal (max-width: 420px)state.users.length > 1Behaviour matrix
${user.name}'s Workspacepre-filledcanvas.createNot a member of workspace N(PermissionDenied)workspace.createMy Channelfor first channelTest plan
My Channelshows red slug-rule hint and disables submitNotes
adminorworkspace_manageright. PR #20 grants it viaproxy.is_adminfederation. This PR can land before/after but tests cleanly with PR #20 active.state.workspaces=[General], so the modal doesn't fire. The modal only fires on a truly emptystate.workspaces.Two paired fixes for the operator-deployed bootstrap path: ## Server (handlers/workspace.rs) `workspace.create` now `INSERT OR IGNORE INTO workspace_members` for the creator after the workspace insert. Without this row, every per-workspace-gated operation (canvas.create, channel.member.add, ...) fails with `Not a member of workspace N` even though the creator passed the workspace.create permission check. `channel.create` and `canvas.create` already auto-add the creator with `role=admin`/`role=owner` — workspace.create was the only one missing the equivalent. The INSERT only runs when `caller_id` is set (proxy-mode + dev-mode with picker). In dev-mode unauth path, the existing `if !is_dev_mode()` guard inside canvas.create's membership check makes the missing row a non-issue. ## Client (chat-app.js) `init()` proxy-mode branch now `await`s `workspace.list` instead of fire-and-forget. Previously `pickUser` ran with a racy empty `state.workspaces`, which (pre-fix) silently created a workspace named "General" — confusing. `pickUser`'s zero-workspace branch calls `showFirstRunSetup(user)` instead of the silent auto-create. Slack-pattern first-run onboarding: forces the operator to name their workspace + first channel before unlocking the chat. `showFirstRunSetup` polish (vs. an earlier draft that reused the .user-picker-overlay class wholesale): * Backdrop bumped to rgba(0,0,0,0.85) + backdrop-filter blur(4px) so the underlying chat layout (sidebar avatar etc.) is fully hidden — fixes the "A circle bleeding through the workspace input" visual bug. * Submit button starts disabled + grey; enables only when both fields validate. Honest button state matching form validity. * Live per-field validation: - workspace name: required, max 80 chars - channel name: client-side mirror of server's validate_channel_slug (`^[a-z0-9][a-z0-9_-]*$`) — catches bad names before the server roundtrip. * Workspace name pre-filled with `${user.name}'s Workspace` (selected on focus so it can be overwritten immediately). * Inline animated spinner during submit + lock both inputs (visual motion = "still working"). * Per-field hint/error line + form-level error span. * Tighter vertical rhythm (gap:14px), narrower modal (max-width 420px) since it's only 2 fields. * Keyboard: Enter advances field-to-field; Cmd/Ctrl+Enter submits from anywhere; Esc clears form-level error. * "Switch user" link visible only when state.users.length > 1 (i.e. dev mode with picker). Proxy mode locks the user as before. DOM built via createElement throughout — only user-derived value (user.name) is set via textContent, no innerHTML on user data. ## Why a prompt instead of a "General" default hero_collab is a single-tenant component of a self-hosted OS, not a SaaS. The operator owns the deployment; subsequent users join existing workspaces by invite. The operator should name their workspace once, on install, like every modern self-hosted product. Subsequent users provisioning via user.me land without admin (the existing permissions model) and never see this screen because state.workspaces is non-empty for them.5b34e83ba7to1c008b47d9