@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.
- package/README.md +8 -7
- package/dist/api.d.ts +2 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +248 -92
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +5 -0
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +2 -0
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +7 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +7 -0
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +13 -28
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +74 -274
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +36 -19
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/pack.d.ts +46 -0
- package/dist/cli/pack.d.ts.map +1 -0
- package/dist/cli/pack.js +250 -0
- package/dist/cli/pack.js.map +1 -0
- package/dist/client.d.ts +26 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/calendar.d.ts +5 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts +13 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +74 -29
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +2 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/pack-connect.d.ts +23 -0
- package/dist/components/pack-connect.d.ts.map +1 -0
- package/dist/components/pack-connect.js +86 -0
- package/dist/components/pack-connect.js.map +1 -0
- package/dist/components/pack-token.d.ts +27 -0
- package/dist/components/pack-token.d.ts.map +1 -0
- package/dist/components/pack-token.js +11 -0
- package/dist/components/pack-token.js.map +1 -0
- package/dist/components/paged.d.ts +24 -0
- package/dist/components/paged.d.ts.map +1 -1
- package/dist/components/paged.js +40 -4
- package/dist/components/paged.js.map +1 -1
- package/dist/components/projects.d.ts +13 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -5
- package/dist/components/projects.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +8 -4
- package/dist/components/skills.js.map +1 -1
- package/dist/components/ui.d.ts +2 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +7 -2
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +3 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +1 -1
- package/dist/components/work.js.map +1 -1
- package/dist/components/workspace.d.ts +46 -0
- package/dist/components/workspace.d.ts.map +1 -0
- package/dist/components/workspace.js +124 -0
- package/dist/components/workspace.js.map +1 -0
- package/dist/console.css +268 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +62 -23
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +16 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +64 -2
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +12 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +20 -2
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +66 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +80 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +75 -7
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -9
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +4 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +500 -158
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +169 -165
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +19 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +19 -14
- package/docs/specs/rest.md +34 -19
- package/package.json +2 -1
- package/skills/building-a-console/SKILL.md +5 -5
- package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
- package/skills/deploying-a-console/SKILL.md +9 -8
- package/skills/working-the-board/SKILL.md +5 -5
- package/skills/working-the-board/references/setting-up.md +1 -1
- package/skills/working-the-board/references/terms.md +2 -0
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +18 -7
- package/src/cli/cli.ts +345 -109
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +82 -341
- package/src/cli/help.ts +43 -19
- package/src/cli/pack.ts +356 -0
- package/src/client.ts +84 -0
- package/src/compat.ts +1 -1
- package/src/components/calendar.tsx +8 -2
- package/src/components/decision-list.tsx +227 -94
- package/src/components/event-feed.tsx +2 -0
- package/src/components/pack-connect.tsx +179 -0
- package/src/components/pack-token.tsx +74 -0
- package/src/components/paged.tsx +72 -3
- package/src/components/projects.tsx +108 -9
- package/src/components/skills.tsx +51 -33
- package/src/components/ui.tsx +16 -3
- package/src/components/work.tsx +4 -1
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +176 -29
- package/src/data.ts +89 -2
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +94 -8
- package/src/i18n/ko.ts +84 -7
- package/src/index.ts +25 -1
- package/src/router.ts +37 -14
- package/src/routes.ts +4 -0
- package/src/schemas.ts +202 -189
- package/src/styles/console.css +268 -5
- 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-
|
|
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 {
|
|
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
|
-
<
|
|
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">
|
|
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
|
-
<
|
|
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
|
-
/**
|
|
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
|
|
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-
|
|
263
|
-
{shown.map((decision) =>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
decision.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
<
|
|
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":
|