@skillcdn/console 0.1.24 → 0.3.1

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 (156) hide show
  1. package/README.md +8 -7
  2. package/dist/api.d.ts +2 -2
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +2 -2
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +1 -1
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +248 -92
  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 +13 -28
  19. package/dist/cli/format.d.ts.map +1 -1
  20. package/dist/cli/format.js +74 -274
  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 +36 -19
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/pack.d.ts +46 -0
  26. package/dist/cli/pack.d.ts.map +1 -0
  27. package/dist/cli/pack.js +250 -0
  28. package/dist/cli/pack.js.map +1 -0
  29. package/dist/client.d.ts +26 -1
  30. package/dist/client.d.ts.map +1 -1
  31. package/dist/client.js +14 -1
  32. package/dist/client.js.map +1 -1
  33. package/dist/compat.d.ts +1 -1
  34. package/dist/compat.js +1 -1
  35. package/dist/components/calendar.d.ts +5 -1
  36. package/dist/components/calendar.d.ts.map +1 -1
  37. package/dist/components/calendar.js +3 -3
  38. package/dist/components/calendar.js.map +1 -1
  39. package/dist/components/decision-list.d.ts +13 -1
  40. package/dist/components/decision-list.d.ts.map +1 -1
  41. package/dist/components/decision-list.js +74 -29
  42. package/dist/components/decision-list.js.map +1 -1
  43. package/dist/components/event-feed.d.ts.map +1 -1
  44. package/dist/components/event-feed.js +2 -0
  45. package/dist/components/event-feed.js.map +1 -1
  46. package/dist/components/pack-connect.d.ts +23 -0
  47. package/dist/components/pack-connect.d.ts.map +1 -0
  48. package/dist/components/pack-connect.js +86 -0
  49. package/dist/components/pack-connect.js.map +1 -0
  50. package/dist/components/pack-token.d.ts +27 -0
  51. package/dist/components/pack-token.d.ts.map +1 -0
  52. package/dist/components/pack-token.js +11 -0
  53. package/dist/components/pack-token.js.map +1 -0
  54. package/dist/components/paged.d.ts +24 -0
  55. package/dist/components/paged.d.ts.map +1 -1
  56. package/dist/components/paged.js +40 -4
  57. package/dist/components/paged.js.map +1 -1
  58. package/dist/components/projects.d.ts +13 -0
  59. package/dist/components/projects.d.ts.map +1 -1
  60. package/dist/components/projects.js +37 -5
  61. package/dist/components/projects.js.map +1 -1
  62. package/dist/components/skills.d.ts.map +1 -1
  63. package/dist/components/skills.js +8 -4
  64. package/dist/components/skills.js.map +1 -1
  65. package/dist/components/ui.d.ts +2 -0
  66. package/dist/components/ui.d.ts.map +1 -1
  67. package/dist/components/ui.js +7 -2
  68. package/dist/components/ui.js.map +1 -1
  69. package/dist/components/work.d.ts +3 -1
  70. package/dist/components/work.d.ts.map +1 -1
  71. package/dist/components/work.js +1 -1
  72. package/dist/components/work.js.map +1 -1
  73. package/dist/components/workspace.d.ts +46 -0
  74. package/dist/components/workspace.d.ts.map +1 -0
  75. package/dist/components/workspace.js +124 -0
  76. package/dist/components/workspace.js.map +1 -0
  77. package/dist/console.css +268 -5
  78. package/dist/console.d.ts.map +1 -1
  79. package/dist/console.js +62 -23
  80. package/dist/console.js.map +1 -1
  81. package/dist/data.d.ts +16 -1
  82. package/dist/data.d.ts.map +1 -1
  83. package/dist/data.js +64 -2
  84. package/dist/data.js.map +1 -1
  85. package/dist/dates.d.ts +12 -2
  86. package/dist/dates.d.ts.map +1 -1
  87. package/dist/dates.js +20 -2
  88. package/dist/dates.js.map +1 -1
  89. package/dist/i18n/en.d.ts +66 -0
  90. package/dist/i18n/en.d.ts.map +1 -1
  91. package/dist/i18n/en.js +80 -8
  92. package/dist/i18n/en.js.map +1 -1
  93. package/dist/i18n/ko.d.ts.map +1 -1
  94. package/dist/i18n/ko.js +75 -7
  95. package/dist/i18n/ko.js.map +1 -1
  96. package/dist/index.d.ts +5 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +5 -2
  99. package/dist/index.js.map +1 -1
  100. package/dist/router.d.ts +10 -5
  101. package/dist/router.d.ts.map +1 -1
  102. package/dist/router.js +24 -9
  103. package/dist/router.js.map +1 -1
  104. package/dist/routes.d.ts +4 -0
  105. package/dist/routes.d.ts.map +1 -1
  106. package/dist/routes.js +4 -0
  107. package/dist/routes.js.map +1 -1
  108. package/dist/schemas.d.ts +500 -158
  109. package/dist/schemas.d.ts.map +1 -1
  110. package/dist/schemas.js +169 -165
  111. package/dist/schemas.js.map +1 -1
  112. package/dist/vocabulary.d.ts +19 -1
  113. package/dist/vocabulary.d.ts.map +1 -1
  114. package/dist/vocabulary.js +16 -0
  115. package/dist/vocabulary.js.map +1 -1
  116. package/docs/specs/cli.md +19 -14
  117. package/docs/specs/rest.md +34 -19
  118. package/package.json +2 -1
  119. package/skills/building-a-console/SKILL.md +5 -5
  120. package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
  121. package/skills/deploying-a-console/SKILL.md +9 -8
  122. package/skills/working-the-board/SKILL.md +5 -5
  123. package/skills/working-the-board/references/setting-up.md +1 -1
  124. package/skills/working-the-board/references/terms.md +2 -0
  125. package/skills/working-the-board/references/the-sentence.md +1 -1
  126. package/src/api.ts +18 -7
  127. package/src/cli/cli.ts +345 -109
  128. package/src/cli/credentials.ts +7 -0
  129. package/src/cli/directory.ts +9 -0
  130. package/src/cli/format.ts +82 -341
  131. package/src/cli/help.ts +43 -19
  132. package/src/cli/pack.ts +356 -0
  133. package/src/client.ts +84 -0
  134. package/src/compat.ts +1 -1
  135. package/src/components/calendar.tsx +8 -2
  136. package/src/components/decision-list.tsx +227 -94
  137. package/src/components/event-feed.tsx +2 -0
  138. package/src/components/pack-connect.tsx +179 -0
  139. package/src/components/pack-token.tsx +74 -0
  140. package/src/components/paged.tsx +72 -3
  141. package/src/components/projects.tsx +108 -9
  142. package/src/components/skills.tsx +51 -33
  143. package/src/components/ui.tsx +16 -3
  144. package/src/components/work.tsx +4 -1
  145. package/src/components/workspace.tsx +298 -0
  146. package/src/console.tsx +176 -29
  147. package/src/data.ts +89 -2
  148. package/src/dates.ts +23 -2
  149. package/src/i18n/en.ts +94 -8
  150. package/src/i18n/ko.ts +84 -7
  151. package/src/index.ts +25 -1
  152. package/src/router.ts +37 -14
  153. package/src/routes.ts +4 -0
  154. package/src/schemas.ts +202 -189
  155. package/src/styles/console.css +268 -5
  156. package/src/vocabulary.ts +25 -0
