@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/console.tsx
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
RULES_PATH,
|
|
36
36
|
RULES_TITLE,
|
|
37
37
|
type TaskState,
|
|
38
|
+
type WeekStart,
|
|
38
39
|
} from "./api.js";
|
|
39
40
|
import { Board, BoardFilters, type BoardProps, byDue, filterTasks } from "./components/board.js";
|
|
40
41
|
import { Calendar, type CalendarProps, Workload } from "./components/calendar.js";
|
|
@@ -44,12 +45,12 @@ import { DecisionForm } from "./components/decision-form.js";
|
|
|
44
45
|
import { DecisionCard, DecisionList, type DecisionListProps } from "./components/decision-list.js";
|
|
45
46
|
import { EventFeed, type EventFeedProps } from "./components/event-feed.js";
|
|
46
47
|
import { GoalForm, GoalList, type GoalListProps, GoalView, goalMonth } from "./components/goals.js";
|
|
48
|
+
import { PackTokenWays } from "./components/pack-token.js";
|
|
47
49
|
import { PAGE_SIZE } from "./components/paged.js";
|
|
48
50
|
import { ListingForm, PeopleList, type PeopleListProps } from "./components/people.js";
|
|
49
51
|
import {
|
|
50
52
|
MemberList,
|
|
51
53
|
type MemberListProps,
|
|
52
|
-
PackTokenForm,
|
|
53
54
|
ProjectForm,
|
|
54
55
|
ProjectList,
|
|
55
56
|
type ProjectListProps,
|
|
@@ -81,6 +82,12 @@ import {
|
|
|
81
82
|
Time,
|
|
82
83
|
} from "./components/ui.js";
|
|
83
84
|
import { WorkList, type WorkListProps } from "./components/work.js";
|
|
85
|
+
import {
|
|
86
|
+
peopleVisibilityOf,
|
|
87
|
+
WorkspaceSettingsForm,
|
|
88
|
+
WorkspaceSettingsView,
|
|
89
|
+
weekStartOf,
|
|
90
|
+
} from "./components/workspace.js";
|
|
84
91
|
import { ConnectPage } from "./connect-page.js";
|
|
85
92
|
import { type ConsoleData, useConsoleData } from "./data.js";
|
|
86
93
|
import { monthOf, today } from "./dates.js";
|
|
@@ -861,6 +868,7 @@ function ProjectPage(props: {
|
|
|
861
868
|
tasks={shown}
|
|
862
869
|
month={month}
|
|
863
870
|
today={now}
|
|
871
|
+
weekStart={weekStartOf(data.me?.workspace)}
|
|
864
872
|
categories={data.categories}
|
|
865
873
|
goals={data.goals}
|
|
866
874
|
taskHref={href}
|
|
@@ -1075,6 +1083,7 @@ function ProjectPage(props: {
|
|
|
1075
1083
|
tasks={towards}
|
|
1076
1084
|
month={month}
|
|
1077
1085
|
today={now}
|
|
1086
|
+
weekStart={weekStartOf(data.me?.workspace)}
|
|
1078
1087
|
categories={data.categories}
|
|
1079
1088
|
goals={[goal]}
|
|
1080
1089
|
taskHref={href}
|
|
@@ -1220,19 +1229,34 @@ function ProjectPage(props: {
|
|
|
1220
1229
|
const me = data.me?.person ?? undefined;
|
|
1221
1230
|
const configures = owner;
|
|
1222
1231
|
const members = words.projects.members;
|
|
1232
|
+
// In a project open to the workspace everyone is a member already (ADR-0035): the list is
|
|
1233
|
+
// everyone, nobody is added or removed, and an owner is made from a row. Where the People
|
|
1234
|
+
// page shows a member only whom they share a project with, an owner lists someone the list
|
|
1235
|
+
// does not show by their login.
|
|
1236
|
+
const open = project.visibility === "workspace";
|
|
1237
|
+
const narrowed =
|
|
1238
|
+
peopleVisibilityOf(data.me?.workspace) === "shared_projects" && me?.role !== "admin";
|
|
1223
1239
|
return (
|
|
1224
1240
|
<>
|
|
1225
1241
|
<div className="sc-page-head">
|
|
1226
1242
|
<h1 className="sc-page-title">{members.title}</h1>
|
|
1227
1243
|
</div>
|
|
1228
|
-
<p className="sc-lead">{
|
|
1244
|
+
{open && <p className="sc-lead">{members.open}</p>}
|
|
1229
1245
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1230
1246
|
<components.MemberList
|
|
1231
1247
|
members={data.members}
|
|
1232
1248
|
people={data.people.filter(inWorkspace)}
|
|
1233
1249
|
me={me}
|
|
1250
|
+
open={open}
|
|
1234
1251
|
busy={busy}
|
|
1235
|
-
onAdd={
|
|
1252
|
+
onAdd={
|
|
1253
|
+
configures && !open
|
|
1254
|
+
? (input) => void act(() => data.actions.addMember(input))
|
|
1255
|
+
: undefined
|
|
1256
|
+
}
|
|
1257
|
+
lookup={
|
|
1258
|
+
configures && !open && narrowed ? (login) => data.actions.findPerson(login) : undefined
|
|
1259
|
+
}
|
|
1236
1260
|
onChangeRole={
|
|
1237
1261
|
configures
|
|
1238
1262
|
? (member, role) =>
|
|
@@ -1240,7 +1264,7 @@ function ProjectPage(props: {
|
|
|
1240
1264
|
: undefined
|
|
1241
1265
|
}
|
|
1242
1266
|
onRemove={
|
|
1243
|
-
configures
|
|
1267
|
+
configures && !open
|
|
1244
1268
|
? (member) => void act(() => data.actions.removeMember(member.person.id))
|
|
1245
1269
|
: undefined
|
|
1246
1270
|
}
|
|
@@ -1382,28 +1406,33 @@ function ProjectSettingsPage(props: {
|
|
|
1382
1406
|
<>
|
|
1383
1407
|
{head}
|
|
1384
1408
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1385
|
-
<
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1409
|
+
<PackTokenWays
|
|
1410
|
+
title={settings.packToken}
|
|
1411
|
+
intro={words.projects.packToken.intro}
|
|
1412
|
+
set={project.packToken !== null}
|
|
1413
|
+
setAt={project.packToken?.setAt}
|
|
1414
|
+
busy={busy}
|
|
1415
|
+
onStart={data.actions.connectPackToken}
|
|
1416
|
+
poll={data.actions.packTokenConnection}
|
|
1417
|
+
onCancelConnect={data.actions.cancelPackTokenConnection}
|
|
1418
|
+
onConnected={() => navigation.replace(here)}
|
|
1419
|
+
onSubmit={(token) =>
|
|
1420
|
+
void act(async () => {
|
|
1421
|
+
await data.actions.setPackToken(token);
|
|
1422
|
+
navigation.replace(here);
|
|
1423
|
+
})
|
|
1424
|
+
}
|
|
1425
|
+
onRemove={
|
|
1426
|
+
project.packToken === null
|
|
1427
|
+
? undefined
|
|
1428
|
+
: () =>
|
|
1429
|
+
void act(async () => {
|
|
1430
|
+
await data.actions.removePackToken();
|
|
1431
|
+
navigation.replace(here);
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
onBack={() => navigation.back(here)}
|
|
1435
|
+
/>
|
|
1407
1436
|
</>
|
|
1408
1437
|
);
|
|
1409
1438
|
}
|
|
@@ -1455,7 +1484,9 @@ function ProjectSettingsPage(props: {
|
|
|
1455
1484
|
{owner && (
|
|
1456
1485
|
<>
|
|
1457
1486
|
{" "}
|
|
1458
|
-
<a href={projectSettingsHref(key, "pack-token")}>
|
|
1487
|
+
<a href={projectSettingsHref(key, "pack-token")}>
|
|
1488
|
+
{project.packToken === null ? settings.packTokenAdd : settings.packTokenEdit}
|
|
1489
|
+
</a>
|
|
1459
1490
|
</>
|
|
1460
1491
|
)}
|
|
1461
1492
|
</dd>
|
|
@@ -1516,7 +1547,7 @@ function ProjectSettingsPage(props: {
|
|
|
1516
1547
|
* from, which whoever runs the console set. Read by everyone; the rules written by an administrator.
|
|
1517
1548
|
*/
|
|
1518
1549
|
function WorkspaceSettingsPage(props: {
|
|
1519
|
-
readonly form: "rules" | undefined;
|
|
1550
|
+
readonly form: "rules" | "workspace" | "pack-token" | undefined;
|
|
1520
1551
|
readonly data: ConsoleData;
|
|
1521
1552
|
readonly navigation: Navigation;
|
|
1522
1553
|
}) {
|
|
@@ -1524,7 +1555,8 @@ function WorkspaceSettingsPage(props: {
|
|
|
1524
1555
|
const { form, data, navigation } = props;
|
|
1525
1556
|
const { busy, error, act } = useAction();
|
|
1526
1557
|
const administrator = data.me?.person?.role === "admin";
|
|
1527
|
-
const
|
|
1558
|
+
const workspace = data.me?.workspace;
|
|
1559
|
+
const skills = workspace?.skills;
|
|
1528
1560
|
const [rules, setRules] = useState<RestRules | undefined>(undefined);
|
|
1529
1561
|
useEffect(() => {
|
|
1530
1562
|
const controller = new AbortController();
|
|
@@ -1587,10 +1619,90 @@ function WorkspaceSettingsPage(props: {
|
|
|
1587
1619
|
</>
|
|
1588
1620
|
);
|
|
1589
1621
|
}
|
|
1622
|
+
if (form === "pack-token") {
|
|
1623
|
+
if (!administrator || workspace === undefined) {
|
|
1624
|
+
return <NothingHere />;
|
|
1625
|
+
}
|
|
1626
|
+
const held = workspace.skills.packToken ?? null;
|
|
1627
|
+
return (
|
|
1628
|
+
<>
|
|
1629
|
+
{head}
|
|
1630
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1631
|
+
<PackTokenWays
|
|
1632
|
+
title={words.settings.skillsToken}
|
|
1633
|
+
intro={words.settings.skillsTokenIntro}
|
|
1634
|
+
set={held !== null}
|
|
1635
|
+
setAt={held?.setAt}
|
|
1636
|
+
busy={busy}
|
|
1637
|
+
onStart={data.actions.connectWorkspacePackToken}
|
|
1638
|
+
poll={data.actions.workspacePackTokenConnection}
|
|
1639
|
+
onCancelConnect={data.actions.cancelWorkspacePackTokenConnection}
|
|
1640
|
+
onConnected={() => navigation.replace(PATHS.settings)}
|
|
1641
|
+
onSubmit={(token) =>
|
|
1642
|
+
void act(async () => {
|
|
1643
|
+
await data.actions.setWorkspacePackToken(token);
|
|
1644
|
+
navigation.replace(PATHS.settings);
|
|
1645
|
+
})
|
|
1646
|
+
}
|
|
1647
|
+
onRemove={
|
|
1648
|
+
held === null
|
|
1649
|
+
? undefined
|
|
1650
|
+
: () =>
|
|
1651
|
+
void act(async () => {
|
|
1652
|
+
await data.actions.removeWorkspacePackToken();
|
|
1653
|
+
navigation.replace(PATHS.settings);
|
|
1654
|
+
})
|
|
1655
|
+
}
|
|
1656
|
+
onBack={() => navigation.back(PATHS.settings)}
|
|
1657
|
+
/>
|
|
1658
|
+
</>
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
if (form === "workspace") {
|
|
1662
|
+
if (!administrator || workspace === undefined) {
|
|
1663
|
+
return <NothingHere />;
|
|
1664
|
+
}
|
|
1665
|
+
return (
|
|
1666
|
+
<>
|
|
1667
|
+
{head}
|
|
1668
|
+
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1669
|
+
<section className="sc-panel sc-panel-form" aria-label={words.settings.workspace.aria}>
|
|
1670
|
+
<h2 className="sc-section-title">{words.settings.workspace.title}</h2>
|
|
1671
|
+
<WorkspaceSettingsForm
|
|
1672
|
+
workspace={workspace}
|
|
1673
|
+
busy={busy}
|
|
1674
|
+
onSubmit={(patch) =>
|
|
1675
|
+
void act(async () => {
|
|
1676
|
+
await data.actions.updateWorkspace(patch);
|
|
1677
|
+
navigation.replace(PATHS.settings);
|
|
1678
|
+
})
|
|
1679
|
+
}
|
|
1680
|
+
onCancel={() => navigation.back(PATHS.settings)}
|
|
1681
|
+
/>
|
|
1682
|
+
</section>
|
|
1683
|
+
</>
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1590
1686
|
return (
|
|
1591
1687
|
<>
|
|
1592
1688
|
{head}
|
|
1593
1689
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1690
|
+
{workspace !== undefined && (
|
|
1691
|
+
<section className="sc-panel" aria-label={words.settings.workspace.aria}>
|
|
1692
|
+
<div className="sc-section-head">
|
|
1693
|
+
<h2 className="sc-section-title">{words.settings.workspace.title}</h2>
|
|
1694
|
+
{administrator && (
|
|
1695
|
+
<a
|
|
1696
|
+
className="sc-button sc-button-secondary sc-button-sm"
|
|
1697
|
+
href={settingsHref("workspace")}
|
|
1698
|
+
>
|
|
1699
|
+
{words.common.edit}
|
|
1700
|
+
</a>
|
|
1701
|
+
)}
|
|
1702
|
+
</div>
|
|
1703
|
+
<WorkspaceSettingsView workspace={workspace} />
|
|
1704
|
+
</section>
|
|
1705
|
+
)}
|
|
1594
1706
|
<section className="sc-panel" aria-label={words.rules.title}>
|
|
1595
1707
|
<h2 className="sc-section-title">{words.rules.title}</h2>
|
|
1596
1708
|
{rules === undefined ? (
|
|
@@ -1651,6 +1763,30 @@ function WorkspaceSettingsPage(props: {
|
|
|
1651
1763
|
)}
|
|
1652
1764
|
</dd>
|
|
1653
1765
|
</div>
|
|
1766
|
+
{skills.address !== null && (
|
|
1767
|
+
<div>
|
|
1768
|
+
<dt>{words.settings.skillsToken}</dt>
|
|
1769
|
+
<dd>
|
|
1770
|
+
{skills.packToken == null ? (
|
|
1771
|
+
<span className="sc-muted">{words.projects.settings.packTokenNone}</span>
|
|
1772
|
+
) : (
|
|
1773
|
+
<>
|
|
1774
|
+
{words.projects.settings.packTokenSet} <Time iso={skills.packToken.setAt} />
|
|
1775
|
+
</>
|
|
1776
|
+
)}
|
|
1777
|
+
{administrator && (
|
|
1778
|
+
<>
|
|
1779
|
+
{" "}
|
|
1780
|
+
<a href={settingsHref("pack-token")}>
|
|
1781
|
+
{skills.packToken == null
|
|
1782
|
+
? words.projects.settings.packTokenAdd
|
|
1783
|
+
: words.projects.settings.packTokenEdit}
|
|
1784
|
+
</a>
|
|
1785
|
+
</>
|
|
1786
|
+
)}
|
|
1787
|
+
</dd>
|
|
1788
|
+
</div>
|
|
1789
|
+
)}
|
|
1654
1790
|
</dl>
|
|
1655
1791
|
<p className="sc-muted">{words.settings.skillsFromConfiguration}</p>
|
|
1656
1792
|
</section>
|
|
@@ -1777,6 +1913,8 @@ function PlatePage(props: {
|
|
|
1777
1913
|
readonly empty: { readonly title: string; readonly body: string };
|
|
1778
1914
|
/** What a refusal says where the plate may not be the asker's to see; the server's words otherwise. */
|
|
1779
1915
|
readonly refused?: { readonly title: string; readonly body: string } | undefined;
|
|
1916
|
+
/** The day the week begins on, as the workspace says (ADR-0035). */
|
|
1917
|
+
readonly weekStart?: WeekStart | undefined;
|
|
1780
1918
|
}) {
|
|
1781
1919
|
const words = useWords();
|
|
1782
1920
|
const { components, load, couldNotLoad, refused } = props;
|
|
@@ -1837,6 +1975,7 @@ function PlatePage(props: {
|
|
|
1837
1975
|
<components.WorkList
|
|
1838
1976
|
work={plate}
|
|
1839
1977
|
week={props.week}
|
|
1978
|
+
weekStart={props.weekStart}
|
|
1840
1979
|
taskHref={(projectKey, task) => taskHref(projectKey, task.number)}
|
|
1841
1980
|
decisionHref={(projectKey, decisionId) => decisionHref(projectKey, decisionId)}
|
|
1842
1981
|
weekHref={props.weekHref}
|
|
@@ -1860,6 +1999,7 @@ function WorkPage(props: {
|
|
|
1860
1999
|
load={work}
|
|
1861
2000
|
title={words.work.title}
|
|
1862
2001
|
week={props.week}
|
|
2002
|
+
weekStart={weekStartOf(props.data.me?.workspace)}
|
|
1863
2003
|
weekHref={workHref}
|
|
1864
2004
|
loading={words.work.loading}
|
|
1865
2005
|
couldNotLoad={words.work.couldNotLoad}
|
|
@@ -1888,6 +2028,7 @@ function PersonWorkPage(props: {
|
|
|
1888
2028
|
title={words.people.workOf(name)}
|
|
1889
2029
|
aside={<LinkButton href={PATHS.people}>{words.nav.people}</LinkButton>}
|
|
1890
2030
|
week={props.week}
|
|
2031
|
+
weekStart={weekStartOf(data.me?.workspace)}
|
|
1891
2032
|
weekHref={(day) => personWorkHref(id, day)}
|
|
1892
2033
|
loading={words.people.loadingWork}
|
|
1893
2034
|
couldNotLoad={words.people.couldNotLoadWork}
|
|
@@ -1958,6 +2099,8 @@ function WorkspacePage(props: {
|
|
|
1958
2099
|
const making = route.name === "new-listing" && administrator;
|
|
1959
2100
|
// An owner sees the work of the people in their projects; the console refuses the rest.
|
|
1960
2101
|
const oversees = administrator || data.projects.some((project) => project.role === "owner");
|
|
2102
|
+
// The list is what the policy shows (ADR-0035): everyone, or the people one shares a project with.
|
|
2103
|
+
const narrowed = peopleVisibilityOf(data.me?.workspace) === "shared_projects" && !administrator;
|
|
1961
2104
|
return (
|
|
1962
2105
|
<>
|
|
1963
2106
|
<div className="sc-page-head">
|
|
@@ -1968,6 +2111,7 @@ function WorkspacePage(props: {
|
|
|
1968
2111
|
</LinkButton>
|
|
1969
2112
|
)}
|
|
1970
2113
|
</div>
|
|
2114
|
+
{narrowed && <p className="sc-lead">{words.people.narrowed}</p>}
|
|
1971
2115
|
{error !== undefined && <Callout tone="danger">{error}</Callout>}
|
|
1972
2116
|
{making && (
|
|
1973
2117
|
<section className="sc-panel sc-panel-form" aria-label={words.people.letInAria}>
|
package/src/data.ts
CHANGED
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
type RestMemberPatch,
|
|
33
33
|
type RestMePatch,
|
|
34
34
|
type RestOrganization,
|
|
35
|
+
type RestPackConnect,
|
|
36
|
+
type RestPackConnectStarted,
|
|
35
37
|
type RestPerson,
|
|
36
38
|
type RestPersonPatch,
|
|
37
39
|
type RestPersonWork,
|
|
@@ -54,6 +56,9 @@ import {
|
|
|
54
56
|
type RestVersion,
|
|
55
57
|
type RestVersions,
|
|
56
58
|
type RestWork,
|
|
59
|
+
type RestWorkspace,
|
|
60
|
+
type RestWorkspacePackConnect,
|
|
61
|
+
type RestWorkspacePatch,
|
|
57
62
|
restEventSchema,
|
|
58
63
|
} from "./api.js";
|
|
59
64
|
|
|
@@ -80,6 +85,11 @@ export interface ConsoleActions {
|
|
|
80
85
|
/** Sets the pack token of the project the page is on, or takes it away; for an owner (ADR-0028). */
|
|
81
86
|
setPackToken(token: string): Promise<RestProject>;
|
|
82
87
|
removePackToken(): Promise<RestProject>;
|
|
88
|
+
/** Asks the deployment to connect the project's pack with a code a person approves (ADR-0036); for an owner. */
|
|
89
|
+
connectPackToken(): Promise<RestPackConnectStarted>;
|
|
90
|
+
/** How that connection stands; once connected, the project is the one answered. */
|
|
91
|
+
packTokenConnection(signal?: AbortSignal): Promise<RestPackConnect>;
|
|
92
|
+
cancelPackTokenConnection(): Promise<void>;
|
|
83
93
|
addMember(input: RestMemberInput): Promise<RestMember>;
|
|
84
94
|
updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
|
|
85
95
|
removeMember(personId: string): Promise<void>;
|
|
@@ -133,6 +143,16 @@ export interface ConsoleActions {
|
|
|
133
143
|
removeListing(id: string): Promise<void>;
|
|
134
144
|
/** Keeps the person's own choice of language (ADR-0012). */
|
|
135
145
|
updateMe(patch: RestMePatch): Promise<RestMe>;
|
|
146
|
+
/** Changes the workspace's settings (ADR-0035); for an administrator. `me` says them from then on. */
|
|
147
|
+
updateWorkspace(patch: RestWorkspacePatch): Promise<RestWorkspace>;
|
|
148
|
+
/** The workspace's SkillCDN token (ADR-0036), for an administrator: set, taken away, or connected with a code. */
|
|
149
|
+
setWorkspacePackToken(token: string): Promise<RestWorkspace>;
|
|
150
|
+
removeWorkspacePackToken(): Promise<RestWorkspace>;
|
|
151
|
+
connectWorkspacePackToken(): Promise<RestPackConnectStarted>;
|
|
152
|
+
workspacePackTokenConnection(signal?: AbortSignal): Promise<RestWorkspacePackConnect>;
|
|
153
|
+
cancelWorkspacePackTokenConnection(): Promise<void>;
|
|
154
|
+
/** One person of the workspace by their exact login, or nobody: what an owner lists someone the People page does not show by. */
|
|
155
|
+
findPerson(login: string, signal?: AbortSignal): Promise<RestPerson | undefined>;
|
|
136
156
|
/** Marks a run that will not come back as abandoned. */
|
|
137
157
|
abandonRun(id: string): Promise<RestRun>;
|
|
138
158
|
/** Everything that happened to one task of the project, oldest first. */
|
|
@@ -538,6 +558,15 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
538
558
|
setProject(changed);
|
|
539
559
|
return changed;
|
|
540
560
|
},
|
|
561
|
+
connectPackToken: () => scope().connectPackToken(),
|
|
562
|
+
async packTokenConnection(signal) {
|
|
563
|
+
const state = await scope().packTokenConnection(signal);
|
|
564
|
+
if (state.status === "connected") {
|
|
565
|
+
setProject(state.project);
|
|
566
|
+
}
|
|
567
|
+
return state;
|
|
568
|
+
},
|
|
569
|
+
cancelPackTokenConnection: () => scope().cancelPackTokenConnection(),
|
|
541
570
|
async addMember(input) {
|
|
542
571
|
const member = await scope().addMember(input);
|
|
543
572
|
await refreshProject();
|
|
@@ -649,6 +678,34 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
|
|
|
649
678
|
setMe(answer);
|
|
650
679
|
return answer;
|
|
651
680
|
},
|
|
681
|
+
async updateWorkspace(patch) {
|
|
682
|
+
const workspace = await client.updateWorkspace(patch);
|
|
683
|
+
setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
|
|
684
|
+
// The member visibility may have changed: the people are read again under the new setting.
|
|
685
|
+
await refreshWorkspace();
|
|
686
|
+
return workspace;
|
|
687
|
+
},
|
|
688
|
+
async setWorkspacePackToken(token) {
|
|
689
|
+
const workspace = await client.setWorkspacePackToken({ token });
|
|
690
|
+
setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
|
|
691
|
+
return workspace;
|
|
692
|
+
},
|
|
693
|
+
async removeWorkspacePackToken() {
|
|
694
|
+
const workspace = await client.removeWorkspacePackToken();
|
|
695
|
+
setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
|
|
696
|
+
return workspace;
|
|
697
|
+
},
|
|
698
|
+
connectWorkspacePackToken: () => client.connectWorkspacePackToken(),
|
|
699
|
+
async workspacePackTokenConnection(signal) {
|
|
700
|
+
const state = await client.workspacePackTokenConnection(signal);
|
|
701
|
+
if (state.status === "connected") {
|
|
702
|
+
const { workspace } = state;
|
|
703
|
+
setMe((current) => (current === undefined ? undefined : { ...current, workspace }));
|
|
704
|
+
}
|
|
705
|
+
return state;
|
|
706
|
+
},
|
|
707
|
+
cancelWorkspacePackTokenConnection: () => client.cancelWorkspacePackTokenConnection(),
|
|
708
|
+
findPerson: (login, signal) => client.findPerson(login, signal),
|
|
652
709
|
async abandonRun(id) {
|
|
653
710
|
const run = await scope().endRun(id, { status: "abandoned" });
|
|
654
711
|
await refreshProject();
|
package/src/dates.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WeekStart } from "./vocabulary.js";
|
|
2
|
+
|
|
1
3
|
// Days, as the plan is made in (ADR-0019): `2026-10-20`, read and written as the strings they
|
|
2
4
|
// are, so that a deadline is the same day wherever the people are. The browser's own clock says
|
|
3
5
|
// which day is today; nothing here looks at it unless asked.
|
|
@@ -62,8 +64,27 @@ export function addMonths(month: string, count: number): string {
|
|
|
62
64
|
return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The language and region of the device the pages are read on, as the browser says: `ko-KR`,
|
|
69
|
+
* `en-GB`. The fallback where there is no browser to ask. The week begins as this has it
|
|
70
|
+
* where the workspace leaves the choice to the system default (ADR-0035).
|
|
71
|
+
*/
|
|
72
|
+
export function deviceLanguage(fallback: string): string {
|
|
73
|
+
const browser = (globalThis as { readonly navigator?: { readonly language?: string } }).navigator;
|
|
74
|
+
return browser?.language ?? fallback;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The weekday a week begins on: as the workspace says (ADR-0035), Monday or Sunday, else as the
|
|
79
|
+
* language and region have it, the device's by {@link deviceLanguage}. 1 for Monday, 7 for Sunday.
|
|
80
|
+
*/
|
|
81
|
+
export function firstDayOfWeek(language: string, weekStart?: WeekStart | undefined): number {
|
|
82
|
+
if (weekStart === "monday") {
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
if (weekStart === "sunday") {
|
|
86
|
+
return 7;
|
|
87
|
+
}
|
|
67
88
|
try {
|
|
68
89
|
const locale = new Intl.Locale(language) as Intl.Locale & {
|
|
69
90
|
readonly weekInfo?: { readonly firstDay: number };
|
package/src/i18n/en.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type {
|
|
|
3
3
|
CategoryColor,
|
|
4
4
|
GoalState,
|
|
5
5
|
MembershipSource,
|
|
6
|
+
PackConnectEnding,
|
|
7
|
+
PeopleVisibility,
|
|
6
8
|
PersonRole,
|
|
7
9
|
PersonStatus,
|
|
8
10
|
ProjectRole,
|
|
@@ -12,6 +14,7 @@ import type {
|
|
|
12
14
|
SkillsStatus,
|
|
13
15
|
TaskPriority,
|
|
14
16
|
TaskState,
|
|
17
|
+
WeekStart,
|
|
15
18
|
} from "../vocabulary.js";
|
|
16
19
|
|
|
17
20
|
// Every word the default console shows, in English: the source of the keys every other pack
|
|
@@ -44,9 +47,18 @@ export const en = {
|
|
|
44
47
|
status: { active: "Active", deactivated: "Deactivated" } satisfies Record<PersonStatus, string>,
|
|
45
48
|
projectRole: { owner: "Owner", member: "Member" } satisfies Record<ProjectRole, string>,
|
|
46
49
|
visibility: {
|
|
47
|
-
workspace: "
|
|
48
|
-
private: "
|
|
50
|
+
workspace: "Open: everyone of the workspace",
|
|
51
|
+
private: "Private: only those listed",
|
|
49
52
|
} satisfies Record<ProjectVisibility, string>,
|
|
53
|
+
peopleVisibility: {
|
|
54
|
+
everyone: "Everyone of the workspace",
|
|
55
|
+
shared_projects: "Only the people one shares a project with",
|
|
56
|
+
} satisfies Record<PeopleVisibility, string>,
|
|
57
|
+
weekStart: {
|
|
58
|
+
auto: "System default",
|
|
59
|
+
monday: "Monday",
|
|
60
|
+
sunday: "Sunday",
|
|
61
|
+
} satisfies Record<WeekStart, string>,
|
|
50
62
|
runStatus: {
|
|
51
63
|
running: "Open",
|
|
52
64
|
waiting: "Waiting for a decision",
|
|
@@ -195,6 +207,13 @@ export const en = {
|
|
|
195
207
|
chooseSomeone: "Choose someone",
|
|
196
208
|
as: "As",
|
|
197
209
|
add: "Add",
|
|
210
|
+
open: "An open project: everyone of the workspace is a member. The owners configure it, and an owner is made from this list.",
|
|
211
|
+
addByLoginAria: "Add a member by login",
|
|
212
|
+
login: "Login",
|
|
213
|
+
addByLogin: "Add by login",
|
|
214
|
+
lookupHint:
|
|
215
|
+
"Someone the list does not show: their login at the git host, or their address, exactly.",
|
|
216
|
+
nobodyCalled: (login: string) => `Nobody of the workspace is called ${login}.`,
|
|
198
217
|
empty: {
|
|
199
218
|
title: "Nobody is listed",
|
|
200
219
|
workspace: "Everyone of the workspace is in; the administrators own it.",
|
|
@@ -202,11 +221,33 @@ export const en = {
|
|
|
202
221
|
},
|
|
203
222
|
},
|
|
204
223
|
packToken: {
|
|
205
|
-
aria: "The
|
|
224
|
+
aria: "The SkillCDN token",
|
|
225
|
+
intro:
|
|
226
|
+
"A private pack is read with a SkillCDN token for its repository; a public one needs none. Connect it with a code approved at SkillCDN, or paste a token made there by hand. The console keeps the token sealed and shows it to nobody. A project that reads the organization's pack may leave this to the workspace's token, on the Settings page.",
|
|
227
|
+
connectTitle: "With a code approved at SkillCDN",
|
|
228
|
+
pasteTitle: "With a token made by hand",
|
|
229
|
+
pasteAria: "Paste a token",
|
|
206
230
|
label: "Token",
|
|
207
|
-
hint: "
|
|
231
|
+
hint: "Made on the account page at SkillCDN, for the pack's repository.",
|
|
232
|
+
setSince: "A token has been set since",
|
|
208
233
|
placeholder: "scdn_repo_…",
|
|
234
|
+
connect: "Connect at SkillCDN",
|
|
235
|
+
connectHint:
|
|
236
|
+
"The console asks SkillCDN for a code. Open the page it gives, signed in there as someone who may open the repository, check the code and approve.",
|
|
237
|
+
connectCode: "Code",
|
|
238
|
+
connectOpen: "Open the page at SkillCDN",
|
|
239
|
+
connectWaiting: "Waiting for the approval at SkillCDN.",
|
|
240
|
+
connectExpires: "The code expires",
|
|
241
|
+
connectCancel: "Stop waiting",
|
|
242
|
+
connectDone: "Connected: the console reads the pack with the token from now on.",
|
|
243
|
+
connectEnded: {
|
|
244
|
+
expired: "The code expired before anyone approved it. Connect again for a new one.",
|
|
245
|
+
declined: "The request was declined at SkillCDN, or the code was spent.",
|
|
246
|
+
refused:
|
|
247
|
+
"SkillCDN refused the request: the repository is not one the person may open, the pack is public and needs no token, or they hold as many tokens as one may.",
|
|
248
|
+
} satisfies Record<PackConnectEnding, string>,
|
|
209
249
|
set: "Set the token",
|
|
250
|
+
replace: "Replace the token",
|
|
210
251
|
remove: "Remove the token",
|
|
211
252
|
},
|
|
212
253
|
settings: {
|
|
@@ -215,10 +256,11 @@ export const en = {
|
|
|
215
256
|
edit: "Edit the project",
|
|
216
257
|
organizationsSkills: (address: string) => `The organization's: ${address}`,
|
|
217
258
|
noSkills: "None: the console names no address for the organization's.",
|
|
218
|
-
packToken: "
|
|
219
|
-
packTokenNone: "None
|
|
259
|
+
packToken: "SkillCDN token",
|
|
260
|
+
packTokenNone: "None",
|
|
220
261
|
packTokenSet: "Set",
|
|
221
262
|
packTokenEdit: "Change it",
|
|
263
|
+
packTokenAdd: "Set one",
|
|
222
264
|
},
|
|
223
265
|
},
|
|
224
266
|
|
|
@@ -498,6 +540,26 @@ export const en = {
|
|
|
498
540
|
noSkillsAddress: "No address: each project names its own, or has none.",
|
|
499
541
|
skillsFromConfiguration:
|
|
500
542
|
"Set by whoever runs the console, in its configuration: the deployment the skills are read through, and the organization's address.",
|
|
543
|
+
skillsToken: "SkillCDN token",
|
|
544
|
+
skillsTokenIntro:
|
|
545
|
+
"Every project that names no address of its own, and holds no token of its own, reads the organization's pack with this token; a public pack needs none. Connect it with a code approved at SkillCDN, or paste a token made there by hand. The console keeps the token sealed and shows it to nobody.",
|
|
546
|
+
workspace: {
|
|
547
|
+
title: "Workspace",
|
|
548
|
+
aria: "Workspace settings",
|
|
549
|
+
peopleVisibility: "Member visibility",
|
|
550
|
+
peopleVisibilityHint:
|
|
551
|
+
"Whom a member sees on the People page and in the pickers: everyone of the workspace, or only the people they share a project with. An administrator always sees everyone, and a project open to the workspace has everyone in it.",
|
|
552
|
+
weekStart: "The week begins on",
|
|
553
|
+
weekStartHint:
|
|
554
|
+
"On the calendars and on My work. System default follows the language and region set on each person's device; Monday or Sunday holds for everyone.",
|
|
555
|
+
timeZone: "Time zone",
|
|
556
|
+
timeZoneHint:
|
|
557
|
+
"The console command and the agents print moments in this zone. The pages show them in each device's own. None: the command prints them in UTC.",
|
|
558
|
+
timeZoneNone: "None",
|
|
559
|
+
timeZoneDevice: (zone: string) => `This device: ${zone}`,
|
|
560
|
+
timeZoneUseDevice: (zone: string) => `Use this device's: ${zone}`,
|
|
561
|
+
save: "Save the settings",
|
|
562
|
+
},
|
|
501
563
|
},
|
|
502
564
|
|
|
503
565
|
rules: {
|
|
@@ -561,9 +623,14 @@ export const en = {
|
|
|
561
623
|
dueOn: "the due date",
|
|
562
624
|
state: "the state",
|
|
563
625
|
color: "the colour",
|
|
626
|
+
peopleVisibility: "the member visibility",
|
|
627
|
+
weekStart: "the first day of the week",
|
|
628
|
+
timeZone: "the time zone",
|
|
629
|
+
packToken: "the SkillCDN token",
|
|
564
630
|
} as Readonly<Record<string, string>>,
|
|
565
631
|
event: {
|
|
566
632
|
personJoined: () => "joined the board",
|
|
633
|
+
workspaceUpdated: (fields: string) => `changed ${fields} of the workspace`,
|
|
567
634
|
roleChanged: (login: string, role: string) => `made ${login} ${role}`,
|
|
568
635
|
personListed: (login: string) => `added ${login}`,
|
|
569
636
|
personUnlisted: (login: string) => `took ${login} off the list`,
|
|
@@ -684,9 +751,9 @@ export const en = {
|
|
|
684
751
|
indexing: "SkillCDN is still indexing the repository. Look again in a moment.",
|
|
685
752
|
failed: "SkillCDN could not index the repository. What it found is on the address's page.",
|
|
686
753
|
not_found:
|
|
687
|
-
"SkillCDN does not serve this address to the console: there is nothing there, or the pack is private and the project
|
|
754
|
+
"SkillCDN does not serve this address to the console: there is nothing there, or the pack is private and no SkillCDN token reads it, the project's own or the workspace's. An owner connects one on the Settings page, with a code approved at SkillCDN.",
|
|
688
755
|
token_invalid:
|
|
689
|
-
"The
|
|
756
|
+
"The SkillCDN token the pack is read with no longer works at SkillCDN: it expired, was removed, or its maker lost the repository. An owner sets a new one on the Settings page.",
|
|
690
757
|
unavailable: "SkillCDN could not be reached. The skills are as they were; look again later.",
|
|
691
758
|
},
|
|
692
759
|
none: {
|
|
@@ -764,6 +831,7 @@ export const en = {
|
|
|
764
831
|
people: {
|
|
765
832
|
title: "People",
|
|
766
833
|
list: "People",
|
|
834
|
+
narrowed: "The people you share a project with, and the administrators.",
|
|
767
835
|
roleOf: (name: string) => `Role of ${name}`,
|
|
768
836
|
letIn: "Add a person",
|
|
769
837
|
letInAria: "Add a person",
|