@skillcdn/console 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +4 -4
  2. package/dist/api.d.ts +4 -4
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +5 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +290 -46
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +5 -0
  11. package/dist/cli/credentials.d.ts.map +1 -1
  12. package/dist/cli/credentials.js +2 -0
  13. package/dist/cli/credentials.js.map +1 -1
  14. package/dist/cli/directory.d.ts +7 -0
  15. package/dist/cli/directory.d.ts.map +1 -1
  16. package/dist/cli/directory.js +7 -0
  17. package/dist/cli/directory.js.map +1 -1
  18. package/dist/cli/format.d.ts +10 -1
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +76 -20
  21. package/dist/cli/format.js.map +1 -1
  22. package/dist/cli/help.d.ts.map +1 -1
  23. package/dist/cli/help.js +42 -23
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/main.js +1 -0
  26. package/dist/cli/main.js.map +1 -1
  27. package/dist/client.d.ts +39 -9
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +24 -3
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.d.ts +28 -6
  32. package/dist/compat.d.ts.map +1 -1
  33. package/dist/compat.js +55 -14
  34. package/dist/compat.js.map +1 -1
  35. package/dist/components/board.d.ts.map +1 -1
  36. package/dist/components/board.js +6 -4
  37. package/dist/components/board.js.map +1 -1
  38. package/dist/components/calendar.d.ts +18 -8
  39. package/dist/components/calendar.d.ts.map +1 -1
  40. package/dist/components/calendar.js +39 -27
  41. package/dist/components/calendar.js.map +1 -1
  42. package/dist/components/event-feed.d.ts.map +1 -1
  43. package/dist/components/event-feed.js +2 -0
  44. package/dist/components/event-feed.js.map +1 -1
  45. package/dist/components/pack-connect.d.ts +23 -0
  46. package/dist/components/pack-connect.d.ts.map +1 -0
  47. package/dist/components/pack-connect.js +86 -0
  48. package/dist/components/pack-connect.js.map +1 -0
  49. package/dist/components/pack-token.d.ts +27 -0
  50. package/dist/components/pack-token.d.ts.map +1 -0
  51. package/dist/components/pack-token.js +11 -0
  52. package/dist/components/pack-token.js.map +1 -0
  53. package/dist/components/picker.d.ts +39 -0
  54. package/dist/components/picker.d.ts.map +1 -0
  55. package/dist/components/picker.js +155 -0
  56. package/dist/components/picker.js.map +1 -0
  57. package/dist/components/projects.d.ts +13 -0
  58. package/dist/components/projects.d.ts.map +1 -1
  59. package/dist/components/projects.js +37 -5
  60. package/dist/components/projects.js.map +1 -1
  61. package/dist/components/task-form.d.ts +18 -4
  62. package/dist/components/task-form.d.ts.map +1 -1
  63. package/dist/components/task-form.js +56 -13
  64. package/dist/components/task-form.js.map +1 -1
  65. package/dist/components/task-list.js +2 -2
  66. package/dist/components/task-list.js.map +1 -1
  67. package/dist/components/task-view.d.ts +7 -1
  68. package/dist/components/task-view.d.ts.map +1 -1
  69. package/dist/components/task-view.js +6 -5
  70. package/dist/components/task-view.js.map +1 -1
  71. package/dist/components/ui.d.ts +7 -0
  72. package/dist/components/ui.d.ts.map +1 -1
  73. package/dist/components/ui.js +11 -0
  74. package/dist/components/ui.js.map +1 -1
  75. package/dist/components/work.d.ts +3 -1
  76. package/dist/components/work.d.ts.map +1 -1
  77. package/dist/components/work.js +6 -4
  78. package/dist/components/work.js.map +1 -1
  79. package/dist/components/workspace.d.ts +46 -0
  80. package/dist/components/workspace.d.ts.map +1 -0
  81. package/dist/components/workspace.js +124 -0
  82. package/dist/components/workspace.js.map +1 -0
  83. package/dist/console.css +319 -11
  84. package/dist/console.d.ts.map +1 -1
  85. package/dist/console.js +83 -24
  86. package/dist/console.js.map +1 -1
  87. package/dist/data.d.ts +26 -1
  88. package/dist/data.d.ts.map +1 -1
  89. package/dist/data.js +59 -1
  90. package/dist/data.js.map +1 -1
  91. package/dist/dates.d.ts +12 -2
  92. package/dist/dates.d.ts.map +1 -1
  93. package/dist/dates.js +20 -2
  94. package/dist/dates.js.map +1 -1
  95. package/dist/i18n/en.d.ts +64 -3
  96. package/dist/i18n/en.d.ts.map +1 -1
  97. package/dist/i18n/en.js +81 -16
  98. package/dist/i18n/en.js.map +1 -1
  99. package/dist/i18n/ko.d.ts.map +1 -1
  100. package/dist/i18n/ko.js +77 -16
  101. package/dist/i18n/ko.js.map +1 -1
  102. package/dist/index.d.ts +6 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +6 -2
  105. package/dist/index.js.map +1 -1
  106. package/dist/limits.d.ts +2 -0
  107. package/dist/limits.d.ts.map +1 -1
  108. package/dist/limits.js +2 -0
  109. package/dist/limits.js.map +1 -1
  110. package/dist/router.d.ts +4 -4
  111. package/dist/router.d.ts.map +1 -1
  112. package/dist/router.js +11 -2
  113. package/dist/router.js.map +1 -1
  114. package/dist/routes.d.ts +4 -0
  115. package/dist/routes.d.ts.map +1 -1
  116. package/dist/routes.js +4 -0
  117. package/dist/routes.js.map +1 -1
  118. package/dist/schemas.d.ts +194 -13
  119. package/dist/schemas.d.ts.map +1 -1
  120. package/dist/schemas.js +121 -21
  121. package/dist/schemas.js.map +1 -1
  122. package/dist/vocabulary.d.ts +21 -1
  123. package/dist/vocabulary.d.ts.map +1 -1
  124. package/dist/vocabulary.js +18 -0
  125. package/dist/vocabulary.js.map +1 -1
  126. package/docs/specs/cli.md +25 -20
  127. package/docs/specs/rest.md +42 -19
  128. package/package.json +2 -2
  129. package/skills/building-a-console/SKILL.md +6 -6
  130. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  131. package/skills/deploying-a-console/SKILL.md +9 -8
  132. package/skills/deploying-a-console/references/resources.md +1 -1
  133. package/skills/working-the-board/SKILL.md +5 -5
  134. package/skills/working-the-board/references/setting-up.md +1 -1
  135. package/skills/working-the-board/references/terms.md +3 -1
  136. package/skills/working-the-board/references/the-sentence.md +1 -1
  137. package/src/api.ts +21 -0
  138. package/src/cli/cli.ts +384 -51
  139. package/src/cli/credentials.ts +7 -0
  140. package/src/cli/directory.ts +9 -0
  141. package/src/cli/format.ts +83 -20
  142. package/src/cli/help.ts +50 -23
  143. package/src/cli/main.ts +1 -0
  144. package/src/client.ts +107 -10
  145. package/src/compat.ts +63 -14
  146. package/src/components/board.tsx +10 -4
  147. package/src/components/calendar.tsx +76 -34
  148. package/src/components/event-feed.tsx +2 -0
  149. package/src/components/pack-connect.tsx +179 -0
  150. package/src/components/pack-token.tsx +74 -0
  151. package/src/components/picker.tsx +297 -0
  152. package/src/components/projects.tsx +108 -9
  153. package/src/components/task-form.tsx +149 -63
  154. package/src/components/task-list.tsx +2 -2
  155. package/src/components/task-view.tsx +23 -9
  156. package/src/components/ui.tsx +25 -0
  157. package/src/components/work.tsx +10 -5
  158. package/src/components/workspace.tsx +298 -0
  159. package/src/console.tsx +202 -32
  160. package/src/data.ts +101 -1
  161. package/src/dates.ts +23 -2
  162. package/src/i18n/en.ts +92 -16
  163. package/src/i18n/ko.ts +84 -16
  164. package/src/index.ts +32 -1
  165. package/src/limits.ts +2 -0
  166. package/src/router.ts +17 -5
  167. package/src/routes.ts +4 -0
  168. package/src/schemas.ts +140 -19
  169. package/src/styles/console.css +319 -11
  170. package/src/vocabulary.ts +28 -0
