Skip to content

feat(web): grouped workspace skills tab (Phase 2)#347

Open
mgoldsborough wants to merge 1 commit into
mainfrom
feat/workspace-skills-grouped
Open

feat(web): grouped workspace skills tab (Phase 2)#347
mgoldsborough wants to merge 1 commit into
mainfrom
feat/workspace-skills-grouped

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Summary

Phase 2 of SKILLS_SURFACE.md. The workspace settings tab at `/w/:slug/settings/skills` refactors from "one mixed list with a scope filter" to four grouped sections, each scoped to one writable audience:

  • Workspace — editable rows, Create button, full Edit / Delete / status / move-scope affordances.
  • Inherited from organization — disabled visual; selecting opens the body read-only with an "Edit in org settings →" deep link to `/org/skills` for org admins.
  • Inherited from bundles — disabled visual; read-only with existing "edit through the bundle's own settings" explanation.
  • Personal footer — count of user-tier skills + "Edit in profile →" link to `/profile` (Phase 3 promotes to `/profile/skills`). Personal skills follow the identity across workspaces — surfacing them as a count rather than as an authorable section keeps the mental model honest about where they live.

Plus the Phase 2 cleanup (spec OQ #5): drop the local `ORG_ADMIN_ROLES` redeclaration in `src/tools/platform/skills.ts` in favor of the canonical export from `src/identity/types.ts`. The two surfaces now agree by import.

Structural changes

  • `SkillsBrowser` accepts `surface?: "workspace"`. When set: one unfiltered `skills__list` fetch, renderer partitions via `groupByScope({ excludeUser: true })`, scope filter hidden, create form locked to `"workspace"`, `PersonalFooter` renders below the list.
  • `SkillRow` accepts `inherited?: boolean` for the dimmed treatment.
  • `SkillDetailView` accepts `readOnly` + `inheritedFrom` props; the previous `isBundle`-only gate broadens to honor whichever scope is inherited on the current surface.
  • Org-admin surface (`lockedScope="org"`) is unchanged — same single-section view, same forced create scope. Phase 1 tests still pass.

Test plan

  • `web/test/SkillsBrowser.workspace.test.tsx` — five behavioral cases:
    1. Scope `` is absent. Sections render in order: workspace, inherited org, inherited bundles; no user-tier section. Personal footer shows correct count + `/profile` deep link. Create form locked to `scope: "workspace"` regardless of internal state — the load-bearing assertion the server's `checkPathAccess` can't catch (mirror of the Phase 1 assertion for org). Initial `skills__list` is unfiltered (the renderer partitions). [x] Phase 1 tests still pass (orgRoleGate, SkillsBrowser.org). [x] `bun run verify:static` clean. [x] `bun run test:web` — 459 pass / 0 fail. [x] Skills-adjacent unit + integration tests — 179 pass / 0 fail. [x] `cd web && bun run build` clean. One pre-existing `bun run test:unit` failure unrelated to this PR: `test/unit/bundles/automations/markdown.test.ts` can't find `dompurify` (web subpackage dep). Reproduces on `main` HEAD. Flagged but not in scope. Phase 3 (separate PR) Promote `/profile` to a tabbed surface and add the Skills tab; rewire the `PersonalFooter` deep link from `/profile` to `/profile/skills` in the same PR.

Phase 2 of SKILLS_SURFACE.md. The workspace settings tab at
/w/:slug/settings/skills moves from "one mixed list with a scope
filter" to four grouped sections that match the spec's three-surface
model:

- **Workspace** — editable rows, Create button, full Edit / Delete /
  status / move-scope affordances.
- **Inherited from organization** — disabled visual treatment;
  selecting opens the body read-only with an "Edit in org settings →"
  deep link to /org/skills for org admins.
- **Inherited from bundles** — disabled visual treatment; read-only
  with the existing "edit through the bundle's own settings"
  explanation.
- **Personal footer** — count of user-tier skills + "Edit in profile
  →" link to /profile. Personal skills follow the identity across
  workspaces (the runtime invariant); the workspace tab surfaces them
  as a count rather than as an authorable section so the mental model
  matches the storage location.

Structural changes:

- `SkillsBrowser` accepts a new `surface?: "workspace"` prop. When
  set, it fetches every scope in one `skills__list` call, the
  renderer partitions in `groupByScope({ excludeUser: true })`, the
  scope filter is hidden, the create form is locked to scope
  "workspace", and a `PersonalFooter` renders below the list.
- `SkillRow` accepts `inherited?: boolean` for dimmed visual
  treatment on org/bundle rows.
- `SkillDetailView` accepts `readOnly` + `inheritedFrom` props; the
  previous `isBundle`-only gate broadens to honor whichever scope is
  inherited on the current surface.
- The org-admin surface (`lockedScope="org"`) is unchanged — same
  single-section view, same forced create scope.

Phase 2 cleanup folded in (spec open question #5): drop the local
`new Set(["admin","owner"])` redeclaration in
`src/tools/platform/skills.ts` in favor of the canonical
`ORG_ADMIN_ROLES` export from `src/identity/types.ts`. The workspace
surface and the server permission gate now agree on the predicate by
import, not by string-match.

Tests:

- New `web/test/SkillsBrowser.workspace.test.tsx` — five behavioral
  cases:
  1. Scope filter <select> is absent.
  2. Sections render in order: workspace, inherited org, inherited
     bundles; no user-tier section.
  3. Personal footer shows correct count + /profile deep link.
  4. Create form locked to scope="workspace" regardless of internal
     state — the load-bearing assertion the server's checkPathAccess
     can't catch.
  5. Initial skills__list is unfiltered (the renderer partitions).
- Phase 1 tests (orgRoleGate, SkillsBrowser.org) continue to pass —
  the org surface contract is preserved.
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