@skillcdn/console 0.3.1 → 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.
- package/README.md +2 -2
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -3
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +105 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +9 -7
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +13 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +13 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -2
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +6 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +13 -7
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +36 -24
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -5
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +11 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +5 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +163 -11
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +24 -4
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +11 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +22 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +11 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +16 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +17 -9
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/schemas.d.ts +12 -11
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +11 -8
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -0
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +2 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +9 -9
- package/docs/specs/rest.md +14 -14
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +3 -3
- package/skills/working-the-board/references/terms.md +2 -2
- package/src/api.ts +5 -0
- package/src/cli/cli.ts +131 -18
- package/src/cli/format.ts +11 -7
- package/src/cli/help.ts +15 -11
- package/src/cli/main.ts +1 -0
- package/src/client.ts +23 -10
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +10 -4
- package/src/components/calendar.tsx +68 -32
- package/src/components/picker.tsx +297 -0
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +2 -2
- package/src/components/task-view.tsx +23 -9
- package/src/components/ui.tsx +25 -0
- package/src/components/work.tsx +6 -4
- package/src/console.tsx +29 -3
- package/src/data.ts +44 -1
- package/src/i18n/en.ts +16 -8
- package/src/i18n/ko.ts +17 -9
- package/src/index.ts +16 -1
- package/src/limits.ts +2 -0
- package/src/schemas.ts +11 -7
- package/src/styles/console.css +163 -11
- package/src/vocabulary.ts +3 -0
package/src/components/ui.tsx
CHANGED
|
@@ -145,6 +145,31 @@ export function PersonChip(props: { readonly person: RestPerson; readonly size?:
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** How many of a task's workers are shown as pictures before the rest are a count. */
|
|
149
|
+
const WORKERS_SHOWN = 3;
|
|
150
|
+
|
|
151
|
+
/** The people at work on a task (ADR-0037), as pictures side by side with a count of the rest; nothing while nobody is. */
|
|
152
|
+
export function Workers(props: {
|
|
153
|
+
readonly workers: readonly RestPerson[];
|
|
154
|
+
readonly size?: "xs" | "sm";
|
|
155
|
+
/** How many are shown as pictures; the rest are a count. */
|
|
156
|
+
readonly shown?: number;
|
|
157
|
+
}) {
|
|
158
|
+
if (props.workers.length === 0) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const shown = props.shown ?? WORKERS_SHOWN;
|
|
162
|
+
const rest = props.workers.length - shown;
|
|
163
|
+
return (
|
|
164
|
+
<span className="sc-workers" title={props.workers.map(personName).join(", ")}>
|
|
165
|
+
{props.workers.slice(0, shown).map((worker) => (
|
|
166
|
+
<Avatar key={worker.id} person={worker} size={props.size ?? "sm"} />
|
|
167
|
+
))}
|
|
168
|
+
{rest > 0 && <span className="sc-workers-more">+{rest}</span>}
|
|
169
|
+
</span>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
/** The words of the vocabulary as people read them, in English: the default pack's. */
|
|
149
174
|
export const STATE_LABELS: Readonly<Record<TaskState, string>> = {
|
|
150
175
|
idea: "Idea",
|
package/src/components/work.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { RestTask, RestWork, WeekStart } from "../api.js";
|
|
2
|
-
import {
|
|
3
|
-
import { useWords } from "../i18n/index.js";
|
|
2
|
+
import { deviceLanguage, firstDayOfWeek, today as todayOf, weekOf } from "../dates.js";
|
|
3
|
+
import { useLanguage, useWords } from "../i18n/index.js";
|
|
4
4
|
import { Week } from "./calendar.js";
|
|
5
5
|
import { ProjectMark } from "./projects.js";
|
|
6
6
|
import { TaskList } from "./task-list.js";
|
|
7
7
|
import { PersonChip, Time } from "./ui.js";
|
|
8
8
|
|
|
9
9
|
// What is on a person's plate (ADR-0019), across the projects they may see: the week at the
|
|
10
|
-
// top, with the open tasks
|
|
10
|
+
// top, with the open tasks they work on on its days, and what of theirs was done or
|
|
11
11
|
// dropped on them, as a project's calendar shows it; below, the open tasks by when they are
|
|
12
12
|
// due, and the decisions that wait for a person. Takes its data as props and nothing from the
|
|
13
13
|
// network; the week shown is the address's (ADR-0010).
|
|
@@ -35,8 +35,10 @@ export interface WorkListProps {
|
|
|
35
35
|
export function WorkList(props: WorkListProps) {
|
|
36
36
|
const words = useWords();
|
|
37
37
|
const work = words.work;
|
|
38
|
+
const { tag } = useLanguage();
|
|
38
39
|
const now = props.today ?? todayOf();
|
|
39
|
-
|
|
40
|
+
// This week ends with the week's last day, as the workspace begins its weeks (ADR-0039).
|
|
41
|
+
const weekEnd = weekOf(now, firstDayOfWeek(deviceLanguage(tag), props.weekStart)).at(-1) ?? now;
|
|
40
42
|
const { tasks, decisions } = props.work;
|
|
41
43
|
const closed = props.work.closed ?? [];
|
|
42
44
|
const projectOf = new Map([...tasks, ...closed].map((item) => [item.task.id, item.project]));
|
package/src/console.tsx
CHANGED
|
@@ -600,6 +600,28 @@ function TaskPage(props: {
|
|
|
600
600
|
(fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
|
|
601
601
|
? fetched
|
|
602
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);
|
|
603
625
|
const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
|
|
604
626
|
const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
|
|
605
627
|
const { taskHistory, taskComments } = data.actions;
|
|
@@ -653,6 +675,9 @@ function TaskPage(props: {
|
|
|
653
675
|
task={task}
|
|
654
676
|
people={data.people.filter(inWorkspace)}
|
|
655
677
|
tasks={data.tasks}
|
|
678
|
+
parent={parent}
|
|
679
|
+
findTasks={data.actions.findTasks}
|
|
680
|
+
findGoals={data.actions.findGoals}
|
|
656
681
|
decisions={data.decisions}
|
|
657
682
|
runs={data.runs}
|
|
658
683
|
comments={comments}
|
|
@@ -805,9 +830,9 @@ function ProjectPage(props: {
|
|
|
805
830
|
<section className="sc-panel sc-panel-form" aria-label={words.task.newAria}>
|
|
806
831
|
<TaskForm
|
|
807
832
|
people={data.people.filter(inWorkspace)}
|
|
808
|
-
|
|
833
|
+
findTasks={data.actions.findTasks}
|
|
809
834
|
categories={data.categories}
|
|
810
|
-
|
|
835
|
+
findGoals={data.actions.findGoals}
|
|
811
836
|
busy={busy}
|
|
812
837
|
onSubmit={(input: RestTaskInput) =>
|
|
813
838
|
void act(async () => {
|
|
@@ -907,6 +932,7 @@ function ProjectPage(props: {
|
|
|
907
932
|
<Workload
|
|
908
933
|
tasks={shown}
|
|
909
934
|
today={now}
|
|
935
|
+
weekStart={weekStartOf(data.me?.workspace)}
|
|
910
936
|
personHref={(person) =>
|
|
911
937
|
boardHref(key, person === undefined ? {} : { person: person.id })
|
|
912
938
|
}
|
|
@@ -1796,7 +1822,7 @@ function WorkspaceSettingsPage(props: {
|
|
|
1796
1822
|
}
|
|
1797
1823
|
|
|
1798
1824
|
/** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
|
|
1799
|
-
/** Whether a person is in: who a picker of
|
|
1825
|
+
/** Whether a person is in: who a picker of workers and of members offers (ADR-0022). */
|
|
1800
1826
|
const inWorkspace = (person: { readonly status: "active" | "deactivated" }): boolean =>
|
|
1801
1827
|
person.status === "active";
|
|
1802
1828
|
|
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,
|
|
@@ -107,6 +108,18 @@ export interface ConsoleActions {
|
|
|
107
108
|
page: { readonly limit: number; readonly offset: number },
|
|
108
109
|
signal?: AbortSignal,
|
|
109
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>;
|
|
110
123
|
raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
|
|
111
124
|
answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
|
|
112
125
|
/** Grows a decision's record: its context, or what followed. */
|
|
@@ -265,6 +278,32 @@ async function readTasks(
|
|
|
265
278
|
return { items };
|
|
266
279
|
}
|
|
267
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
|
+
|
|
268
307
|
/** How many pages of the decisions are read before the record is shown as it stands. */
|
|
269
308
|
const DECISIONS_MAX_PAGES = 20;
|
|
270
309
|
|
|
@@ -353,7 +392,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
353
392
|
scope.get(signal),
|
|
354
393
|
readTasks(scope, signal),
|
|
355
394
|
readDecisions(scope, signal),
|
|
356
|
-
scope
|
|
395
|
+
readGoals(scope, signal),
|
|
357
396
|
scope.runs(undefined, signal),
|
|
358
397
|
scope.members(signal),
|
|
359
398
|
]);
|
|
@@ -608,6 +647,10 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
608
647
|
return archived;
|
|
609
648
|
},
|
|
610
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),
|
|
611
654
|
async raiseDecision(input) {
|
|
612
655
|
const decision = await scope().raiseDecision(input);
|
|
613
656
|
await refreshProject();
|
package/src/i18n/en.ts
CHANGED
|
@@ -121,6 +121,11 @@ export const en = {
|
|
|
121
121
|
next: "Next page",
|
|
122
122
|
range: (from: number, to: number, of: number) => `${from}-${to} of ${of}`,
|
|
123
123
|
},
|
|
124
|
+
picker: {
|
|
125
|
+
nothingFound: "Nothing matches",
|
|
126
|
+
searching: "Searching",
|
|
127
|
+
more: "Show more",
|
|
128
|
+
},
|
|
124
129
|
},
|
|
125
130
|
|
|
126
131
|
nav: {
|
|
@@ -304,7 +309,7 @@ export const en = {
|
|
|
304
309
|
},
|
|
305
310
|
partOf: "Part of",
|
|
306
311
|
owner: "Owner",
|
|
307
|
-
|
|
312
|
+
workers: "Workers",
|
|
308
313
|
written: "Written",
|
|
309
314
|
changed: "Changed",
|
|
310
315
|
state: "State",
|
|
@@ -338,8 +343,11 @@ export const en = {
|
|
|
338
343
|
bodyPlaceholder: "What is to be done, in Markdown.",
|
|
339
344
|
state: "State",
|
|
340
345
|
priority: "Priority",
|
|
341
|
-
|
|
346
|
+
workers: "Workers",
|
|
347
|
+
addWorker: "Add a worker",
|
|
342
348
|
partOf: "Part of",
|
|
349
|
+
searchTasks: "Search by number or title",
|
|
350
|
+
searchGoals: "Search by title",
|
|
343
351
|
category: "Category",
|
|
344
352
|
noCategory: "No category",
|
|
345
353
|
goal: "Goal",
|
|
@@ -425,7 +433,7 @@ export const en = {
|
|
|
425
433
|
person: "Person",
|
|
426
434
|
open: "Open",
|
|
427
435
|
inProgress: "In progress",
|
|
428
|
-
|
|
436
|
+
dueThisWeek: "Due this week",
|
|
429
437
|
overdue: "Overdue",
|
|
430
438
|
unassigned: "Nobody's yet",
|
|
431
439
|
},
|
|
@@ -497,8 +505,8 @@ export const en = {
|
|
|
497
505
|
loading: "Loading your work",
|
|
498
506
|
couldNotLoad: "Your work could not be loaded.",
|
|
499
507
|
empty: {
|
|
500
|
-
title: "Nothing is
|
|
501
|
-
body: "A task
|
|
508
|
+
title: "Nothing is on your plate",
|
|
509
|
+
body: "A task you work on, in any project you may see, shows up here.",
|
|
502
510
|
},
|
|
503
511
|
overdue: "Overdue",
|
|
504
512
|
thisWeek: "Due this week",
|
|
@@ -609,7 +617,7 @@ export const en = {
|
|
|
609
617
|
title: "the title",
|
|
610
618
|
body: "the body",
|
|
611
619
|
priority: "the priority",
|
|
612
|
-
|
|
620
|
+
workers: "the workers",
|
|
613
621
|
parentId: "what it is part of",
|
|
614
622
|
links: "the links",
|
|
615
623
|
name: "the name",
|
|
@@ -861,8 +869,8 @@ export const en = {
|
|
|
861
869
|
loadingWork: "Loading the work",
|
|
862
870
|
couldNotLoadWork: "The work could not be loaded.",
|
|
863
871
|
noWork: {
|
|
864
|
-
title: (name: string) => `Nothing is
|
|
865
|
-
body: "An open task
|
|
872
|
+
title: (name: string) => `Nothing is on ${name}'s plate`,
|
|
873
|
+
body: "An open task they work on, in a project you may see, shows up here.",
|
|
866
874
|
},
|
|
867
875
|
notOverseen: {
|
|
868
876
|
title: "Not yours to see",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -83,6 +83,11 @@ export const ko: Messages = {
|
|
|
83
83
|
next: "다음",
|
|
84
84
|
range: (from, to, of) => `${of}건 중 ${from}-${to}`,
|
|
85
85
|
},
|
|
86
|
+
picker: {
|
|
87
|
+
nothingFound: "맞는 것이 없습니다",
|
|
88
|
+
searching: "검색 중",
|
|
89
|
+
more: "더 보기",
|
|
90
|
+
},
|
|
86
91
|
},
|
|
87
92
|
|
|
88
93
|
nav: {
|
|
@@ -264,7 +269,7 @@ export const ko: Messages = {
|
|
|
264
269
|
},
|
|
265
270
|
partOf: "상위 작업:",
|
|
266
271
|
owner: "담당자",
|
|
267
|
-
|
|
272
|
+
workers: "작업자",
|
|
268
273
|
written: "작성",
|
|
269
274
|
changed: "변경",
|
|
270
275
|
state: "상태",
|
|
@@ -298,8 +303,11 @@ export const ko: Messages = {
|
|
|
298
303
|
bodyPlaceholder: "무엇을 할지, Markdown으로.",
|
|
299
304
|
state: "상태",
|
|
300
305
|
priority: "우선순위",
|
|
301
|
-
|
|
306
|
+
workers: "작업자",
|
|
307
|
+
addWorker: "작업자 추가",
|
|
302
308
|
partOf: "상위 작업",
|
|
309
|
+
searchTasks: "번호나 제목으로 검색",
|
|
310
|
+
searchGoals: "제목으로 검색",
|
|
303
311
|
category: "카테고리",
|
|
304
312
|
noCategory: "카테고리 없음",
|
|
305
313
|
goal: "목표",
|
|
@@ -385,9 +393,9 @@ export const ko: Messages = {
|
|
|
385
393
|
person: "사람",
|
|
386
394
|
open: "열림",
|
|
387
395
|
inProgress: "진행 중",
|
|
388
|
-
|
|
396
|
+
dueThisWeek: "이번 주 마감",
|
|
389
397
|
overdue: "기한 지남",
|
|
390
|
-
unassigned: "아직
|
|
398
|
+
unassigned: "아직 작업자 없음",
|
|
391
399
|
},
|
|
392
400
|
},
|
|
393
401
|
|
|
@@ -456,8 +464,8 @@ export const ko: Messages = {
|
|
|
456
464
|
loading: "내 작업을 불러오는 중",
|
|
457
465
|
couldNotLoad: "내 작업을 불러올 수 없습니다.",
|
|
458
466
|
empty: {
|
|
459
|
-
title: "
|
|
460
|
-
body: "볼 수 있는 어느 프로젝트에서든
|
|
467
|
+
title: "내가 맡은 작업이 없습니다",
|
|
468
|
+
body: "볼 수 있는 어느 프로젝트에서든 내가 작업자인 작업이 여기에 나타납니다.",
|
|
461
469
|
},
|
|
462
470
|
overdue: "기한 지남",
|
|
463
471
|
thisWeek: "이번 주 마감",
|
|
@@ -567,7 +575,7 @@ export const ko: Messages = {
|
|
|
567
575
|
title: "제목",
|
|
568
576
|
body: "본문",
|
|
569
577
|
priority: "우선순위",
|
|
570
|
-
|
|
578
|
+
workers: "작업자",
|
|
571
579
|
parentId: "상위 작업",
|
|
572
580
|
links: "링크",
|
|
573
581
|
name: "이름",
|
|
@@ -816,8 +824,8 @@ export const ko: Messages = {
|
|
|
816
824
|
loadingWork: "작업을 불러오는 중",
|
|
817
825
|
couldNotLoadWork: "작업을 불러올 수 없습니다.",
|
|
818
826
|
noWork: {
|
|
819
|
-
title: (name) => `${name}에게
|
|
820
|
-
body: "내가 볼 수 있는 프로젝트에서 그
|
|
827
|
+
title: (name) => `${name}에게 맡겨진 작업이 없습니다`,
|
|
828
|
+
body: "내가 볼 수 있는 프로젝트에서 그 사람이 작업자인 열린 작업이 여기에 나타납니다.",
|
|
821
829
|
},
|
|
822
830
|
notOverseen: {
|
|
823
831
|
title: "볼 수 없는 작업입니다",
|
package/src/index.ts
CHANGED
|
@@ -102,6 +102,13 @@ export {
|
|
|
102
102
|
PeopleList,
|
|
103
103
|
type PeopleListProps,
|
|
104
104
|
} from "./components/people.js";
|
|
105
|
+
export {
|
|
106
|
+
PICKER_PAGE_LIMIT,
|
|
107
|
+
Picker,
|
|
108
|
+
type PickerPage,
|
|
109
|
+
type PickerProps,
|
|
110
|
+
type PickerSearch,
|
|
111
|
+
} from "./components/picker.js";
|
|
105
112
|
export {
|
|
106
113
|
clampScale,
|
|
107
114
|
FITTED,
|
|
@@ -168,7 +175,14 @@ export {
|
|
|
168
175
|
} from "./components/shell.js";
|
|
169
176
|
export { SignIn, type SignInProps } from "./components/sign-in.js";
|
|
170
177
|
export { SkillList, type SkillListProps } from "./components/skills.js";
|
|
171
|
-
export {
|
|
178
|
+
export {
|
|
179
|
+
type GoalSearch,
|
|
180
|
+
goalLabel,
|
|
181
|
+
TaskForm,
|
|
182
|
+
type TaskFormProps,
|
|
183
|
+
type TaskSearch,
|
|
184
|
+
taskLabel,
|
|
185
|
+
} from "./components/task-form.js";
|
|
172
186
|
export { TaskList, type TaskListProps } from "./components/task-list.js";
|
|
173
187
|
export { TaskView, type TaskViewProps } from "./components/task-view.js";
|
|
174
188
|
export {
|
|
@@ -204,6 +218,7 @@ export {
|
|
|
204
218
|
STATE_LABELS,
|
|
205
219
|
StateBadge,
|
|
206
220
|
Time,
|
|
221
|
+
Workers,
|
|
207
222
|
} from "./components/ui.js";
|
|
208
223
|
export { WorkList, type WorkListProps } from "./components/work.js";
|
|
209
224
|
export {
|
package/src/limits.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const MAX_QUESTION_LENGTH = 500;
|
|
|
9
9
|
export const MAX_BODY_LENGTH = 20_000;
|
|
10
10
|
/** The links of a task: a repository, a pull request, a document. */
|
|
11
11
|
export const MAX_LINKS = 20;
|
|
12
|
+
/** How many people may be at work on one task at a time (ADR-0037): a crew, not a mailing list. */
|
|
13
|
+
export const MAX_WORKERS = 20;
|
|
12
14
|
export const MAX_LINK_LABEL_LENGTH = 120;
|
|
13
15
|
export const MAX_URL_LENGTH = 2048;
|
|
14
16
|
/** The options of a decision, and how a person words each. */
|
package/src/schemas.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
MAX_TOKEN_DAYS,
|
|
40
40
|
MAX_TOKEN_NAME_LENGTH,
|
|
41
41
|
MAX_URL_LENGTH,
|
|
42
|
+
MAX_WORKERS,
|
|
42
43
|
MIN_OPTIONS,
|
|
43
44
|
} from "./limits.js";
|
|
44
45
|
import { hasForbiddenCodePoint } from "./text.js";
|
|
@@ -480,8 +481,10 @@ export const restTaskSchema = z.object({
|
|
|
480
481
|
body: z.string(),
|
|
481
482
|
state: z.enum(TASK_STATES),
|
|
482
483
|
priority: z.enum(TASK_PRIORITIES),
|
|
484
|
+
/** Whose task it is: one person, whoever wrote it. */
|
|
483
485
|
owner: restPersonSchema,
|
|
484
|
-
|
|
486
|
+
/** The people at work on it (ADR-0037), in the order they came to it; `[]` while nobody is. */
|
|
487
|
+
workers: z.array(restPersonSchema),
|
|
485
488
|
/** The task this one is part of, or `null`. */
|
|
486
489
|
parentId: z.nullable(uuid),
|
|
487
490
|
/** The category the task is sorted into, as the workspace keeps it, or `null`. */
|
|
@@ -530,7 +533,8 @@ export const restTaskInputSchema = z.object({
|
|
|
530
533
|
body: z.optional(body),
|
|
531
534
|
state: z.optional(z.enum(TASK_STATES)),
|
|
532
535
|
priority: z.optional(z.enum(TASK_PRIORITIES)),
|
|
533
|
-
|
|
536
|
+
/** The people to be at work on it, each one who may work in the project, in place of those there were; `[]` for nobody. */
|
|
537
|
+
workerIds: z.optional(z.array(uuid).check(z.maxLength(MAX_WORKERS))),
|
|
534
538
|
parentId: z.optional(z.nullable(uuid)),
|
|
535
539
|
/** The key of a category of the workspace, or `null` for none. */
|
|
536
540
|
category: z.optional(z.nullable(categoryKey)),
|
|
@@ -549,7 +553,7 @@ export const restTaskPatchSchema = z.object({
|
|
|
549
553
|
body: z.optional(body),
|
|
550
554
|
state: z.optional(z.enum(TASK_STATES)),
|
|
551
555
|
priority: z.optional(z.enum(TASK_PRIORITIES)),
|
|
552
|
-
|
|
556
|
+
workerIds: z.optional(z.array(uuid).check(z.maxLength(MAX_WORKERS))),
|
|
553
557
|
parentId: z.optional(z.nullable(uuid)),
|
|
554
558
|
category: z.optional(z.nullable(categoryKey)),
|
|
555
559
|
goalId: z.optional(z.nullable(uuid)),
|
|
@@ -596,8 +600,8 @@ export const restGoalSchema = z.object({
|
|
|
596
600
|
});
|
|
597
601
|
export type RestGoal = z.infer<typeof restGoalSchema>;
|
|
598
602
|
|
|
599
|
-
/** `GET /api/v1/projects/<key>/goals`: the roadmap, the active goals first. */
|
|
600
|
-
export const restGoalsSchema = z.object({ items: z.array(restGoalSchema) });
|
|
603
|
+
/** `GET /api/v1/projects/<key>/goals`: a page of the roadmap, the active goals first, and whether there is more after it. */
|
|
604
|
+
export const restGoalsSchema = z.object({ items: z.array(restGoalSchema), more: z.boolean() });
|
|
601
605
|
export type RestGoals = z.infer<typeof restGoalsSchema>;
|
|
602
606
|
|
|
603
607
|
/** What `POST /api/v1/projects/<key>/goals` is sent: the title, and the rest with defaults. */
|
|
@@ -686,8 +690,8 @@ const projectRef = z.object({
|
|
|
686
690
|
|
|
687
691
|
/**
|
|
688
692
|
* `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
|
|
689
|
-
* the open tasks
|
|
690
|
-
* project; and, for the week on the plate, `closed`, the tasks
|
|
693
|
+
* the open tasks they work on, and the decisions that wait for a person, each with its
|
|
694
|
+
* project; and, for the week on the plate, `closed`, the tasks they work on that are done
|
|
691
695
|
* or dropped and not yet put away. Left out by a console older than it.
|
|
692
696
|
*/
|
|
693
697
|
export const restWorkSchema = z.object({
|
package/src/styles/console.css
CHANGED
|
@@ -1238,6 +1238,150 @@ table {
|
|
|
1238
1238
|
justify-self: start;
|
|
1239
1239
|
}
|
|
1240
1240
|
|
|
1241
|
+
/* The workers of a task on its form: each a chip with the way to take them off, and the way to add one. */
|
|
1242
|
+
.sc-chips {
|
|
1243
|
+
display: flex;
|
|
1244
|
+
flex-wrap: wrap;
|
|
1245
|
+
align-items: center;
|
|
1246
|
+
gap: var(--sc-space-2);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.sc-chip {
|
|
1250
|
+
display: inline-flex;
|
|
1251
|
+
align-items: center;
|
|
1252
|
+
gap: var(--sc-space-1);
|
|
1253
|
+
min-height: var(--sc-control-size);
|
|
1254
|
+
padding: 0 var(--sc-space-1) 0 var(--sc-space-2);
|
|
1255
|
+
border: var(--sc-border);
|
|
1256
|
+
border-radius: var(--sc-radius-md);
|
|
1257
|
+
background: var(--sc-color-bg-inset);
|
|
1258
|
+
font-size: var(--sc-text-sm);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.sc-chip-remove,
|
|
1262
|
+
.sc-picker-clear {
|
|
1263
|
+
display: inline-grid;
|
|
1264
|
+
flex: none;
|
|
1265
|
+
place-items: center;
|
|
1266
|
+
width: 1.5rem;
|
|
1267
|
+
height: 1.5rem;
|
|
1268
|
+
border: 0;
|
|
1269
|
+
border-radius: var(--sc-radius-sm);
|
|
1270
|
+
background: transparent;
|
|
1271
|
+
color: var(--sc-color-text-muted);
|
|
1272
|
+
font-size: var(--sc-text-md);
|
|
1273
|
+
line-height: 1;
|
|
1274
|
+
cursor: pointer;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.sc-chip-remove:hover:not(:disabled),
|
|
1278
|
+
.sc-picker-clear:hover:not(:disabled) {
|
|
1279
|
+
background: var(--sc-color-bg-raised);
|
|
1280
|
+
color: var(--sc-color-text);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
.sc-chip-remove:focus-visible,
|
|
1284
|
+
.sc-picker-clear:focus-visible {
|
|
1285
|
+
outline: var(--sc-focus-ring);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.sc-chips .sc-chip-add {
|
|
1289
|
+
width: auto;
|
|
1290
|
+
min-width: 10rem;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/* A picker: the field that searches a listing, its page of results under it, and the choice in its place. */
|
|
1294
|
+
.sc-picker {
|
|
1295
|
+
position: relative;
|
|
1296
|
+
min-width: 0;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.sc-picker-chosen {
|
|
1300
|
+
display: flex;
|
|
1301
|
+
align-items: center;
|
|
1302
|
+
gap: var(--sc-space-2);
|
|
1303
|
+
min-height: var(--sc-control-size);
|
|
1304
|
+
padding: var(--sc-space-1) var(--sc-space-1) var(--sc-space-1) var(--sc-space-3);
|
|
1305
|
+
border: 1px solid var(--sc-color-border-strong);
|
|
1306
|
+
border-radius: var(--sc-radius-md);
|
|
1307
|
+
background: var(--sc-color-field);
|
|
1308
|
+
font-size: var(--sc-text-sm);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.sc-picker-value {
|
|
1312
|
+
flex: 1;
|
|
1313
|
+
min-width: 0;
|
|
1314
|
+
overflow: hidden;
|
|
1315
|
+
text-overflow: ellipsis;
|
|
1316
|
+
white-space: nowrap;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.sc-picker-list {
|
|
1320
|
+
position: absolute;
|
|
1321
|
+
z-index: 40;
|
|
1322
|
+
top: calc(100% + var(--sc-space-1));
|
|
1323
|
+
right: 0;
|
|
1324
|
+
left: 0;
|
|
1325
|
+
max-height: 18rem;
|
|
1326
|
+
margin: 0;
|
|
1327
|
+
padding: var(--sc-space-1);
|
|
1328
|
+
overflow-y: auto;
|
|
1329
|
+
list-style: none;
|
|
1330
|
+
border: var(--sc-border);
|
|
1331
|
+
border-radius: var(--sc-radius-md);
|
|
1332
|
+
background: var(--sc-color-bg-inset);
|
|
1333
|
+
box-shadow: var(--sc-shadow-stage);
|
|
1334
|
+
font-size: var(--sc-text-sm);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/* Where the window has no room below the field, and more above, the list stands above it. */
|
|
1338
|
+
.sc-picker-list-above {
|
|
1339
|
+
top: auto;
|
|
1340
|
+
bottom: calc(100% + var(--sc-space-1));
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.sc-picker-option {
|
|
1344
|
+
padding: var(--sc-space-2);
|
|
1345
|
+
overflow: hidden;
|
|
1346
|
+
border-radius: var(--sc-radius-sm);
|
|
1347
|
+
text-overflow: ellipsis;
|
|
1348
|
+
white-space: nowrap;
|
|
1349
|
+
cursor: pointer;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.sc-picker-option-active {
|
|
1353
|
+
background: var(--sc-color-bg-raised);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.sc-picker-note {
|
|
1357
|
+
padding: var(--sc-space-2);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/* The workers of a task on a card or a row: pictures side by side, and how many more. */
|
|
1361
|
+
.sc-workers {
|
|
1362
|
+
display: inline-flex;
|
|
1363
|
+
align-items: center;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.sc-workers > * + * {
|
|
1367
|
+
margin-left: -0.375rem;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
.sc-workers-more {
|
|
1371
|
+
margin-inline: var(--sc-space-1);
|
|
1372
|
+
color: var(--sc-color-text-muted);
|
|
1373
|
+
font-size: var(--sc-text-xs);
|
|
1374
|
+
font-weight: var(--sc-weight-medium);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.sc-worker-list {
|
|
1378
|
+
display: grid;
|
|
1379
|
+
gap: var(--sc-space-1);
|
|
1380
|
+
margin: 0;
|
|
1381
|
+
padding: 0;
|
|
1382
|
+
list-style: none;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1241
1385
|
.sc-form-actions {
|
|
1242
1386
|
display: flex;
|
|
1243
1387
|
flex-wrap: wrap;
|
|
@@ -2906,8 +3050,10 @@ table {
|
|
|
2906
3050
|
white-space: nowrap;
|
|
2907
3051
|
}
|
|
2908
3052
|
|
|
2909
|
-
/*
|
|
2910
|
-
|
|
3053
|
+
/* The task under the pointer is lit a little by its colour, in every piece of its bar and in
|
|
3054
|
+
every week it crosses, never one piece alone; by its colour and not by a filter, which would
|
|
3055
|
+
light the card within it too. */
|
|
3056
|
+
.sc-calendar-bar-pointed {
|
|
2911
3057
|
background: color-mix(in srgb, var(--sc-category-fill), white 12%);
|
|
2912
3058
|
}
|
|
2913
3059
|
|
|
@@ -2925,6 +3071,18 @@ table {
|
|
|
2925
3071
|
border-end-end-radius: var(--sc-radius-full);
|
|
2926
3072
|
}
|
|
2927
3073
|
|
|
3074
|
+
/* The title of a bar with more days in the week runs along the bar as far as its days reach,
|
|
3075
|
+
less the room of its end, instead of stopping at the first day's edge: above every piece,
|
|
3076
|
+
the one under the pointer included, and under the cards; the pointer goes through it to the
|
|
3077
|
+
piece of the day beneath. The days are the first piece's own custom property. */
|
|
3078
|
+
.sc-calendar-task-title-spanning {
|
|
3079
|
+
position: relative;
|
|
3080
|
+
z-index: 25;
|
|
3081
|
+
flex: none;
|
|
3082
|
+
max-width: calc(var(--sc-span, 1) * 100% - 2.25rem);
|
|
3083
|
+
pointer-events: none;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
2928
3086
|
.sc-calendar-bar.sc-calendar-late {
|
|
2929
3087
|
border-color: color-mix(in srgb, var(--sc-color-danger) 70%, transparent);
|
|
2930
3088
|
color: var(--sc-color-danger);
|
|
@@ -3365,18 +3523,12 @@ table {
|
|
|
3365
3523
|
position: relative;
|
|
3366
3524
|
}
|
|
3367
3525
|
|
|
3368
|
-
/* The
|
|
3369
|
-
.
|
|
3370
|
-
.sc-calendar-task:focus-visible,
|
|
3371
|
-
.sc-calendar-bar:hover,
|
|
3372
|
-
.sc-calendar-bar:focus-visible {
|
|
3373
|
-
z-index: 20;
|
|
3374
|
-
}
|
|
3375
|
-
|
|
3526
|
+
/* The card rises above its neighbours and above a title running along a bar; the item under the
|
|
3527
|
+
pointer itself stays in its layer, so that nothing of it covers such a title. */
|
|
3376
3528
|
.sc-task-tip {
|
|
3377
3529
|
position: absolute;
|
|
3378
3530
|
left: 0;
|
|
3379
|
-
z-index:
|
|
3531
|
+
z-index: 30;
|
|
3380
3532
|
width: max-content;
|
|
3381
3533
|
max-width: 20rem;
|
|
3382
3534
|
padding: var(--sc-space-2) var(--sc-space-3);
|
package/src/vocabulary.ts
CHANGED
|
@@ -30,6 +30,9 @@ export const GOAL_STATES = ["planned", "active", "done", "dropped"] as const;
|
|
|
30
30
|
|
|
31
31
|
export type GoalState = (typeof GOAL_STATES)[number];
|
|
32
32
|
|
|
33
|
+
/** The states a goal is still open in: planned, or being worked towards. */
|
|
34
|
+
export const OPEN_GOAL_STATES: readonly GoalState[] = ["planned", "active"];
|
|
35
|
+
|
|
33
36
|
/**
|
|
34
37
|
* The colours a category, or a project's mark, may be shown in: names, so that a custom console
|
|
35
38
|
* paints them in its own tokens and no value a person typed reaches a stylesheet.
|