package/dist/console.js CHANGED
@@ -10,9 +10,10 @@ import { DecisionForm } from "./components/decision-form.js";
10
10
  import { DecisionCard, DecisionList } from "./components/decision-list.js";
11
11
  import { EventFeed } from "./components/event-feed.js";
12
12
  import { GoalForm, GoalList, GoalView, goalMonth } from "./components/goals.js";
13
+ import { PackTokenWays } from "./components/pack-token.js";
13
14
  import { PAGE_SIZE } from "./components/paged.js";
14
15
  import { ListingForm, PeopleList } from "./components/people.js";
15
- import { MemberList, PackTokenForm, ProjectForm, ProjectList, ProjectMark, } from "./components/projects.js";
16
+ import { MemberList, ProjectForm, ProjectList, ProjectMark, } from "./components/projects.js";
16
17
  import { GoalTimeline } from "./components/roadmap.js";
17
18
  import { RulesForm, RulesView } from "./components/rules.js";
18
19
  import { isPlainClick, Shell, } from "./components/shell.js";
@@ -24,6 +25,7 @@ import { TaskView } from "./components/task-view.js";
24
25
  import { NewToken, TokenForm, TokenList } from "./components/tokens.js";
25
26
  import { Button, Callout, CategoryBadge, EmptyState, personName, Spinner, StateBadge, Time, } from "./components/ui.js";
26
27
  import { WorkList } from "./components/work.js";
28
+ import { peopleVisibilityOf, WorkspaceSettingsForm, WorkspaceSettingsView, weekStartOf, } from "./components/workspace.js";
27
29
  import { ConnectPage } from "./connect-page.js";
28
30
  import { useConsoleData } from "./data.js";
29
31
  import { monthOf, today } from "./dates.js";
