@skillcdn/console 0.3.0 → 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 +3 -3
- 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 +185 -28
- 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 +10 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +67 -13
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +30 -13
- package/dist/cli/help.js.map +1 -1
- 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/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/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/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/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 +156 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +60 -21
- 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 +37 -0
- 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 +53 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +65 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +60 -7
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +4 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +11 -2
- 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 +182 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +110 -13
- 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 +16 -11
- package/docs/specs/rest.md +31 -8
- package/package.json +2 -2
- 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 +8 -7
- package/skills/working-the-board/SKILL.md +2 -2
- 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 +16 -0
- package/src/cli/cli.ts +253 -33
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +72 -13
- package/src/cli/help.ts +36 -13
- package/src/client.ts +84 -0
- package/src/components/calendar.tsx +8 -2
- 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/projects.tsx +108 -9
- package/src/components/work.tsx +4 -1
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +173 -29
- package/src/data.ts +57 -0
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +76 -8
- package/src/i18n/ko.ts +67 -7
- package/src/index.ts +16 -0
- package/src/router.ts +17 -5
- package/src/routes.ts +4 -0
- package/src/schemas.ts +129 -12
- package/src/styles/console.css +156 -0
- package/src/vocabulary.ts +25 -0
package/src/cli/help.ts
CHANGED
|
@@ -136,9 +136,10 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
136
136
|
},
|
|
137
137
|
use: {
|
|
138
138
|
usage: "use <key> [--url <origin>] [--account <login>]",
|
|
139
|
-
synopsis:
|
|
139
|
+
synopsis:
|
|
140
|
+
"Binds this directory to a project: writes .skillcdn-console.json here, and prints the sentence for the checkout's agreement.",
|
|
140
141
|
about:
|
|
141
|
-
"Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names.",
|
|
142
|
+
"Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed in a private repository and left out of a public one. The file names the project; and the console, where that is not the default login here or --url says so; and the account, where the console has several logins or --account says so. Never a token. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over the project it names. Then prints the sentence for the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), to be copied as it is, since nothing sends an agent to the console until the checkout says so.",
|
|
142
143
|
refusals: ["project.not_found: no project has the key, or it is not yours to see"],
|
|
143
144
|
},
|
|
144
145
|
work: {
|
|
@@ -203,9 +204,26 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
203
204
|
},
|
|
204
205
|
people: {
|
|
205
206
|
usage: "people",
|
|
206
|
-
synopsis: "
|
|
207
|
+
synopsis: "The people you see, by login, with what each is in the workspace.",
|
|
207
208
|
about:
|
|
208
|
-
"
|
|
209
|
+
"The people of the workspace you see, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Everyone, unless the workspace shows a member only the people they share a project with (console workspace says which; an administrator sees everyone). Then, where you see everyone, the logins an administrator let in that nobody has signed in with yet. The logins are what --assignee, member and person take; a login is found exactly, so an owner lists someone the list does not show.",
|
|
210
|
+
},
|
|
211
|
+
workspace: {
|
|
212
|
+
usage:
|
|
213
|
+
"workspace | workspace edit [--people-visibility <everyone|shared-projects>] [--week-start <auto|monday|sunday>] [--time-zone <zone|none>] | workspace pack-token --connect | workspace pack-token --stdin | workspace pack-token --remove",
|
|
214
|
+
overview:
|
|
215
|
+
"workspace | workspace edit [flags] | workspace pack-token --connect|--stdin|--remove",
|
|
216
|
+
synopsis:
|
|
217
|
+
"The workspace's settings; or some changed, or its SkillCDN token set, for an administrator.",
|
|
218
|
+
about:
|
|
219
|
+
"What the workspace is called; who sees whom on the People page and in the pickers (everyone; or shared-projects: a member sees only the people they share a project with, an administrator everyone; a project open to the workspace has everyone in it, so a member of one sees everyone); the day the calendars begin a week on (auto: the system default, as each device has it); the workspace's time zone, an IANA name such as Asia/Seoul, in which this command prints moments as ISO 8601 with its offset, as the login kept it at login or whoami, or none, which leaves them as the API gives them, in UTC; where the organization's skills come from; and the most days a token may last. edit changes any of the first three, for an administrator; --json leaves moments as the API gives them. pack-token is the SkillCDN token the organization's pack is read with, by every project that names no address of its own and holds no token of its own (ADR-0036), for an administrator: with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody; with --stdin, a token made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away.",
|
|
220
|
+
refusals: [
|
|
221
|
+
"auth.forbidden: only an administrator changes the workspace's settings or its token",
|
|
222
|
+
"request.invalid: the time zone is not a name the console knows",
|
|
223
|
+
"workspace.pack_token_refused: SkillCDN does not serve the organization's pack to the token, or the console names no address for the organization's skills",
|
|
224
|
+
"workspace.pack_connect_refused: the console names no address for the organization's skills, the pack is on the console's machine, or SkillCDN refused to begin",
|
|
225
|
+
"workspace.pack_token_unsupported, workspace.pack_connect_unsupported: the console keeps no secret to seal a token with (AUTH_SECRET), or reaches no deployment",
|
|
226
|
+
],
|
|
209
227
|
},
|
|
210
228
|
person: {
|
|
211
229
|
usage:
|
|
@@ -224,12 +242,13 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
224
242
|
},
|
|
225
243
|
project: {
|
|
226
244
|
usage:
|
|
227
|
-
'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --stdin | project pack-token <key> --remove',
|
|
245
|
+
'project <key> | project new <key> "<name>" [--description <markdown>|--file <path>] [--visibility workspace|private] [--skills <address>|none] [--color <name>] [--symbol <text>] | project edit <key> [--name "<name>"] [the same flags; none clears] | project icon <key> <file> | project icon <key> --remove | project pack-token <key> --connect | project pack-token <key> --stdin | project pack-token <key> --remove',
|
|
228
246
|
overview:
|
|
229
|
-
"project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --stdin|--remove",
|
|
230
|
-
synopsis:
|
|
247
|
+
"project <key> | project new <key> <name> [flags] | project edit <key> [flags] | project icon <key> <file>|--remove | project pack-token <key> --connect|--stdin|--remove",
|
|
248
|
+
synopsis:
|
|
249
|
+
"One project in full; or a project made, changed, given its mark or its SkillCDN token.",
|
|
231
250
|
about:
|
|
232
|
-
"One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its
|
|
251
|
+
"One project in full; or a project made, with you as its owner, private unless --visibility workspace says everyone of the workspace works in it; or one changed, only in what the flags say, for an owner; or its image for its mark, set from a file of this machine or taken away; or its SkillCDN token (pack-token, ADR-0028), which the console keeps sealed to read a private pack with, and which a project that reads the organization's pack may leave to the workspace's (console workspace pack-token): with --connect, the console asks SkillCDN for a code, this command prints the page and the code, a person who may open the repository approves there, signed in at SkillCDN, and the console receives the token, shown to nobody (ADR-0036); with --stdin, a token a person made by hand at SkillCDN is read from standard input and never an argument; --remove takes the token away. The organization's skills are the project's unless --skills names an address of its own.",
|
|
233
252
|
limits: [
|
|
234
253
|
`key: ${KEY}, up to ${MAX_PROJECT_KEY_LENGTH} characters; it does not change once made`,
|
|
235
254
|
`name: one line, up to ${MAX_PROJECT_NAME_LENGTH} characters; description: up to ${MAX_PROJECT_DESCRIPTION_LENGTH}`,
|
|
@@ -243,13 +262,16 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
243
262
|
"project.invalid_address: the skills address is not one as SkillCDN spells it",
|
|
244
263
|
"project.invalid_icon: the file is not an image of those kinds, by its bytes",
|
|
245
264
|
"request.too_large: the image is over the limit",
|
|
265
|
+
"project.pack_connect_unsupported: the console reaches no deployment, or keeps no secret to seal a token with",
|
|
266
|
+
"project.pack_connect_refused: the project names no pack, the pack is on the console's machine, or SkillCDN refused to begin",
|
|
267
|
+
"skills.unavailable: SkillCDN could not be reached; try again later",
|
|
246
268
|
],
|
|
247
269
|
},
|
|
248
270
|
members: {
|
|
249
271
|
usage: "members",
|
|
250
|
-
synopsis: "
|
|
272
|
+
synopsis: "The members of the project, with what each is in it.",
|
|
251
273
|
about:
|
|
252
|
-
"
|
|
274
|
+
"The members of the project, by login, with what each is in it (owner, who configures it; member, who works on it) and since when. In a private project, those listed; in one open to the workspace, everyone of it, an owner where listed as one or an administrator, a member otherwise. An administrator owns every project.",
|
|
253
275
|
},
|
|
254
276
|
member: {
|
|
255
277
|
usage:
|
|
@@ -257,11 +279,12 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
257
279
|
synopsis:
|
|
258
280
|
"Lists a person in the project, changes what they are in it, or takes them off; for an owner.",
|
|
259
281
|
about:
|
|
260
|
-
"Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
|
|
282
|
+
"Lists a person of the workspace in the project, as a member unless --role says owner; changes what they are in it; or takes them off the list. For an owner of the project. In a project open to the workspace everyone is a member already: only an owner is listed (member add --role owner, or member role <login> owner), and an owner made a member again stays the member everyone is. With --project naming several keys with commas, member add lists the person in each of them in turn, one line per project; one listed already is said so, and the rest go on.",
|
|
261
283
|
refusals: [
|
|
262
284
|
"auth.forbidden: only an owner of the project lists its members",
|
|
263
285
|
"member.invalid_person: the person is not of the workspace",
|
|
264
286
|
"member.exists: the person is listed already; member role changes what they are",
|
|
287
|
+
"member.open_project: the project is open to the workspace, so everyone is a member already; list an owner with --role owner",
|
|
265
288
|
"member.not_found: the person is not listed in the project",
|
|
266
289
|
],
|
|
267
290
|
},
|
|
@@ -328,7 +351,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
328
351
|
usage: "take <number|id> [--agent <name>]",
|
|
329
352
|
synopsis: "Takes a task: a run begins, and the task is in progress and the person's.",
|
|
330
353
|
about:
|
|
331
|
-
"Takes a task: a run begins, and the task is in progress and the person's; the
|
|
354
|
+
"Takes a task: a run begins, and the task is in progress and the person's; the run's id and the task's id are printed on a line of their own, then the task, then the rules of the board. A run still open on the task is taken over, since the board cannot know whether its agent still runs: it ends as abandoned, the feed says who took over, and that agent is refused on its next command; console task <number> shows the runs on a task, with when each was last heard from, before it is taken. --agent is what people see at work; left out, it is what the person called the token.",
|
|
332
355
|
limits: [`agent: one line, up to ${MAX_AGENT_LENGTH} characters`],
|
|
333
356
|
refusals: [
|
|
334
357
|
"task.not_found: no task has that number or id",
|
|
@@ -604,7 +627,7 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
604
627
|
{
|
|
605
628
|
title:
|
|
606
629
|
"Configuring, as the person: what an administrator or an owner does on the pages, their agent does here, named on it",
|
|
607
|
-
commands: ["people", "person", "project", "members", "member", "category"],
|
|
630
|
+
commands: ["workspace", "people", "person", "project", "members", "member", "category"],
|
|
608
631
|
},
|
|
609
632
|
];
|
|
610
633
|
|
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) =>
|
|
@@ -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;
|
|
@@ -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":
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { PackConnectEnding, RestPackConnect, RestPackConnectStarted } from "../api.js";
|
|
3
|
+
import { useWords } from "../i18n/index.js";
|
|
4
|
+
import { Button, Time } from "./ui.js";
|
|
5
|
+
|
|
6
|
+
// A private pack connected with a code a person approves (ADR-0036): an owner, or for the
|
|
7
|
+
// workspace's token an administrator, asks, the console asks the deployment and shows what it
|
|
8
|
+
// answered, the code and the page, and the person approves there; the page asks the console
|
|
9
|
+
// again at the interval the deployment named until the token is kept or the request ended.
|
|
10
|
+
// Takes its data through the callbacks and keeps nothing itself.
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How a connection stands, as the component needs it: what the API answers for a project's
|
|
14
|
+
* token and for the workspace's both say this much, with what was connected beside it.
|
|
15
|
+
*/
|
|
16
|
+
export type PackConnectState =
|
|
17
|
+
| Extract<RestPackConnect, { status: "none" | "pending" | "ended" }>
|
|
18
|
+
| { readonly status: "connected" };
|
|
19
|
+
|
|
20
|
+
export interface PackConnectProps {
|
|
21
|
+
/** Asks the deployment to connect; answers what the person is shown. */
|
|
22
|
+
readonly onStart: () => Promise<RestPackConnectStarted>;
|
|
23
|
+
/** How the connection stands; the token is kept when the person approved. */
|
|
24
|
+
readonly poll: (signal?: AbortSignal) => Promise<PackConnectState>;
|
|
25
|
+
/** Gives the connection up. */
|
|
26
|
+
readonly onCancel: () => Promise<void>;
|
|
27
|
+
/** Called once the token is kept. */
|
|
28
|
+
readonly onConnected?: (() => void) | undefined;
|
|
29
|
+
readonly busy?: boolean | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Shown =
|
|
33
|
+
| { readonly kind: "idle" }
|
|
34
|
+
| {
|
|
35
|
+
readonly kind: "waiting";
|
|
36
|
+
readonly code: string;
|
|
37
|
+
readonly url: string;
|
|
38
|
+
readonly expiresAt: string;
|
|
39
|
+
readonly intervalSeconds: number;
|
|
40
|
+
}
|
|
41
|
+
| { readonly kind: "ended"; readonly reason: PackConnectEnding }
|
|
42
|
+
| { readonly kind: "connected" };
|
|
43
|
+
|
|
44
|
+
/** The least often the console is asked again, whatever the deployment says. */
|
|
45
|
+
const MIN_INTERVAL_SECONDS = 2;
|
|
46
|
+
|
|
47
|
+
export function PackConnect(props: PackConnectProps) {
|
|
48
|
+
const words = useWords();
|
|
49
|
+
const pack = words.projects.packToken;
|
|
50
|
+
const { poll, onConnected } = props;
|
|
51
|
+
const [shown, setShown] = useState<Shown>({ kind: "idle" });
|
|
52
|
+
const [problem, setProblem] = useState<string | undefined>(undefined);
|
|
53
|
+
|
|
54
|
+
// A request begun before the page was opened is picked up where it stands.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
poll(controller.signal)
|
|
58
|
+
.then((state) => {
|
|
59
|
+
if (!controller.signal.aborted && state.status === "pending") {
|
|
60
|
+
setShown({ kind: "waiting", ...state });
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
.catch(() => undefined);
|
|
64
|
+
return () => controller.abort();
|
|
65
|
+
}, [poll]);
|
|
66
|
+
|
|
67
|
+
// While waiting, the console is asked again at the interval the deployment named; a new
|
|
68
|
+
// answer, pending still, is a new state, which schedules the next asking.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (shown.kind !== "waiting") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
poll(controller.signal)
|
|
76
|
+
.then((state) => {
|
|
77
|
+
if (controller.signal.aborted) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setProblem(undefined);
|
|
81
|
+
if (state.status === "pending") {
|
|
82
|
+
setShown({ kind: "waiting", ...state });
|
|
83
|
+
} else if (state.status === "connected") {
|
|
84
|
+
setShown({ kind: "connected" });
|
|
85
|
+
onConnected?.();
|
|
86
|
+
} else if (state.status === "ended") {
|
|
87
|
+
setShown({ kind: "ended", reason: state.reason });
|
|
88
|
+
} else {
|
|
89
|
+
setShown({ kind: "idle" });
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
.catch((failure: unknown) => {
|
|
93
|
+
if (controller.signal.aborted) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// The console could not be asked this time; it is asked again.
|
|
97
|
+
setProblem(failure instanceof Error ? failure.message : String(failure));
|
|
98
|
+
setShown((current) => (current.kind === "waiting" ? { ...current } : current));
|
|
99
|
+
});
|
|
100
|
+
}, Math.max(shown.intervalSeconds, MIN_INTERVAL_SECONDS) * 1000);
|
|
101
|
+
return () => {
|
|
102
|
+
controller.abort();
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
};
|
|
105
|
+
}, [shown, poll, onConnected]);
|
|
106
|
+
|
|
107
|
+
const start = () => {
|
|
108
|
+
setProblem(undefined);
|
|
109
|
+
props
|
|
110
|
+
.onStart()
|
|
111
|
+
.then((started) => setShown({ kind: "waiting", ...started }))
|
|
112
|
+
.catch((failure: unknown) => {
|
|
113
|
+
setProblem(failure instanceof Error ? failure.message : String(failure));
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const cancel = () => {
|
|
117
|
+
props
|
|
118
|
+
.onCancel()
|
|
119
|
+
.then(() => setShown({ kind: "idle" }))
|
|
120
|
+
.catch((failure: unknown) => {
|
|
121
|
+
setProblem(failure instanceof Error ? failure.message : String(failure));
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="sc-pack-connect" aria-live="polite">
|
|
127
|
+
{shown.kind === "waiting" ? (
|
|
128
|
+
<>
|
|
129
|
+
<p className="sc-pack-connect-code">
|
|
130
|
+
<span className="sc-field-label">{pack.connectCode}</span>
|
|
131
|
+
<code className="sc-pack-code">{shown.code}</code>
|
|
132
|
+
</p>
|
|
133
|
+
<div className="sc-form-actions">
|
|
134
|
+
<a
|
|
135
|
+
className="sc-button sc-button-primary"
|
|
136
|
+
href={shown.url}
|
|
137
|
+
target="_blank"
|
|
138
|
+
rel="noopener noreferrer"
|
|
139
|
+
>
|
|
140
|
+
{pack.connectOpen}
|
|
141
|
+
</a>
|
|
142
|
+
<Button variant="ghost" disabled={props.busy === true} onClick={cancel}>
|
|
143
|
+
{pack.connectCancel}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
<p className="sc-pack-connect-waiting">
|
|
147
|
+
<span className="sc-spinner" aria-hidden="true">
|
|
148
|
+
<span className="sc-spinner-ring" />
|
|
149
|
+
</span>
|
|
150
|
+
<span>
|
|
151
|
+
{pack.connectWaiting} {pack.connectExpires} <Time iso={shown.expiresAt} />
|
|
152
|
+
</span>
|
|
153
|
+
</p>
|
|
154
|
+
</>
|
|
155
|
+
) : shown.kind === "connected" ? (
|
|
156
|
+
<p>{pack.connectDone}</p>
|
|
157
|
+
) : (
|
|
158
|
+
<>
|
|
159
|
+
<p className="sc-field-hint">{pack.connectHint}</p>
|
|
160
|
+
{shown.kind === "ended" && (
|
|
161
|
+
<p className="sc-form-error" role="alert">
|
|
162
|
+
{pack.connectEnded[shown.reason]}
|
|
163
|
+
</p>
|
|
164
|
+
)}
|
|
165
|
+
<div className="sc-form-actions">
|
|
166
|
+
<Button variant="primary" disabled={props.busy === true} onClick={start}>
|
|
167
|
+
{pack.connect}
|
|
168
|
+
</Button>
|
|
169
|
+
</div>
|
|
170
|
+
</>
|
|
171
|
+
)}
|
|
172
|
+
{problem !== undefined && (
|
|
173
|
+
<p className="sc-form-error" role="alert">
|
|
174
|
+
{problem}
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RestPackConnectStarted } from "../api.js";
|
|
2
|
+
import { useWords } from "../i18n/index.js";
|
|
3
|
+
import { PackConnect, type PackConnectState } from "./pack-connect.js";
|
|
4
|
+
import { PackTokenForm } from "./projects.js";
|
|
5
|
+
import { Button } from "./ui.js";
|
|
6
|
+
|
|
7
|
+
// The SkillCDN token a private pack is read with (ADR-0028, ADR-0036), as a project's Settings
|
|
8
|
+
// page and the workspace's offer it: what the token is for, then the two ways to one side by
|
|
9
|
+
// side, a code approved at SkillCDN and a token made there by hand. Takes its data through the
|
|
10
|
+
// callbacks and keeps nothing itself; whose token it is, the page says in the title and the intro.
|
|
11
|
+
|
|
12
|
+
export interface PackTokenWaysProps {
|
|
13
|
+
/** What the section is called, and what assistive technology reads it as. */
|
|
14
|
+
readonly title: string;
|
|
15
|
+
/** What the token is for, in the words of the page it is on. */
|
|
16
|
+
readonly intro: string;
|
|
17
|
+
/** Whether a token is held now, and since when. */
|
|
18
|
+
readonly set: boolean;
|
|
19
|
+
readonly setAt?: string | undefined;
|
|
20
|
+
readonly busy?: boolean | undefined;
|
|
21
|
+
/** Asks the deployment to connect; answers what the person is shown. */
|
|
22
|
+
readonly onStart: () => Promise<RestPackConnectStarted>;
|
|
23
|
+
/** How the connection stands; the token is kept when the person approved. */
|
|
24
|
+
readonly poll: (signal?: AbortSignal) => Promise<PackConnectState>;
|
|
25
|
+
/** Gives the connection up. */
|
|
26
|
+
readonly onCancelConnect: () => Promise<void>;
|
|
27
|
+
/** Called once the token is kept. */
|
|
28
|
+
readonly onConnected?: (() => void) | undefined;
|
|
29
|
+
/** Called with a token made by hand, as typed or pasted. */
|
|
30
|
+
readonly onSubmit: (token: string) => void;
|
|
31
|
+
readonly onRemove?: (() => void) | undefined;
|
|
32
|
+
/** Leaves the page without changing anything. */
|
|
33
|
+
readonly onBack?: (() => void) | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function PackTokenWays(props: PackTokenWaysProps) {
|
|
37
|
+
const words = useWords();
|
|
38
|
+
const pack = words.projects.packToken;
|
|
39
|
+
return (
|
|
40
|
+
<section className="sc-panel sc-panel-form" aria-label={props.title}>
|
|
41
|
+
<h2 className="sc-section-title">{props.title}</h2>
|
|
42
|
+
<p className="sc-pack-intro">{props.intro}</p>
|
|
43
|
+
<div className="sc-pack-ways">
|
|
44
|
+
<section className="sc-pack-way" aria-label={pack.connectTitle}>
|
|
45
|
+
<h3 className="sc-pack-way-title">{pack.connectTitle}</h3>
|
|
46
|
+
<PackConnect
|
|
47
|
+
onStart={props.onStart}
|
|
48
|
+
poll={props.poll}
|
|
49
|
+
onCancel={props.onCancelConnect}
|
|
50
|
+
onConnected={props.onConnected}
|
|
51
|
+
busy={props.busy}
|
|
52
|
+
/>
|
|
53
|
+
</section>
|
|
54
|
+
<section className="sc-pack-way" aria-label={pack.pasteTitle}>
|
|
55
|
+
<h3 className="sc-pack-way-title">{pack.pasteTitle}</h3>
|
|
56
|
+
<PackTokenForm
|
|
57
|
+
set={props.set}
|
|
58
|
+
setAt={props.setAt}
|
|
59
|
+
busy={props.busy}
|
|
60
|
+
onSubmit={props.onSubmit}
|
|
61
|
+
onRemove={props.onRemove}
|
|
62
|
+
/>
|
|
63
|
+
</section>
|
|
64
|
+
</div>
|
|
65
|
+
{props.onBack !== undefined && (
|
|
66
|
+
<div className="sc-form-actions sc-pack-actions">
|
|
67
|
+
<Button variant="ghost" onClick={props.onBack}>
|
|
68
|
+
{words.common.cancel}
|
|
69
|
+
</Button>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
}
|