@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/src/console.tsx CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  RULES_PATH,
36
36
  RULES_TITLE,
37
37
  type TaskState,
38
+ type WeekStart,
38
39
  } from "./api.js";
39
40
  import { Board, BoardFilters, type BoardProps, byDue, filterTasks } from "./components/board.js";
40
41
  import { Calendar, type CalendarProps, Workload } from "./components/calendar.js";
@@ -44,12 +45,12 @@ import { DecisionForm } from "./components/decision-form.js";
44
45
  import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
45
46
  import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
46
47
  import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
48
+ import { PackTokenWays } from "./components/pack-token.js";
47
49
  import { PAGE_SIZE } from "./components/paged.js";
48
50
  import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
49
51
  import {
50
52
  MemberList,
51
53
  type MemberListProps,
52
- PackTokenForm,
53
54
  ProjectForm,
54
55
  ProjectList,
55
56
  type ProjectListProps,
@@ -81,6 +82,12 @@ import {
81
82
  Time,
82
83
  } from "./components/ui.js";
83
84
  import { WorkList, type WorkListProps } from "./components/work.js";
85
+ import {
86
+ peopleVisibilityOf,
87
+ WorkspaceSettingsForm,
88
+ WorkspaceSettingsView,
89
+ weekStartOf,
90
+ } from "./components/workspace.js";
84
91
  import { ConnectPage } from "./connect-page.js";
85
92
  import { type ConsoleData, useConsoleData } from "./data.js";
86
93
  import { monthOf, today } from "./dates.js";
@@ -593,6 +600,28 @@ function TaskPage(props: {
593
600
  (fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
594
601
  ? fetched
595
602
  : undefined);
603
+ // The task this one is part of: among the board's, or asked for by itself when it was put away.
604
+ const parentId = task?.parentId ?? null;
605
+ const parentOnBoard =
606
+ parentId === null ? undefined : data.tasks.find((candidate) => candidate.id === parentId);
607
+ const [parentFetched, setParentFetched] = useState<RestTask | undefined>(undefined);
608
+ const parentWanted =
609
+ parentId !== null && parentOnBoard === undefined && data.projectLoaded ? parentId : undefined;
610
+ useEffect(() => {
611
+ if (parentWanted === undefined) {
612
+ return;
613
+ }
614
+ const controller = new AbortController();
615
+ fetchTask(parentWanted, controller.signal)
616
+ .then((found) => {
617
+ if (!controller.signal.aborted) {
618
+ setParentFetched(found);
619
+ }
620
+ })
621
+ .catch(() => undefined);
622
+ return () => controller.abort();
623
+ }, [parentWanted, fetchTask]);
624
+ const parent = parentOnBoard ?? (parentFetched?.id === parentId ? parentFetched : undefined);
596
625
  const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
597
626
  const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
598
627
  const { taskHistory, taskComments } = data.actions;
@@ -646,6 +675,9 @@ function TaskPage(props: {
646
675
  task={task}
647
676
  people={data.people.filter(inWorkspace)}
648
677
  tasks={data.tasks}
678
+ parent={parent}
679
+ findTasks={data.actions.findTasks}
680
+ findGoals={data.actions.findGoals}
649
681
  decisions={data.decisions}
650
682
  runs={data.runs}
651
683
  comments={comments}
@@ -798,9 +830,9 @@ function ProjectPage(props: {
798
830
  <section className="sc-panel sc-panel-form" aria-label={words.task.newAria}>
799
831
  <TaskForm
800
832
  people={data.people.filter(inWorkspace)}
801
- parents={data.tasks}
833
+ findTasks={data.actions.findTasks}
802
834
  categories={data.categories}
803
- goals={data.goals}
835
+ findGoals={data.actions.findGoals}
804
836
  busy={busy}
805
837
  onSubmit={(input: RestTaskInput) =>
806
838
  void act(async () => {
@@ -861,6 +893,7 @@ function ProjectPage(props: {
861
893
  tasks={shown}
862
894
  month={month}
863
895
  today={now}
896
+ weekStart={weekStartOf(data.me?.workspace)}
864
897
  categories={data.categories}
865
898
  goals={data.goals}
866
899
  taskHref={href}
@@ -899,6 +932,7 @@ function ProjectPage(props: {
899
932
  <Workload
900
933
  tasks={shown}
901
934
  today={now}
935
+ weekStart={weekStartOf(data.me?.workspace)}
902
936
  personHref={(person) =>
903
937
  boardHref(key, person === undefined ? {} : { person: person.id })
904
938
  }
@@ -1075,6 +1109,7 @@ function ProjectPage(props: {
1075
1109
  tasks={towards}
1076
1110
  month={month}
1077
1111
  today={now}
1112
+ weekStart={weekStartOf(data.me?.workspace)}
1078
1113
  categories={data.categories}
1079
1114
  goals={[goal]}
1080
1115
  taskHref={href}
@@ -1220,19 +1255,34 @@ function ProjectPage(props: {
1220
1255
  const me = data.me?.person ?? undefined;
1221
1256
  const configures = owner;
1222
1257
  const members = words.projects.members;
1258
+ // In a project open to the workspace everyone is a member already (ADR-0035): the list is
1259
+ // everyone, nobody is added or removed, and an owner is made from a row. Where the People
1260
+ // page shows a member only whom they share a project with, an owner lists someone the list
1261
+ // does not show by their login.
1262
+ const open = project.visibility === "workspace";
1263
+ const narrowed =
1264
+ peopleVisibilityOf(data.me?.workspace) === "shared_projects" && me?.role !== "admin";
1223
1265
  return (
1224
1266
  <>
1225
1267
  <div className="sc-page-head">
1226
1268
  <h1 className="sc-page-title">{members.title}</h1>
1227
1269
  </div>
1228
- <p className="sc-lead">{words.vocabulary.visibility[project.visibility]}</p>
1270
+ {open && <p className="sc-lead">{members.open}</p>}
1229
1271
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1230
1272
  <components.MemberList
1231
1273
  members={data.members}
1232
1274
  people={data.people.filter(inWorkspace)}
1233
1275
  me={me}
1276
+ open={open}
1234
1277
  busy={busy}
1235
- onAdd={configures ? (input) => void act(() => data.actions.addMember(input)) : undefined}
1278
+ onAdd={
1279
+ configures && !open
1280
+ ? (input) => void act(() => data.actions.addMember(input))
1281
+ : undefined
1282
+ }
1283
+ lookup={
1284
+ configures && !open && narrowed ? (login) => data.actions.findPerson(login) : undefined
1285
+ }
1236
1286
  onChangeRole={
1237
1287
  configures
1238
1288
  ? (member, role) =>
@@ -1240,7 +1290,7 @@ function ProjectPage(props: {
1240
1290
  : undefined
1241
1291
  }
1242
1292
  onRemove={
1243
- configures
1293
+ configures && !open
1244
1294
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
1245
1295
  : undefined
1246
1296
  }
@@ -1382,28 +1432,33 @@ function ProjectSettingsPage(props: {
1382
1432
  <>
1383
1433
  {head}
1384
1434
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1385
- <section className="sc-panel sc-panel-form" aria-label={words.projects.packToken.aria}>
1386
- <PackTokenForm
1387
- set={project.packToken !== null}
1388
- busy={busy}
1389
- onSubmit={(token) =>
1390
- void act(async () => {
1391
- await data.actions.setPackToken(token);
1392
- navigation.replace(here);
1393
- })
1394
- }
1395
- onRemove={
1396
- project.packToken === null
1397
- ? undefined
1398
- : () =>
1399
- void act(async () => {
1400
- await data.actions.removePackToken();
1401
- navigation.replace(here);
1402
- })
1403
- }
1404
- onCancel={() => navigation.back(here)}
1405
- />
1406
- </section>
1435
+ <PackTokenWays
1436
+ title={settings.packToken}
1437
+ intro={words.projects.packToken.intro}
1438
+ set={project.packToken !== null}
1439
+ setAt={project.packToken?.setAt}
1440
+ busy={busy}
1441
+ onStart={data.actions.connectPackToken}
1442
+ poll={data.actions.packTokenConnection}
1443
+ onCancelConnect={data.actions.cancelPackTokenConnection}
1444
+ onConnected={() => navigation.replace(here)}
1445
+ onSubmit={(token) =>
1446
+ void act(async () => {
1447
+ await data.actions.setPackToken(token);
1448
+ navigation.replace(here);
1449
+ })
1450
+ }
1451
+ onRemove={
1452
+ project.packToken === null
1453
+ ? undefined
1454
+ : () =>
1455
+ void act(async () => {
1456
+ await data.actions.removePackToken();
1457
+ navigation.replace(here);
1458
+ })
1459
+ }
1460
+ onBack={() => navigation.back(here)}
1461
+ />
1407
1462
  </>
1408
1463
  );
1409
1464
  }
@@ -1455,7 +1510,9 @@ function ProjectSettingsPage(props: {
1455
1510
  {owner && (
1456
1511
  <>
1457
1512
  {" "}
1458
- <a href={projectSettingsHref(key, "pack-token")}>{settings.packTokenEdit}</a>
1513
+ <a href={projectSettingsHref(key, "pack-token")}>
1514
+ {project.packToken === null ? settings.packTokenAdd : settings.packTokenEdit}
1515
+ </a>
1459
1516
  </>
1460
1517
  )}
1461
1518
  </dd>
@@ -1516,7 +1573,7 @@ function ProjectSettingsPage(props: {
1516
1573
  * from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
1517
1574
  */
1518
1575
  function WorkspaceSettingsPage(props: {
1519
- readonly form: "rules" | undefined;
1576
+ readonly form: "rules" | "workspace" | "pack-token" | undefined;
1520
1577
  readonly data: ConsoleData;
1521
1578
  readonly navigation: Navigation;
1522
1579
  }) {
@@ -1524,7 +1581,8 @@ function WorkspaceSettingsPage(props: {
1524
1581
  const { form, data, navigation } = props;
1525
1582
  const { busy, error, act } = useAction();
1526
1583
  const administrator = data.me?.person?.role === "admin";
1527
- const skills = data.me?.workspace.skills;
1584
+ const workspace = data.me?.workspace;
1585
+ const skills = workspace?.skills;
1528
1586
  const [rules, setRules] = useState<RestRules | undefined>(undefined);
1529
1587
  useEffect(() => {
1530
1588
  const controller = new AbortController();
@@ -1587,10 +1645,90 @@ function WorkspaceSettingsPage(props: {
1587
1645
  </>
1588
1646
  );
1589
1647
  }
1648
+ if (form === "pack-token") {
1649
+ if (!administrator || workspace === undefined) {
1650
+ return <NothingHere />;
1651
+ }
1652
+ const held = workspace.skills.packToken ?? null;
1653
+ return (
1654
+ <>
1655
+ {head}
1656
+ {error !== undefined && <Callout tone="danger">{error}</Callout>}
1657
+ <PackTokenWays
1658
+ title={words.settings.skillsToken}
1659
+ intro={words.settings.skillsTokenIntro}
1660
+ set={held !== null}
1661
+ setAt={held?.setAt}
1662
+ busy={busy}
1663
+ onStart={data.actions.connectWorkspacePackToken}
1664
+ poll={data.actions.workspacePackTokenConnection}
1665
+ onCancelConnect={data.actions.cancelWorkspacePackTokenConnection}
1666
+ onConnected={() => navigation.replace(PATHS.settings)}
1667
+ onSubmit={(token) =>
1668
+ void act(async () => {
1669
+ await data.actions.setWorkspacePackToken(token);
1670
+ navigation.replace(PATHS.settings);
1671
+ })
1672
+ }
1673
+ onRemove={
1674
+ held === null
1675
+ ? undefined
1676
+ : () =>
1677
+ void act(async () => {
1678
+ await data.actions.removeWorkspacePackToken();
1679
+ navigation.replace(PATHS.settings);
1680
+ })
1681
+ }
1682
+ onBack={() => navigation.back(PATHS.settings)}
1683
+ />
1684
+ </>
1685
+ );
1686
+ }
1687
+ if (form === "workspace") {
1688
+ if (!administrator || workspace === undefined) {
1689
+ return <NothingHere />;
1690
+ }
1691
+ return (
1692
+ <>
1693
+ {head}
1694
+ {error !== undefined && <Callout tone="danger">{error}</Callout>}
1695
+ <section className="sc-panel sc-panel-form" aria-label={words.settings.workspace.aria}>
1696
+ <h2 className="sc-section-title">{words.settings.workspace.title}</h2>
1697
+ <WorkspaceSettingsForm
1698
+ workspace={workspace}
1699
+ busy={busy}
1700
+ onSubmit={(patch) =>
1701
+ void act(async () => {
1702
+ await data.actions.updateWorkspace(patch);
1703
+ navigation.replace(PATHS.settings);
1704
+ })
1705
+ }
1706
+ onCancel={() => navigation.back(PATHS.settings)}
1707
+ />
1708
+ </section>
1709
+ </>
1710
+ );
1711
+ }
1590
1712
  return (
1591
1713
  <>
1592
1714
  {head}
1593
1715
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1716
+ {workspace !== undefined && (
1717
+ <section className="sc-panel" aria-label={words.settings.workspace.aria}>
1718
+ <div className="sc-section-head">
1719
+ <h2 className="sc-section-title">{words.settings.workspace.title}</h2>
1720
+ {administrator && (
1721
+ <a
1722
+ className="sc-button sc-button-secondary sc-button-sm"
1723
+ href={settingsHref("workspace")}
1724
+ >
1725
+ {words.common.edit}
1726
+ </a>
1727
+ )}
1728
+ </div>
1729
+ <WorkspaceSettingsView workspace={workspace} />
1730
+ </section>
1731
+ )}
1594
1732
  <section className="sc-panel" aria-label={words.rules.title}>
1595
1733
  <h2 className="sc-section-title">{words.rules.title}</h2>
1596
1734
  {rules === undefined ? (
@@ -1651,6 +1789,30 @@ function WorkspaceSettingsPage(props: {
1651
1789
  )}
1652
1790
  </dd>
1653
1791
  </div>
1792
+ {skills.address !== null && (
1793
+ <div>
1794
+ <dt>{words.settings.skillsToken}</dt>
1795
+ <dd>
1796
+ {skills.packToken == null ? (
1797
+ <span className="sc-muted">{words.projects.settings.packTokenNone}</span>
1798
+ ) : (
1799
+ <>
1800
+ {words.projects.settings.packTokenSet} <Time iso={skills.packToken.setAt} />
1801
+ </>
1802
+ )}
1803
+ {administrator && (
1804
+ <>
1805
+ {" "}
1806
+ <a href={settingsHref("pack-token")}>
1807
+ {skills.packToken == null
1808
+ ? words.projects.settings.packTokenAdd
1809
+ : words.projects.settings.packTokenEdit}
1810
+ </a>
1811
+ </>
1812
+ )}
1813
+ </dd>
1814
+ </div>
1815
+ )}
1654
1816
  </dl>
1655
1817
  <p className="sc-muted">{words.settings.skillsFromConfiguration}</p>
1656
1818
  </section>
@@ -1660,7 +1822,7 @@ function WorkspaceSettingsPage(props: {
1660
1822
  }
1661
1823
 
1662
1824
  /** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
1663
- /** Whether a person is in: who a picker of assignees and of members offers (ADR-0022). */
1825
+ /** Whether a person is in: who a picker of workers and of members offers (ADR-0022). */
1664
1826
  const inWorkspace = (person: { readonly status: "active" | "deactivated" }): boolean =>
1665
1827
  person.status === "active";
1666
1828
 
@@ -1777,6 +1939,8 @@ function PlatePage(props: {
1777
1939
  readonly empty: { readonly title: string; readonly body: string };
1778
1940
  /** What a refusal says where the plate may not be the asker's to see; the server's words otherwise. */
1779
1941
  readonly refused?: { readonly title: string; readonly body: string } | undefined;
1942
+ /** The day the week begins on, as the workspace says (ADR-0035). */
1943
+ readonly weekStart?: WeekStart | undefined;
1780
1944
  }) {
1781
1945
  const words = useWords();
1782
1946
  const { components, load, couldNotLoad, refused } = props;
@@ -1837,6 +2001,7 @@ function PlatePage(props: {
1837
2001
  <components.WorkList
1838
2002
  work={plate}
1839
2003
  week={props.week}
2004
+ weekStart={props.weekStart}
1840
2005
  taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
1841
2006
  decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
1842
2007
  weekHref={props.weekHref}
@@ -1860,6 +2025,7 @@ function WorkPage(props: {
1860
2025
  load={work}
1861
2026
  title={words.work.title}
1862
2027
  week={props.week}
2028
+ weekStart={weekStartOf(props.data.me?.workspace)}
1863
2029
  weekHref={workHref}
1864
2030
  loading={words.work.loading}
1865
2031
  couldNotLoad={words.work.couldNotLoad}
@@ -1888,6 +2054,7 @@ function PersonWorkPage(props: {
1888
2054
  title={words.people.workOf(name)}
1889
2055
  aside={<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>}
1890
2056
  week={props.week}
2057
+ weekStart={weekStartOf(data.me?.workspace)}
1891
2058
  weekHref={(day) => personWorkHref(id, day)}
1892
2059
  loading={words.people.loadingWork}
1893
2060
  couldNotLoad={words.people.couldNotLoadWork}
@@ -1958,6 +2125,8 @@ function WorkspacePage(props: {
1958
2125
  const making = route.name === "new-listing" && administrator;
1959
2126
  // An owner sees the work of the people in their projects; the console refuses the rest.
1960
2127
  const oversees = administrator || data.projects.some((project) => project.role === "owner");
2128
+ // The list is what the policy shows (ADR-0035): everyone, or the people one shares a project with.
2129
+ const narrowed = peopleVisibilityOf(data.me?.workspace) === "shared_projects" && !administrator;
1961
2130
  return (
1962
2131
  <>
1963
2132
  <div className="sc-page-head">
@@ -1968,6 +2137,7 @@ function WorkspacePage(props: {
1968
2137
  </LinkButton>
1969
2138
  )}
1970
2139
  </div>
2140
+ {narrowed && <p className="sc-lead">{words.people.narrowed}</p>}
1971
2141
  {error !== undefined && <Callout tone="danger">{error}</Callout>}
1972
2142
  {making && (
1973
2143
  <section className="sc-panel sc-panel-form" aria-label={words.people.letInAria}>
package/src/data.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  type RestGoal,
25
25
  type RestGoalInput,
26
26
  type RestGoalPatch,
27
+ type RestGoals,
27
28
  type RestListing,
28
29
  type RestListingInput,
29
30
  type RestMe,
@@ -32,6 +33,8 @@ import {
32
33
  type RestMemberPatch,
33
34
  type RestMePatch,
34
35
  type RestOrganization,
36
+ type RestPackConnect,
37
+ type RestPackConnectStarted,
35
38
  type RestPerson,
36
39
  type RestPersonPatch,
37
40
  type RestPersonWork,
@@ -54,6 +57,9 @@ import {
54
57
  type RestVersion,
55
58
  type RestVersions,
56
59
  type RestWork,
60
+ type RestWorkspace,
61
+ type RestWorkspacePackConnect,
62
+ type RestWorkspacePatch,
57
63
  restEventSchema,
58
64
  } from "./api.js";
59
65
 
@@ -80,6 +86,11 @@ export interface ConsoleActions {
80
86
  /** Sets the pack token of the project the page is on, or takes it away; for an owner (ADR-0028). */
81
87
  setPackToken(token: string): Promise<RestProject>;
82
88
  removePackToken(): Promise<RestProject>;
89
+ /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
90
+ connectPackToken(): Promise<RestPackConnectStarted>;
91
+ /** How that connection stands; once connected, the project is the one answered. */
92
+ packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
93
+ cancelPackTokenConnection(): Promise<void>;
83
94
  addMember(input: RestMemberInput): Promise<RestMember>;
84
95
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
85
96
  removeMember(personId: string): Promise<void>;
@@ -97,6 +108,18 @@ export interface ConsoleActions {
97
108
  page: { readonly limit: number; readonly offset: number },
98
109
  signal?: AbortSignal,
99
110
  ): Promise<RestTasks>;
111
+ /** What words find among the current tasks of the project, a page at a time, for a picker (ADR-0037); the first page of them for no words. */
112
+ findTasks(
113
+ query: string,
114
+ page: { readonly limit: number; readonly offset: number },
115
+ signal?: AbortSignal,
116
+ ): Promise<RestTasks>;
117
+ /** What words find among the open goals of the project, a page at a time, for a picker. */
118
+ findGoals(
119
+ query: string,
120
+ page: { readonly limit: number; readonly offset: number },
121
+ signal?: AbortSignal,
122
+ ): Promise<RestGoals>;
100
123
  raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
101
124
  answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
102
125
  /** Grows a decision's record: its context, or what followed. */
@@ -133,6 +156,16 @@ export interface ConsoleActions {
133
156
  removeListing(id: string): Promise<void>;
134
157
  /** Keeps the person's own choice of language (ADR-0012). */
135
158
  updateMe(patch: RestMePatch): Promise<RestMe>;
159
+ /** Changes the workspace's settings (ADR-0035); for an administrator. `me` says them from then on. */
160
+ updateWorkspace(patch: RestWorkspacePatch): Promise<RestWorkspace>;
161
+ /** The workspace's SkillCDN token (ADR-0036), for an administrator: set, taken away, or connected with a code. */
162
+ setWorkspacePackToken(token: string): Promise<RestWorkspace>;
163
+ removeWorkspacePackToken(): Promise<RestWorkspace>;
164
+ connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
165
+ workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
166
+ cancelWorkspacePackTokenConnection(): Promise<void>;
167
+ /** One person of the workspace by their exact login, or nobody: what an owner lists someone the People page does not show by. */
168
+ findPerson(login: string, signal?: AbortSignal): Promise<RestPerson | undefined>;
136
169
  /** Marks a run that will not come back as abandoned. */
137
170
  abandonRun(id: string): Promise<RestRun>;
138
171
  /** Everything that happened to one task of the project, oldest first. */
@@ -245,6 +278,32 @@ async function readTasks(
245
278
  return { items };
246
279
  }
247
280
 
281
+ /** How many pages of the roadmap are read before it is shown as it stands. */
282
+ const GOALS_MAX_PAGES = 20;
283
+
284
+ /**
285
+ * The goals of a project, whole: page after page while the server says there is more, since
286
+ * the roadmap, the calendar and the filters draw them all.
287
+ */
288
+ async function readGoals(
289
+ scope: ProjectClient,
290
+ signal: AbortSignal | undefined,
291
+ ): Promise<{ readonly items: RestGoal[] }> {
292
+ const items: RestGoal[] = [];
293
+ for (let page = 0; page < GOALS_MAX_PAGES; page += 1) {
294
+ const result = await scope.goals({ limit: LIST_LIMIT, offset: page * LIST_LIMIT }, signal);
295
+ items.push(...result.items);
296
+ if (!result.more) {
297
+ break;
298
+ }
299
+ }
300
+ return { items };
301
+ }
302
+
303
+ /** The words of a search as the API takes them: none for a blank. */
304
+ const searchWordsOf = (query: string): { readonly q?: string } =>
305
+ query.trim() === "" ? {} : { q: query.trim() };
306
+
248
307
  /** How many pages of the decisions are read before the record is shown as it stands. */
249
308
  const DECISIONS_MAX_PAGES = 20;
250
309
 
@@ -333,7 +392,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
333
392
  scope.get(signal),
334
393
  readTasks(scope, signal),
335
394
  readDecisions(scope, signal),
336
- scope.goals({}, signal),
395
+ readGoals(scope, signal),
337
396
  scope.runs(undefined, signal),
338
397
  scope.members(signal),
339
398
  ]);
@@ -538,6 +597,15 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
538
597
  setProject(changed);
539
598
  return changed;
540
599
  },
600
+ connectPackToken: () => scope().connectPackToken(),
601
+ async packTokenConnection(signal) {
602
+ const state = await scope().packTokenConnection(signal);
603
+ if (state.status === "connected") {
604
+ setProject(state.project);
605
+ }
606
+ return state;
607
+ },
608
+ cancelPackTokenConnection: () => scope().cancelPackTokenConnection(),
541
609
  async addMember(input) {
542
610
  const member = await scope().addMember(input);
543
611
  await refreshProject();
@@ -579,6 +647,10 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
579
647
  return archived;
580
648
  },
581
649
  archivedTasks: (page, signal) => scope().tasks({ archived: true, ...page }, signal),
650
+ findTasks: (query, page, signal) =>
651
+ scope().tasks({ archived: false, ...searchWordsOf(query), ...page }, signal),
652
+ findGoals: (query, page, signal) =>
653
+ scope().goals({ open: true, ...searchWordsOf(query), ...page }, signal),
582
654
  async raiseDecision(input) {
583
655
  const decision = await scope().raiseDecision(input);
584
656
  await refreshProject();
@@ -649,6 +721,34 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
649
721
  setMe(answer);
650
722
  return answer;
651
723
  },
724
+ async updateWorkspace(patch) {
725
+ const workspace = await client.updateWorkspace(patch);
726
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
727
+ // The member visibility may have changed: the people are read again under the new setting.
728
+ await refreshWorkspace();
729
+ return workspace;
730
+ },
731
+ async setWorkspacePackToken(token) {
732
+ const workspace = await client.setWorkspacePackToken({ token });
733
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
734
+ return workspace;
735
+ },
736
+ async removeWorkspacePackToken() {
737
+ const workspace = await client.removeWorkspacePackToken();
738
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
739
+ return workspace;
740
+ },
741
+ connectWorkspacePackToken: () => client.connectWorkspacePackToken(),
742
+ async workspacePackTokenConnection(signal) {
743
+ const state = await client.workspacePackTokenConnection(signal);
744
+ if (state.status === "connected") {
745
+ const { workspace } = state;
746
+ setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
747
+ }
748
+ return state;
749
+ },
750
+ cancelWorkspacePackTokenConnection: () => client.cancelWorkspacePackTokenConnection(),
751
+ findPerson: (login, signal) => client.findPerson(login, signal),
652
752
  async abandonRun(id) {
653
753
  const run = await scope().endRun(id, { status: "abandoned" });
654
754
  await refreshProject();
package/src/dates.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { WeekStart } from "./vocabulary.js";
2
+
1
3
  // Days, as the plan is made in (ADR-0019): `2026-10-20`, read and written as the strings they
2
4
  // are, so that a deadline is the same day wherever the people are. The browser's own clock says
3
5
  // which day is today; nothing here looks at it unless asked.
@@ -62,8 +64,27 @@ export function addMonths(month: string, count: number): string {
62
64
  return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}`;
63
65
  }
64
66
 
65
- /** The weekday a week begins on where the language is spoken: 1 for Monday, 7 for Sunday. */
66
- export function firstDayOfWeek(language: string): number {
67
+ /**
68
+ * The language and region of the device the pages are read on, as the browser says: `ko-KR`,
69
+ * `en-GB`. The fallback where there is no browser to ask. The week begins as this has it
70
+ * where the workspace leaves the choice to the system default (ADR-0035).
71
+ */
72
+ export function deviceLanguage(fallback: string): string {
73
+ const browser = (globalThis as { readonly navigator?: { readonly language?: string } }).navigator;
74
+ return browser?.language ?? fallback;
75
+ }
76
+
77
+ /**
78
+ * The weekday a week begins on: as the workspace says (ADR-0035), Monday or Sunday, else as the
79
+ * language and region have it, the device's by {@link deviceLanguage}. 1 for Monday, 7 for Sunday.
80
+ */
81
+ export function firstDayOfWeek(language: string, weekStart?: WeekStart | undefined): number {
82
+ if (weekStart === "monday") {
83
+ return 1;
84
+ }
85
+ if (weekStart === "sunday") {
86
+ return 7;
87
+ }
67
88
  try {
68
89
  const locale = new Intl.Locale(language) as Intl.Locale & {
69
90
  readonly weekInfo?: { readonly firstDay: number };