@@ -279,6 +281,26 @@ function TaskPage(props) {
279
281
  (fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
280
282
  ? fetched
281
283
  : undefined);
284
+ // The task this one is part of: among the board's, or asked for by itself when it was put away.
285
+ const parentId = task?.parentId ?? null;
286
+ const parentOnBoard = parentId === null ? undefined : data.tasks.find((candidate) => candidate.id === parentId);
287
+ const [parentFetched, setParentFetched] = useState(undefined);
288
+ const parentWanted = parentId !== null && parentOnBoard === undefined && data.projectLoaded ? parentId : undefined;
289
+ useEffect(() => {
290
+ if (parentWanted === undefined) {
291
+ return;
292
+ }
293
+ const controller = new AbortController();
294
+ fetchTask(parentWanted, controller.signal)
295
+ .then((found) => {
296
+ if (!controller.signal.aborted) {
297
+ setParentFetched(found);
298
+ }
299
+ })
300
+ .catch(() => undefined);
301
+ return () => controller.abort();
302
+ }, [parentWanted, fetchTask]);
303
+ const parent = parentOnBoard ?? (parentFetched?.id === parentId ? parentFetched : undefined);
282
304
  const [history, setHistory] = useState(undefined);
283
305
  const [comments, setComments] = useState(undefined);
284
306
  const { taskHistory, taskComments } = data.actions;
@@ -321,7 +343,7 @@ function TaskPage(props) {
321
343
  return (_jsx(EmptyState, { title: words.task.noSuch.title, body: words.task.noSuch.body, action: _jsx(LinkButton, { href: projectHref(key), children: words.nav.board }) }));
322
344
  }
323
345
  const here = taskHref(key, task.number);
324
- return (_jsx(components.TaskView, { task: task, people: data.people.filter(inWorkspace), tasks: data.tasks, decisions: data.decisions, runs: data.runs, comments: comments, history: history, categories: data.categories, goals: data.goals, taskHref: (candidate) => taskHref(key, candidate.number), decisionHref: (decisionId) => decisionHref(key, decisionId), goalHref: (goalId) => goalHref(key, goalId), fileHref: (fileId) => projectPath(key, "files", fileId), docHref: (path) => docHref(key, path), form: route.form, formHref: (form) => taskHref(key, task.number, form), onCancel: () => navigation.back(here), busy: busy, error: error, onChange: (target, patch) => void act(async () => {
346
+ return (_jsx(components.TaskView, { task: task, people: data.people.filter(inWorkspace), tasks: data.tasks, parent: parent, findTasks: data.actions.findTasks, findGoals: data.actions.findGoals, decisions: data.decisions, runs: data.runs, comments: comments, history: history, categories: data.categories, goals: data.goals, taskHref: (candidate) => taskHref(key, candidate.number), decisionHref: (decisionId) => decisionHref(key, decisionId), goalHref: (goalId) => goalHref(key, goalId), fileHref: (fileId) => projectPath(key, "files", fileId), docHref: (path) => docHref(key, path), form: route.form, formHref: (form) => taskHref(key, task.number, form), onCancel: () => navigation.back(here), busy: busy, error: error, onChange: (target, patch) => void act(async () => {
325
347
  await data.actions.updateTask(target.id, patch);
326
348
  navigation.replace(here);
327
349
  }), onMove: (target, state) => void act(() => data.actions.updateTask(target.id, { state })), onRaiseDecision: (input) => void act(async () => {
@@ -362,7 +384,7 @@ function ProjectPage(props) {
362
384
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsxs("div", { className: "sc-page-heading", children: [_jsx("h1", { className: "sc-page-title", children: project.name }), project.description.length > 0 && (_jsx("p", { className: "sc-page-lead", title: project.description, children: project.description }))] }), _jsxs("div", { className: "sc-page-actions", children: [_jsx(LinkButton, { href: projectHref(key, "archive"), children: words.board.archive }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] })] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people.filter(inWorkspace), me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(boardHref(key, filter)) })), _jsx(components.Board, { tasks: shown, taskHref: href, onOpen: (task) => navigation.go(href(task)), onMove: onMove, editHref: (task) => taskHref(key, task.number, "edit"), onEdit: (task) => navigation.go(taskHref(key, task.number, "edit")), onArchive: (task) => void act(() => data.actions.archiveTask(task.id)), empty: data.tasks.length === 0 ? (_jsx(EmptyState, { title: words.board.empty.title, body: words.board.empty.body, action: _jsx(LinkButton, { href: newTaskHref(key), children: words.board.writeTask }) })) : undefined })] }));
363
385
  }
364
386
  if (route.name === "new-task") {
365
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.task.newTitle }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.task.newAria, children: _jsx(TaskForm, { people: data.people.filter(inWorkspace), parents: data.tasks, categories: data.categories, goals: data.goals, busy: busy, onSubmit: (input) => void act(async () => {
387
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.task.newTitle }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.task.newAria, children: _jsx(TaskForm, { people: data.people.filter(inWorkspace), findTasks: data.actions.findTasks, categories: data.categories, findGoals: data.actions.findGoals, busy: busy, onSubmit: (input) => void act(async () => {
366
388
  const made = await data.actions.createTask(input);
367
389
  navigation.replace(taskHref(key, made.number));
368
390
  }), onCancel: () => navigation.back(projectHref(key)) }) })] }));
@@ -379,7 +401,7 @@ function ProjectPage(props) {
379
401
  const overdue = open.filter((task) => task.dueOn !== null && task.dueOn < now).sort(byDue);
380
402
  const unscheduled = open.filter((task) => task.dueOn === null).sort(byDue);
381
403
  const calendar = words.calendar;
382
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: calendar.title }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people.filter(inWorkspace), me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(calendarHref(key, route.month, filter)) })), _jsx(components.Calendar, { tasks: shown, month: month, today: now, categories: data.categories, goals: data.goals, taskHref: href, goalHref: (goal) => goalHref(key, goal.id), monthHref: (wanted) => calendarHref(key, wanted, route.filter) }), _jsxs("div", { className: "sc-calendar-below", children: [_jsxs("section", { className: "sc-task-section", "aria-label": calendar.overdue, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.overdue, " ", _jsx("span", { className: "sc-column-count", children: overdue.length })] }), _jsx(TaskList, { tasks: overdue, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.nothingOverdue }) })] }), _jsxs("section", { className: "sc-task-section", "aria-label": calendar.unscheduled, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.unscheduled, " ", _jsx("span", { className: "sc-column-count", children: unscheduled.length })] }), _jsx(TaskList, { tasks: unscheduled, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.allScheduled }) })] })] }), open.length > 0 && (_jsxs("section", { className: "sc-task-section", "aria-label": calendar.workload.title, children: [_jsx("h2", { className: "sc-section-title", children: calendar.workload.title }), _jsx(Workload, { tasks: shown, today: now, personHref: (person) => boardHref(key, person === undefined ? {} : { person: person.id }) })] }))] }));
404
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: calendar.title }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), data.tasks.length > 0 && (_jsx(BoardFilters, { filter: route.filter, categories: data.categories, goals: data.goals, people: data.people.filter(inWorkspace), me: me, shown: shown.length, total: data.tasks.length, onChange: (filter) => navigation.replace(calendarHref(key, route.month, filter)) })), _jsx(components.Calendar, { tasks: shown, month: month, today: now, weekStart: weekStartOf(data.me?.workspace), categories: data.categories, goals: data.goals, taskHref: href, goalHref: (goal) => goalHref(key, goal.id), monthHref: (wanted) => calendarHref(key, wanted, route.filter) }), _jsxs("div", { className: "sc-calendar-below", children: [_jsxs("section", { className: "sc-task-section", "aria-label": calendar.overdue, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.overdue, " ", _jsx("span", { className: "sc-column-count", children: overdue.length })] }), _jsx(TaskList, { tasks: overdue, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.nothingOverdue }) })] }), _jsxs("section", { className: "sc-task-section", "aria-label": calendar.unscheduled, children: [_jsxs("h2", { className: "sc-section-title", children: [calendar.unscheduled, " ", _jsx("span", { className: "sc-column-count", children: unscheduled.length })] }), _jsx(TaskList, { tasks: unscheduled, taskHref: href, today: now, showState: true, empty: _jsx("p", { className: "sc-muted", children: calendar.allScheduled }) })] })] }), open.length > 0 && (_jsxs("section", { className: "sc-task-section", "aria-label": calendar.workload.title, children: [_jsx("h2", { className: "sc-section-title", children: calendar.workload.title }), _jsx(Workload, { tasks: shown, today: now, weekStart: weekStartOf(data.me?.workspace), personHref: (person) => boardHref(key, person === undefined ? {} : { person: person.id }) })] }))] }));
383
405
  }