package/src/client.ts CHANGED
@@ -44,6 +44,8 @@ import {
44
44
  type RestMemberPatch,
45
45
  type RestMembers,
46
46
  type RestMePatch,
47
+ type RestPackConnect,
48
+ type RestPackConnectStarted,
47
49
  type RestPackTokenInput,
48
50
  type RestPeople,
49
51
  type RestPerson,
@@ -76,6 +78,9 @@ import {
76
78
  type RestVersion,
77
79
  type RestVersions,
78
80
  type RestWork,
81
+ type RestWorkspace,
82
+ type RestWorkspacePackConnect,
83
+ type RestWorkspacePatch,
79
84
  restArchivedTasksSchema,
80
85
  restCategoriesSchema,
81
86
  restCategorySchema,
@@ -96,6 +101,8 @@ import {
96
101
  restMemberSchema,
97
102
  restMembersSchema,
98
103
  restMeSchema,
104
+ restPackConnectSchema,
105
+ restPackConnectStartedSchema,
99
106
  restPeopleSchema,
100
107
  restPersonSchema,
101
108
  restPersonWorkSchema,
@@ -117,6 +124,8 @@ import {
117
124
  restVersionSchema,
118
125
  restVersionsSchema,
119
126
  restWorkSchema,
127
+ restWorkspacePackConnectSchema,
128
+ restWorkspaceSchema,
120
129
  } from "./schemas.js";
121
130
  import type { GoalState, TaskState } from "./vocabulary.js";
122
131
 
@@ -345,6 +354,12 @@ export interface ProjectClient {
345
354
  setPackToken(input: RestPackTokenInput): Promise<RestProject>;
346
355
  /** Takes the pack token away; for an owner. */
347
356
  removePackToken(): Promise<RestProject>;
357
+ /** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
358
+ connectPackToken(): Promise<RestPackConnectStarted>;
359
+ /** How that connection stands; the token is kept when the person approved. For an owner. */
360
+ packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
361
+ /** Gives the connection up; for an owner. */
362
+ cancelPackTokenConnection(): Promise<void>;
348
363
  /** The rules that hold in the project (ADR-0023): the workspace's, and what the project adds. */
349
364
  rules(signal?: AbortSignal): Promise<RestProjectRules>;
350
365
  /** A folder's pages and folders, or the pages a search finds. */
@@ -380,7 +395,26 @@ export interface ConsoleClient {
380
395
  giveUpToken(): Promise<void>;
381
396
  /** Keeps the person's choice of language, or none; for a person signed in. */
382
397
  updateMe(patch: RestMePatch): Promise<RestMe>;
398
+ /** Changes the workspace's settings (ADR-0035); for an administrator, with a session or a token. */
399
+ updateWorkspace(patch: RestWorkspacePatch): Promise<RestWorkspace>;
400
+ /**
401
+ * The workspace's SkillCDN token (ADR-0036), which reads the organization's pack for every
402
+ * project that names no address and holds no token of its own; an administrator's. Set from
403
+ * a token made by hand, taken away, or connected with a code a person approves at SkillCDN,
404
+ * asked after until the token is kept or the request ended.
405
+ */
406
+ setWorkspacePackToken(input: RestPackTokenInput): Promise<RestWorkspace>;
407
+ removeWorkspacePackToken(): Promise<RestWorkspace>;
408
+ connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
409
+ workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
410
+ cancelWorkspacePackTokenConnection(): Promise<void>;
411
+ /** The people the asker may see: everyone, or those they share a project with (ADR-0035). */
383
412
  people(signal?: AbortSignal): Promise<RestPeople>;
413
+ /**
414
+ * One person of the workspace by their exact login, compared without regard to case, or
415
+ * nobody: what an owner lists someone by when the People page does not show them.
416
+ */
417
+ findPerson(login: string, signal?: AbortSignal): Promise<RestPerson | undefined>;
384
418
  /** Changes what a person is, or whether they are in; for an administrator. */
385
419
  updatePerson(id: string, patch: RestPersonPatch): Promise<RestPerson>;
386
420
  /** Lets a login in (ADR-0022): whoever signs in with that account becomes a person; for an administrator. */
@@ -766,6 +800,23 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
766
800
  request("PUT", `${base}${at()}/pack-token`, input, restProjectSchema),
767
801
  removePackToken: () =>
768
802
  request("DELETE", `${base}${at()}/pack-token`, undefined, restProjectSchema),
803
+ connectPackToken: () =>
804
+ request(
805
+ "POST",
806
+ `${base}${at()}/pack-token/connect`,
807
+ undefined,
808
+ restPackConnectStartedSchema,
809
+ ),
810
+ packTokenConnection: (signal) =>
811
+ request(
812
+ "GET",
813
+ `${base}${at()}/pack-token/connect`,
814
+ undefined,
815
+ restPackConnectSchema,
816
+ signal,
817
+ ),
818
+ cancelPackTokenConnection: () =>
819
+ request("DELETE", `${base}${at()}/pack-token/connect`, undefined, undefined),
769
820
  rules: (signal) =>
770
821
  request("GET", `${base}${at("rules")}`, undefined, restProjectRulesSchema, signal),
771
822
  documents: (filter = {}, signal) =>
@@ -829,6 +880,39 @@ export function createClient(options: ClientOptions = {}): ConsoleClient {
829
880
  giveUpToken: () => request("DELETE", `${base}${REST_ROUTES.meToken}`, undefined, undefined),
830
881
  people: (signal) =>
831
882
  request("GET", `${base}${REST_ROUTES.people}`, undefined, restPeopleSchema, signal),
883
+ findPerson: async (login, signal) => {
884
+ const found = await request(
885
+ "GET",
886
+ withQuery(REST_ROUTES.people, { login }),
887
+ undefined,
888
+ restPeopleSchema,
889
+ signal,
890
+ );
891
+ return found.items[0];
892
+ },
893
+ updateWorkspace: (patch) =>
894
+ request("PATCH", `${base}${REST_ROUTES.workspace}`, patch, restWorkspaceSchema),
895
+ setWorkspacePackToken: (input) =>
896
+ request("PUT", `${base}${REST_ROUTES.workspacePackToken}`, input, restWorkspaceSchema),
897
+ removeWorkspacePackToken: () =>
898
+ request("DELETE", `${base}${REST_ROUTES.workspacePackToken}`, undefined, restWorkspaceSchema),
899
+ connectWorkspacePackToken: () =>
900
+ request(
901
+ "POST",
902
+ `${base}${REST_ROUTES.workspacePackToken}/connect`,
903
+ undefined,
904
+ restPackConnectStartedSchema,
905
+ ),
906
+ workspacePackTokenConnection: (signal) =>
907
+ request(
908
+ "GET",
909
+ `${base}${REST_ROUTES.workspacePackToken}/connect`,
910
+ undefined,
911
+ restWorkspacePackConnectSchema,
912
+ signal,
913
+ ),
914
+ cancelWorkspacePackTokenConnection: () =>
915
+ request("DELETE", `${base}${REST_ROUTES.workspacePackToken}/connect`, undefined, undefined),
832
916
  updatePerson: (id, patch) =>
833
917
  request("PATCH", `${base}${restPath("people", id)}`, patch, restPersonSchema),
834
918
  createListing: (input) =>
package/src/compat.ts CHANGED
@@ -75,7 +75,7 @@ export function inRange(version: Version, range: VersionRange): boolean {
75
75
 
76
76
  /**
77
77
  * The common rule. Before 1.0, a command from the console's own version on and within the
78
- * console's minor (`^0.1.18`): any release may change the contract until then (ADR-0007), so the
78
+ * console's minor (`^0.1.18`): any release may change the contract until then (ADR-0034), so the
79
79
  * console's own version is the floor, and the minor is the line. From 1.0, the console's major
80
80
  * and a minor within one of the console's, either way, whatever the patch: a minor adds and a
81
81
  * major breaks, as the version promises from then on.
@@ -5,11 +5,13 @@ import {
5
5
  type RestGoal,
6
6
  type RestPerson,
7
7
  type RestTask,
8
+ type WeekStart,
8
9
  } from "../api.js";
9
10
  import {
10
11
  addDays,
11
12
  addMonths,
12
13
  daysBetween,
14
+ deviceLanguage,
13
15
  firstDayOfWeek,
14
16
  formatDay,
15
17
  formatDays,
@@ -55,6 +57,8 @@ export interface CalendarProps {
55
57
  readonly goalHref?: ((goal: RestGoal) => string) | undefined;
56
58
  /** Where the calendar is on another month. */
57
59
  readonly monthHref: (month: string) => string;
60
+ /** The day the weeks begin on, as the workspace says (ADR-0035); the language's when left out. */
61
+ readonly weekStart?: WeekStart | undefined;
58
62
  }
59
63
 
60
64
  /** One task's stretch across one week: which columns it covers, and in which lane it lies. */
@@ -469,7 +473,7 @@ export function Calendar(props: CalendarProps) {
469
473
  const { tag } = useLanguage();
470
474
  const [pointed, point, leave] = usePointed();
471
475
  const now = props.today ?? todayOf();
472
- const firstDay = firstDayOfWeek(tag);
476
+ const firstDay = firstDayOfWeek(deviceLanguage(tag), props.weekStart);
473
477
  const weeks = monthGrid(props.month, firstDay);
474
478
  const on = onDays(props.tasks);
475
479
  const goalsOn = props.goalHref === undefined ? undefined : goalsOnDays(props.goals ?? []);
@@ -567,6 +571,8 @@ export interface WeekProps {
567
571
  readonly weekHref: (day: string) => string;
568
572
  /** What stands before a task's title where the tasks are of several projects: the project's mark. */
569
573
  readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
574
+ /** The day the week begins on, as the workspace says (ADR-0035); the language's when left out. */
575
+ readonly weekStart?: WeekStart | undefined;
570
576
  }
571
577
 
572
578
  /** One week on its own: the days as columns, the tasks on them, and the weeks around a step away. */
@@ -575,7 +581,7 @@ export function Week(props: WeekProps) {
575
581
  const { tag } = useLanguage();
576
582
  const [pointed, point, leave] = usePointed();
577
583
  const now = props.today ?? todayOf();
578
- const firstDay = firstDayOfWeek(tag);
584
+ const firstDay = firstDayOfWeek(deviceLanguage(tag), props.weekStart);
579
585
  const days = weekOf(props.day ?? now, firstDay);
580
586
  const first = days[0] ?? now;
581
587
  const last = days[days.length - 1] ?? now;
@@ -3,6 +3,7 @@ import {
3
3
  agentLabel,
4
4
  MAX_BODY_LENGTH,
5
5
  MAX_NOTE_LENGTH,
6
+ MAX_OPTION_LABEL_LENGTH,
6
7
  type RestAnswerInput,
7
8
  type RestDecision,
8
9
  type RestDecisionPatch,
@@ -10,8 +11,8 @@ import {
10
11
  } from "../api.js";
11
12
  import { useWords } from "../i18n/index.js";
12
13
  import { Markdown } from "./markdown.js";
13
- import { ShowMore, usePaged } from "./paged.js";
14
- import { Button, cx, PersonChip, Time } from "./ui.js";
14
+ import { Pager, pageOf, RECORD_PAGE_SIZE } from "./paged.js";
15
+ import { Badge, Button, cx, PersonChip, Time } from "./ui.js";
15
16
 
16
17
  // The decisions: the ones that wait, each a card with the way to answer it, since they are what
17
18
  // a person came for; and the ones answered, the record, a row each, since they are read back
@@ -35,6 +36,131 @@ export interface DecisionCardProps {
35
36
  readonly busy?: boolean | undefined;
36
37
  }
37
38
 
39
+ /** Who asked, and when: the person, through their agent when a run asked. */
40
+ function Asked(props: { readonly decision: RestDecision; readonly day?: boolean }) {
41
+ const words = useWords();
42
+ const { decision } = props;
43
+ return (
44
+ <>
45
+ <PersonChip person={decision.raisedBy} />{" "}
46
+ {decision.run === null
47
+ ? words.decisions.asked
48
+ : words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
49
+ <Time iso={decision.createdAt} precision={props.day === true ? "day" : "minute"} />
50
+ </>
51
+ );
52
+ }
53
+
54
+ /** What was chosen: the option's label, marked when it is the person's own words. */
55
+ function Chosen(props: { readonly decision: RestDecision }) {
56
+ const words = useWords();
57
+ const { decision } = props;
58
+ const chosen = decision.options.find((option) => option.id === decision.answer?.option);
59
+ return (
60
+ <>
61
+ <span>{chosen?.label ?? decision.answer?.option ?? ""}</span>
62
+ {chosen?.own === true && <Badge>{words.decisions.ownAnswer}</Badge>}
63
+ </>
64
+ );
65
+ }
66
+
67
+ /**
68
+ * The way to answer: one of the options, or a line of one's own, which is chosen by writing
69
+ * on it; and a word about either.
70
+ */
71
+ function AnswerForm(props: {
72
+ readonly decision: RestDecision;
73
+ readonly onAnswer: (decision: RestDecision, input: RestAnswerInput) => void;
74
+ readonly busy: boolean | undefined;
75
+ }) {
76
+ const words = useWords();
77
+ const { decision } = props;
78
+ const [chosen, setChosen] = useState<string | undefined>(undefined);
79
+ const [own, setOwn] = useState(false);
80
+ const [ownText, setOwnText] = useState("");
81
+ const [note, setNote] = useState("");
82
+ const chooseOwn = () => {
83
+ setOwn(true);
84
+ setChosen(undefined);
85
+ };
86
+ const answer: RestAnswerInput | undefined = own
87
+ ? ownText.trim().length > 0
88
+ ? { own: ownText.trim() }
89
+ : undefined
90
+ : chosen === undefined
91
+ ? undefined
92
+ : { option: chosen };
93
+ const name = `decision-${decision.id}`;
94
+ return (
95
+ <form
96
+ className="sc-answer-form"
97
+ onSubmit={(event) => {
98
+ event.preventDefault();
99
+ if (answer !== undefined) {
100
+ props.onAnswer(decision, {
101
+ ...answer,
102
+ ...(note.trim() === "" ? {} : { note: note.trim() }),
103
+ });
104
+ }
105
+ }}
106
+ >
107
+ <div className="sc-options-list" role="radiogroup" aria-label={words.decisions.options}>
108
+ {decision.options.map((option) => (
109
+ <label
110
+ key={option.id}
111
+ className={cx("sc-option", chosen === option.id && "sc-option-chosen")}
112
+ >
113
+ <input
114
+ type="radio"
115
+ name={name}
116
+ value={option.id}
117
+ checked={chosen === option.id}
118
+ onChange={() => {
119
+ setChosen(option.id);
120
+ setOwn(false);
121
+ }}
122
+ />
123
+ <span>{option.label}</span>
124
+ </label>
125
+ ))}
126
+ <label className={cx("sc-option", "sc-option-own", own && "sc-option-chosen")}>
127
+ <input type="radio" name={name} value="own" checked={own} onChange={chooseOwn} />
128
+ <span>{words.decisions.own}</span>
129
+ <input
130
+ className="sc-input"
131
+ aria-label={words.decisions.own}
132
+ placeholder={words.decisions.ownPlaceholder}
133
+ maxLength={MAX_OPTION_LABEL_LENGTH}
134
+ value={ownText}
135
+ onFocus={chooseOwn}
136
+ onChange={(event) => {
137
+ setOwnText(event.target.value);
138
+ chooseOwn();
139
+ }}
140
+ />
141
+ </label>
142
+ </div>
143
+ <input
144
+ className="sc-input"
145
+ placeholder={words.decisions.notePlaceholder}
146
+ maxLength={MAX_NOTE_LENGTH}
147
+ value={note}
148
+ onChange={(event) => setNote(event.target.value)}
149
+ />
150
+ <div className="sc-form-actions">
151
+ <Button
152
+ type="submit"
153
+ variant="primary"
154
+ size="sm"
155
+ disabled={answer === undefined || props.busy === true}
156
+ >
157
+ {words.decisions.answer}
158
+ </Button>
159
+ </div>
160
+ </form>
161
+ );
162
+ }
163
+
38
164
  /** What followed the decision: shown, and written or changed by whoever may. */
39
165
  function Outcome(props: {
40
166
  readonly decision: RestDecision;
@@ -109,11 +235,7 @@ function Outcome(props: {
109
235
  export function DecisionCard(props: DecisionCardProps) {
110
236
  const words = useWords();
111
237
  const { decision, task } = props;
112
- const [chosen, setChosen] = useState<string | undefined>(undefined);
113
- const [note, setNote] = useState("");
114
238
  const answered = decision.answer !== null;
115
- const chosenLabel = (id: string) =>
116
- decision.options.find((option) => option.id === id)?.label ?? id;
117
239
 
118
240
  return (
119
241
  <article id={decision.id} className={cx("sc-decision", answered && "sc-decision-answered")}>
@@ -126,11 +248,7 @@ export function DecisionCard(props: DecisionCardProps) {
126
248
  )}
127
249
  </h3>
128
250
  <p className="sc-decision-meta">
129
- <PersonChip person={decision.raisedBy} />{" "}
130
- {decision.run === null
131
- ? words.decisions.asked
132
- : words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
133
- <Time iso={decision.createdAt} />
251
+ <Asked decision={decision} />
134
252
  {task !== undefined && (
135
253
  <>
136
254
  {" "}
@@ -156,7 +274,9 @@ export function DecisionCard(props: DecisionCardProps) {
156
274
  )}
157
275
  {decision.answer !== null ? (
158
276
  <div className="sc-answer">
159
- <p className="sc-answer-option">{chosenLabel(decision.answer.option)}</p>
277
+ <p className="sc-answer-option">
278
+ <Chosen decision={decision} />
279
+ </p>
160
280
  {decision.answer.note !== null && (
161
281
  <div className="sc-answer-note">
162
282
  <Markdown source={decision.answer.note} docHref={props.docHref} />
@@ -170,53 +290,7 @@ export function DecisionCard(props: DecisionCardProps) {
170
290
  ) : props.onAnswer === undefined ? (
171
291
  <p className="sc-decision-waiting">{words.decisions.waitingForPerson}</p>
172
292
  ) : (
173
- <form
174
- className="sc-answer-form"
175
- onSubmit={(event) => {
176
- event.preventDefault();
177
- if (chosen !== undefined) {
178
- props.onAnswer?.(decision, {
179
- option: chosen,
180
- ...(note.trim() === "" ? {} : { note: note.trim() }),
181
- });
182
- }
183
- }}
184
- >
185
- <div className="sc-options-list" role="radiogroup" aria-label={words.decisions.options}>
186
- {decision.options.map((option) => (
187
- <label
188
- key={option.id}
189
- className={cx("sc-option", chosen === option.id && "sc-option-chosen")}
190
- >
191
- <input
192
- type="radio"
193
- name={`decision-${decision.id}`}
194
- value={option.id}
195
- checked={chosen === option.id}
196
- onChange={() => setChosen(option.id)}
197
- />
198
- <span>{option.label}</span>
199
- </label>
200
- ))}
201
- </div>
202
- <input
203
- className="sc-input"
204
- placeholder={words.decisions.notePlaceholder}
205
- maxLength={MAX_NOTE_LENGTH}
206
- value={note}
207
- onChange={(event) => setNote(event.target.value)}
208
- />
209
- <div className="sc-form-actions">
210
- <Button
211
- type="submit"
212
- variant="primary"
213
- size="sm"
214
- disabled={chosen === undefined || props.busy === true}
215
- >
216
- {words.decisions.answer}
217
- </Button>
218
- </div>
219
- </form>
293
+ <AnswerForm decision={decision} onAnswer={props.onAnswer} busy={props.busy} />
220
294
  )}
221
295
  {answered && (
222
296
  <Outcome
@@ -238,6 +312,9 @@ export interface DecisionListProps {
238
312
  readonly docHref?: ((path: string) => string) | undefined;
239
313
  /** Where each decision's own page is. */
240
314
  readonly decisionHref?: ((decision: RestDecision) => string) | undefined;
315
+ /** The page of the record shown, from 1, and where each page is (ADR-0010); left out, the record turns its pages in place. */
316
+ readonly page?: number | undefined;
317
+ readonly pageHref?: ((page: number) => string) | undefined;
241
318
  readonly onAnswer?: ((decision: RestDecision, input: RestAnswerInput) => void) | undefined;
242
319
  readonly onUpdate?: ((decision: RestDecision, patch: RestDecisionPatch) => void) | undefined;
243
320
  readonly busy?: boolean | undefined;
@@ -251,48 +328,101 @@ export interface DecisionLogProps {
251
328
  readonly taskHref?: ((task: RestTask) => string) | undefined;
252
329
  /** Where each decision's own page is, with its context, its rationale and what followed. */
253
330
  readonly decisionHref?: ((decision: RestDecision) => string) | undefined;
331
+ /** Where a document the word given with an answer links to is read, by its path. */
332
+ readonly docHref?: ((path: string) => string) | undefined;
333
+ /** The page shown, from 1; the first when left out, the last for one past the end. Read with `pageHref`. */
334
+ readonly page?: number | undefined;
335
+ /** Where each page of the record is, so that a page has an address; left out, the pages turn in place. */
336
+ readonly pageHref?: ((page: number) => string) | undefined;
254
337
  }
255
338
 
256
- /** The record: one row per decision answered, what was asked, what was chosen, by whom, when. */
339
+ /** One row of the record: what was asked, by whom and when; what was answered, with the word given, by whom and when. */
340
+ function RecordRow(props: {
341
+ readonly decision: RestDecision;
342
+ readonly task: RestTask | undefined;
343
+ readonly taskHref: ((task: RestTask) => string) | undefined;
344
+ readonly href: string | undefined;
345
+ readonly docHref: ((path: string) => string) | undefined;
346
+ }) {
347
+ const words = useWords();
348
+ const { decision, task } = props;
349
+ const { answer } = decision;
350
+ return (
351
+ <li className="sc-decision-record-row">
352
+ <div className="sc-decision-record-asked">
353
+ <p className="sc-decision-record-question">
354
+ {props.href === undefined ? (
355
+ decision.question
356
+ ) : (
357
+ <a href={props.href}>{decision.question}</a>
358
+ )}
359
+ {task !== undefined &&
360
+ (props.taskHref === undefined ? (
361
+ <span className="sc-card-number">#{task.number}</span>
362
+ ) : (
363
+ <a className="sc-card-number" href={props.taskHref(task)}>
364
+ #{task.number}
365
+ </a>
366
+ ))}
367
+ </p>
368
+ <p className="sc-decision-record-meta">
369
+ <Asked decision={decision} day />
370
+ </p>
371
+ </div>
372
+ {answer !== null && (
373
+ <div className="sc-decision-record-answer">
374
+ <p className="sc-decision-record-chosen">
375
+ <Chosen decision={decision} />
376
+ </p>
377
+ {answer.note !== null && (
378
+ <div className="sc-decision-record-note">
379
+ <Markdown source={answer.note} docHref={props.docHref} />
380
+ </div>
381
+ )}
382
+ <p className="sc-decision-record-meta">
383
+ <PersonChip person={answer.by} /> {words.decisions.answeredBy}{" "}
384
+ <Time iso={answer.at} precision="day" />
385
+ </p>
386
+ </div>
387
+ )}
388
+ </li>
389
+ );
390
+ }
391
+
392
+ /**
393
+ * The record: one row per decision answered, what was asked and by whom, what was chosen with
394
+ * the word given and by whom; twenty to a page, the page named by the address or turned here.
395
+ */
257
396
  export function DecisionLog(props: DecisionLogProps) {
258
397
  const words = useWords();
259
- const { shown, rest, more } = usePaged(props.decisions);
398
+ const [turned, setTurned] = useState(1);
399
+ const { shown, page, pages } = pageOf(
400
+ props.decisions,
401
+ props.pageHref === undefined ? turned : props.page,
402
+ RECORD_PAGE_SIZE,
403
+ );
260
404
  return (
261
405
  <>
262
- <ol className="sc-decision-log" aria-label={words.decisions.record}>
263
- {shown.map((decision) => {
264
- const task = decision.taskId === null ? undefined : props.tasks?.get(decision.taskId);
265
- const href = props.decisionHref?.(decision);
266
- const chosen =
267
- decision.options.find((option) => option.id === decision.answer?.option)?.label ??
268
- decision.answer?.option ??
269
- "";
270
- return (
271
- <li key={decision.id} className="sc-decision-row">
272
- <span className="sc-decision-row-question">
273
- {href === undefined ? decision.question : <a href={href}>{decision.question}</a>}
274
- </span>
275
- <span className="sc-decision-row-answer">{chosen}</span>
276
- <span className="sc-decision-row-meta">
277
- {decision.answer !== null && (
278
- <>
279
- <PersonChip person={decision.answer.by} /> <Time iso={decision.answer.at} />
280
- </>
281
- )}
282
- {task !== undefined &&
283
- (props.taskHref === undefined ? (
284
- <span className="sc-card-number">#{task.number}</span>
285
- ) : (
286
- <a className="sc-card-number" href={props.taskHref(task)}>
287
- #{task.number}
288
- </a>
289
- ))}
290
- </span>
291
- </li>
292
- );
293
- })}
406
+ <ol className="sc-decision-record" aria-label={words.decisions.record}>
407
+ {shown.map((decision) => (
408
+ <RecordRow
409
+ key={decision.id}
410
+ decision={decision}
411
+ task={decision.taskId === null ? undefined : props.tasks?.get(decision.taskId)}
412
+ taskHref={props.taskHref}
413
+ href={props.decisionHref?.(decision)}
414
+ docHref={props.docHref}
415
+ />
416
+ ))}
294
417
  </ol>
295
- <ShowMore rest={rest} onMore={more} />
418
+ <Pager
419
+ page={page}
420
+ pages={pages}
421
+ total={props.decisions.length}
422
+ pageSize={RECORD_PAGE_SIZE}
423
+ pageHref={props.pageHref}
424
+ onPage={setTurned}
425
+ />
296
426
  </>
297
427
  );
298
428
  }
@@ -332,6 +462,9 @@ export function DecisionList(props: DecisionListProps) {
332
462
  tasks={props.tasks}
333
463
  taskHref={props.taskHref}
334
464
  decisionHref={props.decisionHref}
465
+ docHref={props.docHref}
466
+ page={props.page}
467
+ pageHref={props.pageHref}
335
468
  />
336
469
  </section>
337
470
  )}
@@ -46,6 +46,8 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
46
46
  return feed.event.personDeactivated(login);
47
47
  case "person.reactivated":
48
48
  return feed.event.personReactivated(login);
49
+ case "workspace.updated":
50
+ return feed.event.workspaceUpdated(fields);
49
51
  case "project.created":
50
52
  return feed.event.projectCreated(project);
51
53
  case "project.updated":