384
406
  if (route.name === "roadmap" || route.name === "new-goal") {
385
407
  const roadmap = words.roadmap;
@@ -425,7 +447,7 @@ function ProjectPage(props) {
425
447
  })
426
448
  : undefined, onArchiveTasks: owner && towards.some((task) => !OPEN_TASK_STATES.includes(task.state))
427
449
  ? () => void act(() => data.actions.archiveGoalTasks(goal.id))
428
- : undefined, busy: busy, view: route.view, viewHref: viewHref, board: _jsx(components.Board, { tasks: towards, taskHref: href, onOpen: (task) => navigation.go(href(task)), onMove: onMove, today: now, editHref: (task) => taskHref(key, task.number, "edit"), onEdit: (task) => navigation.go(taskHref(key, task.number, "edit")), onArchive: (task) => void act(() => data.actions.archiveTask(task.id)), empty: towards.length === 0 ? _jsx("p", { className: "sc-muted", children: roadmap.noTasks }) : undefined }), calendar: _jsx(components.Calendar, { tasks: towards, month: month, today: now, categories: data.categories, goals: [goal], taskHref: href, goalHref: (target) => goalHref(key, target.id), monthHref: (wanted) => goalHref(key, goal.id, { view: "calendar", month: wanted }) }) })] }));
450
+ : undefined, busy: busy, view: route.view, viewHref: viewHref, board: _jsx(components.Board, { tasks: towards, taskHref: href, onOpen: (task) => navigation.go(href(task)), onMove: onMove, today: now, editHref: (task) => taskHref(key, task.number, "edit"), onEdit: (task) => navigation.go(taskHref(key, task.number, "edit")), onArchive: (task) => void act(() => data.actions.archiveTask(task.id)), empty: towards.length === 0 ? _jsx("p", { className: "sc-muted", children: roadmap.noTasks }) : undefined }), calendar: _jsx(components.Calendar, { tasks: towards, month: month, today: now, weekStart: weekStartOf(data.me?.workspace), categories: data.categories, goals: [goal], taskHref: href, goalHref: (target) => goalHref(key, target.id), monthHref: (wanted) => goalHref(key, goal.id, { view: "calendar", month: wanted }) }) })] }));
429
451
  }
430
452
  if (route.name === "docs") {
431
453
  return (_jsx(DocsPage, { project: project, path: route.path, view: route.view, data: data, navigation: navigation }));
@@ -472,9 +494,17 @@ function ProjectPage(props) {
472
494
  const me = data.me?.person ?? undefined;
473
495
  const configures = owner;
474
496
  const members = words.projects.members;
475
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: members.title }) }), _jsx("p", { className: "sc-lead", children: words.vocabulary.visibility[project.visibility] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.MemberList, { members: data.members, people: data.people.filter(inWorkspace), me: me, busy: busy, onAdd: configures ? (input) => void act(() => data.actions.addMember(input)) : undefined, onChangeRole: configures
497
+ // In a project open to the workspace everyone is a member already (ADR-0035): the list is
498
+ // everyone, nobody is added or removed, and an owner is made from a row. Where the People
499
+ // page shows a member only whom they share a project with, an owner lists someone the list
500
+ // does not show by their login.
501
+ const open = project.visibility === "workspace";
502
+ const narrowed = peopleVisibilityOf(data.me?.workspace) === "shared_projects" && me?.role !== "admin";
503
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: members.title }) }), open && _jsx("p", { className: "sc-lead", children: members.open }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.MemberList, { members: data.members, people: data.people.filter(inWorkspace), me: me, open: open, busy: busy, onAdd: configures && !open
504
+ ? (input) => void act(() => data.actions.addMember(input))
505
+ : undefined, lookup: configures && !open && narrowed ? (login) => data.actions.findPerson(login) : undefined, onChangeRole: configures
476
506
  ? (member, role) => void act(() => data.actions.updateMember(member.person.id, { role }))
477
- : undefined, onRemove: configures
507
+ : undefined, onRemove: configures && !open
478
508
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
479
509
  : undefined, workHref: owner ? (member) => personWorkHref(member.person.id) : undefined, empty: _jsx(EmptyState, { title: members.empty.title, body: project.visibility === "workspace" ? members.empty.workspace : members.empty.private }) })] }));
480
510
  }
@@ -546,17 +576,17 @@ function ProjectSettingsPage(props) {
546
576
  if (!owner) {
547
577
  return _jsx(NothingHere, {});
548
578
  }
549
- return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.projects.packToken.aria, children: _jsx(PackTokenForm, { set: project.packToken !== null, busy: busy, onSubmit: (token) => void act(async () => {
550
- await data.actions.setPackToken(token);
579
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(PackTokenWays, { title: settings.packToken, intro: words.projects.packToken.intro, set: project.packToken !== null, setAt: project.packToken?.setAt, busy: busy, onStart: data.actions.connectPackToken, poll: data.actions.packTokenConnection, onCancelConnect: data.actions.cancelPackTokenConnection, onConnected: () => navigation.replace(here), onSubmit: (token) => void act(async () => {
580
+ await data.actions.setPackToken(token);
581
+ navigation.replace(here);
582
+ }), onRemove: project.packToken === null
583
+ ? undefined
584
+ : () => void act(async () => {
585
+ await data.actions.removePackToken();
551
586
  navigation.replace(here);
552
- }), onRemove: project.packToken === null
553
- ? undefined
554
- : () => void act(async () => {
555
- await data.actions.removePackToken();
556
- navigation.replace(here);
557
- }), onCancel: () => navigation.back(here) }) })] }));
587
+ }), onBack: () => navigation.back(here) })] }));
558
588
  }
559
- return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": settings.aria, children: _jsxs("dl", { className: "sc-facts sc-facts-plain", children: [_jsxs("div", { children: [_jsx("dt", { children: form.name }), _jsx("dd", { children: project.name })] }), _jsxs("div", { children: [_jsx("dt", { children: form.key }), _jsx("dd", { children: _jsx("code", { className: "sc-project-key", children: project.key }) })] }), _jsxs("div", { children: [_jsx("dt", { children: form.whoIsMember }), _jsx("dd", { children: words.vocabulary.visibility[project.visibility] })] }), _jsxs("div", { children: [_jsx("dt", { children: form.skillsAddress }), _jsx("dd", { children: project.skillsAddress !== null ? (_jsxs("code", { children: [skills?.source ?? "", project.skillsAddress] })) : skills?.address != null ? (settings.organizationsSkills(skills.address)) : (settings.noSkills) })] }), _jsxs("div", { children: [_jsx("dt", { children: settings.packToken }), _jsxs("dd", { children: [project.packToken === null ? (_jsx("span", { className: "sc-muted", children: settings.packTokenNone })) : (_jsxs(_Fragment, { children: [settings.packTokenSet, " ", _jsx(Time, { iso: project.packToken.setAt })] })), owner && (_jsxs(_Fragment, { children: [" ", _jsx("a", { href: projectSettingsHref(key, "pack-token"), children: settings.packTokenEdit })] }))] })] }), _jsxs("div", { children: [_jsx("dt", { children: form.symbol }), _jsx("dd", { children: _jsx(ProjectMark, { project: project, size: "sm" }) })] }), project.description.length > 0 && (_jsxs("div", { className: "sc-facts-wide", children: [_jsx("dt", { children: form.description }), _jsx("dd", { children: project.description })] }))] }) }), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.projectRules, children: [_jsx("h2", { className: "sc-section-title", children: words.rules.projectRules }), rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesView, { rules: rules.project, scope: "project", docHref: (path) => docHref(key, path), editHref: owner ? projectSettingsHref(key, "rules") : undefined, onArchive: owner
589
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": settings.aria, children: _jsxs("dl", { className: "sc-facts sc-facts-plain", children: [_jsxs("div", { children: [_jsx("dt", { children: form.name }), _jsx("dd", { children: project.name })] }), _jsxs("div", { children: [_jsx("dt", { children: form.key }), _jsx("dd", { children: _jsx("code", { className: "sc-project-key", children: project.key }) })] }), _jsxs("div", { children: [_jsx("dt", { children: form.whoIsMember }), _jsx("dd", { children: words.vocabulary.visibility[project.visibility] })] }), _jsxs("div", { children: [_jsx("dt", { children: form.skillsAddress }), _jsx("dd", { children: project.skillsAddress !== null ? (_jsxs("code", { children: [skills?.source ?? "", project.skillsAddress] })) : skills?.address != null ? (settings.organizationsSkills(skills.address)) : (settings.noSkills) })] }), _jsxs("div", { children: [_jsx("dt", { children: settings.packToken }), _jsxs("dd", { children: [project.packToken === null ? (_jsx("span", { className: "sc-muted", children: settings.packTokenNone })) : (_jsxs(_Fragment, { children: [settings.packTokenSet, " ", _jsx(Time, { iso: project.packToken.setAt })] })), owner && (_jsxs(_Fragment, { children: [" ", _jsx("a", { href: projectSettingsHref(key, "pack-token"), children: project.packToken === null ? settings.packTokenAdd : settings.packTokenEdit })] }))] })] }), _jsxs("div", { children: [_jsx("dt", { children: form.symbol }), _jsx("dd", { children: _jsx(ProjectMark, { project: project, size: "sm" }) })] }), project.description.length > 0 && (_jsxs("div", { className: "sc-facts-wide", children: [_jsx("dt", { children: form.description }), _jsx("dd", { children: project.description })] }))] }) }), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.projectRules, children: [_jsx("h2", { className: "sc-section-title", children: words.rules.projectRules }), rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesView, { rules: rules.project, scope: "project", docHref: (path) => docHref(key, path), editHref: owner ? projectSettingsHref(key, "rules") : undefined, onArchive: owner
560
590
  ? () => void act(async () => {
561
591
  await data.actions.archiveDocument(RULES_PATH);
562
592
  setRules(await data.actions.projectRules());
@@ -573,7 +603,8 @@ function WorkspaceSettingsPage(props) {
573
603
  const { form, data, navigation } = props;
574
604
  const { busy, error, act } = useAction();
575
605
  const administrator = data.me?.person?.role === "admin";
576
- const skills = data.me?.workspace.skills;
606
+ const workspace = data.me?.workspace;
607
+ const skills = workspace?.skills;
577
608
  const [rules, setRules] = useState(undefined);
578
609
  useEffect(() => {
579
610
  const controller = new AbortController();
@@ -605,14 +636,40 @@ function WorkspaceSettingsPage(props) {
605
636
  navigation.replace(PATHS.settings);
606
637
  }) })) })] }));
607
638
  }
608
- return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.title, children: [_jsx("h2", { className: "sc-section-title", children: words.rules.title }), rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesView, { rules: rules, scope: "workspace", editHref: administrator ? settingsHref("rules") : undefined, onArchive: administrator
639
+ if (form === "pack-token") {
640
+ if (!administrator || workspace === undefined) {
641
+ return _jsx(NothingHere, {});
642
+ }
643
+ const held = workspace.skills.packToken ?? null;
644
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(PackTokenWays, { title: words.settings.skillsToken, intro: words.settings.skillsTokenIntro, set: held !== null, setAt: held?.setAt, busy: busy, onStart: data.actions.connectWorkspacePackToken, poll: data.actions.workspacePackTokenConnection, onCancelConnect: data.actions.cancelWorkspacePackTokenConnection, onConnected: () => navigation.replace(PATHS.settings), onSubmit: (token) => void act(async () => {
645
+ await data.actions.setWorkspacePackToken(token);
646
+ navigation.replace(PATHS.settings);
647
+ }), onRemove: held === null
648
+ ? undefined
649
+ : () => void act(async () => {
650
+ await data.actions.removeWorkspacePackToken();
651
+ navigation.replace(PATHS.settings);
652
+ }), onBack: () => navigation.back(PATHS.settings) })] }));
653
+ }
654
+ if (form === "workspace") {
655
+ if (!administrator || workspace === undefined) {
656
+ return _jsx(NothingHere, {});
657
+ }
658
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsxs("section", { className: "sc-panel sc-panel-form", "aria-label": words.settings.workspace.aria, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.workspace.title }), _jsx(WorkspaceSettingsForm, { workspace: workspace, busy: busy, onSubmit: (patch) => void act(async () => {
659
+ await data.actions.updateWorkspace(patch);
660
+ navigation.replace(PATHS.settings);
661
+ }), onCancel: () => navigation.back(PATHS.settings) })] })] }));
662
+ }
663
+ return (_jsxs(_Fragment, { children: [head, error !== undefined && _jsx(Callout, { tone: "danger", children: error }), workspace !== undefined && (_jsxs("section", { className: "sc-panel", "aria-label": words.settings.workspace.aria, children: [_jsxs("div", { className: "sc-section-head", children: [_jsx("h2", { className: "sc-section-title", children: words.settings.workspace.title }), administrator && (_jsx("a", { className: "sc-button sc-button-secondary sc-button-sm", href: settingsHref("workspace"), children: words.common.edit }))] }), _jsx(WorkspaceSettingsView, { workspace: workspace })] })), _jsxs("section", { className: "sc-panel", "aria-label": words.rules.title, children: [_jsx("h2", { className: "sc-section-title", children: words.rules.title }), rules === undefined ? (_jsx(Spinner, { label: words.rules.loading })) : (_jsx(RulesView, { rules: rules, scope: "workspace", editHref: administrator ? settingsHref("rules") : undefined, onArchive: administrator
609
664
  ? () => void act(async () => {
610
665
  setRules(await data.actions.archiveRules());
611
666
  })
612
- : undefined, busy: busy }))] }), _jsxs("section", { className: "sc-panel", "aria-label": words.settings.categories, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.categories }), data.categories.length > 0 && (_jsx("p", { className: "sc-category-chips", children: data.categories.map((category) => (_jsx(CategoryBadge, { category: category }, category.key))) })), _jsx(LinkButton, { href: PATHS.categories, children: words.settings.manageCategories })] }), skills !== undefined && (_jsxs("section", { className: "sc-panel", "aria-label": words.settings.skills, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.skills }), _jsxs("dl", { className: "sc-facts sc-facts-plain", children: [_jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsSource }), _jsx("dd", { children: _jsx("a", { href: skills.source, target: "_blank", rel: "noopener noreferrer", children: skills.source }) })] }), _jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsAddress }), _jsx("dd", { children: skills.address === null ? (words.settings.noSkillsAddress) : (_jsx("a", { href: `${skills.source}${skills.address}`, target: "_blank", rel: "noopener noreferrer", children: _jsx("code", { children: skills.address }) })) })] })] }), _jsx("p", { className: "sc-muted", children: words.settings.skillsFromConfiguration })] }))] }));
667
+ : undefined, busy: busy }))] }), _jsxs("section", { className: "sc-panel", "aria-label": words.settings.categories, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.categories }), data.categories.length > 0 && (_jsx("p", { className: "sc-category-chips", children: data.categories.map((category) => (_jsx(CategoryBadge, { category: category }, category.key))) })), _jsx(LinkButton, { href: PATHS.categories, children: words.settings.manageCategories })] }), skills !== undefined && (_jsxs("section", { className: "sc-panel", "aria-label": words.settings.skills, children: [_jsx("h2", { className: "sc-section-title", children: words.settings.skills }), _jsxs("dl", { className: "sc-facts sc-facts-plain", children: [_jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsSource }), _jsx("dd", { children: _jsx("a", { href: skills.source, target: "_blank", rel: "noopener noreferrer", children: skills.source }) })] }), _jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsAddress }), _jsx("dd", { children: skills.address === null ? (words.settings.noSkillsAddress) : (_jsx("a", { href: `${skills.source}${skills.address}`, target: "_blank", rel: "noopener noreferrer", children: _jsx("code", { children: skills.address }) })) })] }), skills.address !== null && (_jsxs("div", { children: [_jsx("dt", { children: words.settings.skillsToken }), _jsxs("dd", { children: [skills.packToken == null ? (_jsx("span", { className: "sc-muted", children: words.projects.settings.packTokenNone })) : (_jsxs(_Fragment, { children: [words.projects.settings.packTokenSet, " ", _jsx(Time, { iso: skills.packToken.setAt })] })), administrator && (_jsxs(_Fragment, { children: [" ", _jsx("a", { href: settingsHref("pack-token"), children: skills.packToken == null
668
+ ? words.projects.settings.packTokenAdd
669
+ : words.projects.settings.packTokenEdit })] }))] })] }))] }), _jsx("p", { className: "sc-muted", children: words.settings.skillsFromConfiguration })] }))] }));
613
670
  }
614
671
  /** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
615
- /** Whether a person is in: who a picker of assignees and of members offers (ADR-0022). */
672
+ /** Whether a person is in: who a picker of workers and of members offers (ADR-0022). */
616
673
  const inWorkspace = (person) => person.status === "active";
617
674
  const uploadOf = (file) => ({
618
675
  name: file.name,
@@ -695,13 +752,13 @@ function PlatePage(props) {
695
752
  }, [load, couldNotLoad, refused]);
696
753
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: props.title }), props.aside] }), problem !== undefined && _jsx(Callout, { tone: "danger", children: problem }), forbidden && refused !== undefined ? (_jsx(EmptyState, { title: refused.title, body: refused.body, action: _jsx(LinkButton, { href: PATHS.work, children: words.nav.work }) })) : plate === undefined ? (problem === undefined && (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: props.loading }) }))) : plate.tasks.length === 0 &&
697
754
  (plate.closed?.length ?? 0) === 0 &&
698
- (plate.decisions?.length ?? 0) === 0 ? (_jsx(EmptyState, { title: props.empty.title, body: props.empty.body })) : (_jsx(components.WorkList, { work: plate, week: props.week, taskHref: (projectKey, task) => taskHref(projectKey, task.number), decisionHref: (projectKey, decisionId) => decisionHref(projectKey, decisionId), weekHref: props.weekHref }))] }));
755
+ (plate.decisions?.length ?? 0) === 0 ? (_jsx(EmptyState, { title: props.empty.title, body: props.empty.body })) : (_jsx(components.WorkList, { work: plate, week: props.week, weekStart: props.weekStart, taskHref: (projectKey, task) => taskHref(projectKey, task.number), decisionHref: (projectKey, decisionId) => decisionHref(projectKey, decisionId), weekHref: props.weekHref }))] }));
699
756
  }
700
757
  /** What is on the person's own plate, across their projects. */
701
758
  function WorkPage(props) {
702
759
  const words = useWords();
703
760
  const { work } = props.data.actions;
704
- return (_jsx(PlatePage, { components: props.components, load: work, title: words.work.title, week: props.week, weekHref: workHref, loading: words.work.loading, couldNotLoad: words.work.couldNotLoad, empty: words.work.empty }));
761
+ return (_jsx(PlatePage, { components: props.components, load: work, title: words.work.title, week: props.week, weekStart: weekStartOf(props.data.me?.workspace), weekHref: workHref, loading: words.work.loading, couldNotLoad: words.work.couldNotLoad, empty: words.work.empty }));
705
762
  }
706
763
  /** Another person's plate, for whoever oversees them: an administrator, or an owner of a project they are in. */
707
764
  function PersonWorkPage(props) {
@@ -711,7 +768,7 @@ function PersonWorkPage(props) {
711
768
  const name = person === undefined ? "?" : personName(person);
712
769
  const { personWork } = data.actions;
713
770
  const load = useCallback((signal) => personWork(id, signal), [personWork, id]);
714
- return (_jsx(PlatePage, { components: props.components, load: load, title: words.people.workOf(name), aside: _jsx(LinkButton, { href: PATHS.people, children: words.nav.people }), week: props.week, weekHref: (day) => personWorkHref(id, day), loading: words.people.loadingWork, couldNotLoad: words.people.couldNotLoadWork, empty: { title: words.people.noWork.title(name), body: words.people.noWork.body }, refused: words.people.notOverseen }));
771
+ return (_jsx(PlatePage, { components: props.components, load: load, title: words.people.workOf(name), aside: _jsx(LinkButton, { href: PATHS.people, children: words.nav.people }), week: props.week, weekStart: weekStartOf(data.me?.workspace), weekHref: (day) => personWorkHref(id, day), loading: words.people.loadingWork, couldNotLoad: words.people.couldNotLoadWork, empty: { title: words.people.noWork.title(name), body: words.people.noWork.body }, refused: words.people.notOverseen }));
715
772
  }
716
773
  function WorkspacePage(props) {
717
774
  const words = useWords();
@@ -745,7 +802,9 @@ function WorkspacePage(props) {
745
802
  const making = route.name === "new-listing" && administrator;
746
803
  // An owner sees the work of the people in their projects; the console refuses the rest.
747
804
  const oversees = administrator || data.projects.some((project) => project.role === "owner");
748
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.people.title }), administrator && !making && (_jsx(LinkButton, { href: PATHS.newListing, variant: "primary", children: words.people.letIn }))] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.people.letInAria, children: _jsx(ListingForm, { providers: data.me?.signIn ?? [], busy: busy, onSubmit: (input) => void act(async () => {
805
+ // The list is what the policy shows (ADR-0035): everyone, or the people one shares a project with.
806
+ const narrowed = peopleVisibilityOf(data.me?.workspace) === "shared_projects" && !administrator;
807
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.people.title }), administrator && !making && (_jsx(LinkButton, { href: PATHS.newListing, variant: "primary", children: words.people.letIn }))] }), narrowed && _jsx("p", { className: "sc-lead", children: words.people.narrowed }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel sc-panel-form", "aria-label": words.people.letInAria, children: _jsx(ListingForm, { providers: data.me?.signIn ?? [], busy: busy, onSubmit: (input) => void act(async () => {
749
808
  await data.actions.createListing(input);
750
809
  navigation.replace(PATHS.people);
751
810
  }), onCancel: () => navigation.back(PATHS.people) }) })), _jsx(components.PeopleList, { people: data.people, listed: data.listed, organization: data.organization, me: me, busy: busy, onChangeRole: administrator