@unson/brainbase-mcp 0.7.0 → 0.8.0
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/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- package/dist/canonical-task-service.js +16 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/company-os-objectives.d.ts +11 -0
- package/dist/company-os-objectives.js +84 -47
- package/dist/foundation-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -0
- package/dist/foundation-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -0
- package/dist/judgment-problem-snapshot.d.ts +30 -2
- package/dist/judgment-problem-snapshot.js +302 -6
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- package/dist/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -0
- package/dist/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +12 -0
- package/dist/personal-knowledge.js +29 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -0
- package/dist/server.d.ts +7 -2
- package/dist/server.js +9 -6
- package/dist/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +21 -3
- package/dist/world-model.js +94 -12
- package/package.json +94 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +119 -45
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +180 -74
- package/ui/objective-editor.js +490 -43
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 「プロジェクトと関係者」: which projects exist, who takes part in or is
|
|
3
|
+
* accountable for each one, and whether that record is right.
|
|
4
|
+
*
|
|
5
|
+
* Built on the organization edition's screen pattern (workspace kit): the
|
|
6
|
+
* workspace has the page head, the source notice, summary metrics and the
|
|
7
|
+
* project ledger; the right rail has the selected project — its outline, each
|
|
8
|
+
* 関係者 and the corrections. The first project is selected on load.
|
|
9
|
+
*
|
|
10
|
+
* Reads `GET {base}/projects` and `GET {base}/projects/:id` and corrects
|
|
11
|
+
* through `POST {base}/corrections` (see `graph-view-shared.js`). 「関係者」
|
|
12
|
+
* are the people recorded as taking part in or being accountable for a
|
|
13
|
+
* project; they are not logins or sharing settings. Participation ends with
|
|
14
|
+
* an end date and is never deleted. The host injects the fetcher, base path,
|
|
15
|
+
* launch token, right rail and page context; this module keeps no global state.
|
|
16
|
+
*
|
|
17
|
+
* Another host composes this part unchanged and adds only its own controls
|
|
18
|
+
* through the options of `createGraphProjectsView` (read-only or partial
|
|
19
|
+
* corrections, its source notice, page-head buttons, extra metrics, rail blocks,
|
|
20
|
+
* selection sync, and the owner's own share in a separate section; see
|
|
21
|
+
* `graph-own-share.js`). Every option defaults to the behaviour above.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
activityBadge,
|
|
26
|
+
badge,
|
|
27
|
+
createGraphClient,
|
|
28
|
+
createGraphCorrection,
|
|
29
|
+
createGraphLayout,
|
|
30
|
+
focusLedgerRow,
|
|
31
|
+
getDocument,
|
|
32
|
+
graphCommandList,
|
|
33
|
+
graphCorrectionScope,
|
|
34
|
+
graphPageContext,
|
|
35
|
+
graphStateNotice,
|
|
36
|
+
hostReadOnlyNote,
|
|
37
|
+
isEdgeView,
|
|
38
|
+
isEntityView,
|
|
39
|
+
isRecord,
|
|
40
|
+
makeElement,
|
|
41
|
+
ownLabel,
|
|
42
|
+
readOnlyGraphClient,
|
|
43
|
+
recordDetails,
|
|
44
|
+
relationLabel,
|
|
45
|
+
renderCorrectionHistory,
|
|
46
|
+
renderGraphIssues,
|
|
47
|
+
renderProvenance,
|
|
48
|
+
shellArg,
|
|
49
|
+
textOrNull,
|
|
50
|
+
validityLabel,
|
|
51
|
+
validityText,
|
|
52
|
+
GRAPH_CORRECTION_FIELDS,
|
|
53
|
+
GRAPH_RELATION_LABELS,
|
|
54
|
+
graphHostEmptyNotice,
|
|
55
|
+
} from './graph-view-shared.js';
|
|
56
|
+
import { graphOwnShareSource, ownProjectDetail, ownShareSection, ownShareStateNotice } from './graph-own-share.js';
|
|
57
|
+
import {
|
|
58
|
+
workspaceActions,
|
|
59
|
+
workspaceButton,
|
|
60
|
+
workspaceDefinition,
|
|
61
|
+
workspaceDetailEmpty,
|
|
62
|
+
workspaceHostActions,
|
|
63
|
+
workspaceHostNotice,
|
|
64
|
+
workspaceLedger,
|
|
65
|
+
workspaceMetrics,
|
|
66
|
+
workspaceNotice,
|
|
67
|
+
workspacePageHeader,
|
|
68
|
+
workspaceRailBlock,
|
|
69
|
+
workspaceRailHead,
|
|
70
|
+
} from './workspace-kit.js';
|
|
71
|
+
|
|
72
|
+
export const GRAPH_PROJECTS_VIEW_CONTRACT_VERSION = 'brainbase.graph-projects-view.v2';
|
|
73
|
+
|
|
74
|
+
/** The two ways a person is related to a project in the OSS Graph. */
|
|
75
|
+
export const PARTICIPATION_LABELS = Object.freeze({ participates_in: '参加', accountable_for: '責任' });
|
|
76
|
+
|
|
77
|
+
export const GRAPH_PROJECT_LEDGER_COLUMNS = Object.freeze(['プロジェクト', '目的', '責任を持つ人', '状態', '関係者', '有効期間']);
|
|
78
|
+
|
|
79
|
+
const INVALID = Object.freeze({ state: 'invalid', reason: '応答の形式が不正です' });
|
|
80
|
+
|
|
81
|
+
function isProjectPerson(value) {
|
|
82
|
+
return isRecord(value) && typeof value.id === 'string' && typeof value.name === 'string' && typeof value.accountable === 'boolean';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isProjectItem(value) {
|
|
86
|
+
return isEntityView(value) && value.type === 'project'
|
|
87
|
+
&& Number.isInteger(value.participantCount) && Number.isInteger(value.accountableCount)
|
|
88
|
+
&& (value.people === undefined || (Array.isArray(value.people) && value.people.every(isProjectPerson)));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A project list is shown only when it is well formed. An empty list is
|
|
93
|
+
* `ok` only when the host confirmed that no project exists; otherwise the
|
|
94
|
+
* absence is `unknown`, never zero items.
|
|
95
|
+
*/
|
|
96
|
+
export function normalizeProjectList(result) {
|
|
97
|
+
if (result?.state !== 'ok') return result ?? INVALID;
|
|
98
|
+
const payload = result.payload;
|
|
99
|
+
if (!Array.isArray(payload.projects) || !payload.projects.every(isProjectItem)) return INVALID;
|
|
100
|
+
if (payload.projects.length === 0 && payload.absenceConfirmed !== true) return { state: 'unknown' };
|
|
101
|
+
return { state: 'ok', payload };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function normalizeProjectDetail(result) {
|
|
105
|
+
if (result?.state !== 'ok') return result ?? INVALID;
|
|
106
|
+
const payload = result.payload;
|
|
107
|
+
const project = payload.project;
|
|
108
|
+
if (!isEntityView(project) || project.type !== 'project') return INVALID;
|
|
109
|
+
if (!Array.isArray(payload.participants) || !payload.participants.every(isEdgeView)) return INVALID;
|
|
110
|
+
if (payload.relations !== undefined && (!Array.isArray(payload.relations) || !payload.relations.every(isEdgeView))) return INVALID;
|
|
111
|
+
return { state: 'ok', payload };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The summary metrics. 関係者 counts each person once across projects and is
|
|
116
|
+
* `null` (未確認, never zero) when the host did not name the people.
|
|
117
|
+
*/
|
|
118
|
+
export function summarizeProjects(projects) {
|
|
119
|
+
const active = projects.filter((project) => project.active === true);
|
|
120
|
+
const named = projects.every((project) => Array.isArray(project.people));
|
|
121
|
+
return {
|
|
122
|
+
total: projects.length,
|
|
123
|
+
active: active.length,
|
|
124
|
+
inProgress: active.filter((project) => textOrNull(project.status) === '進行中').length,
|
|
125
|
+
people: named ? new Set(projects.flatMap((project) => project.people.map((person) => person.id))).size : null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function accountableCell(project) {
|
|
130
|
+
if (Array.isArray(project.people)) {
|
|
131
|
+
const names = project.people.filter((person) => person.accountable).map((person) => person.name);
|
|
132
|
+
return names.length > 0 ? names.join('、') : { text: '未登録', className: 'is-unresolved' };
|
|
133
|
+
}
|
|
134
|
+
return project.accountableCount > 0 ? `${project.accountableCount}人` : { text: '未登録', className: 'is-unresolved' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function textCell(value, fallback) {
|
|
138
|
+
return textOrNull(value) ?? { text: fallback, className: 'is-unresolved' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isNode(value) {
|
|
142
|
+
return value !== null && typeof value === 'object' && (typeof value.tagName === 'string' || typeof value.nodeType === 'number');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function failureText(error) {
|
|
146
|
+
return textOrNull(error instanceof Error ? error.message : typeof error === 'string' ? error : null) ?? '理由不明';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Mounts 「プロジェクトと関係者」.
|
|
151
|
+
*
|
|
152
|
+
* @param {object} options
|
|
153
|
+
* @param {Element} options.root Where the workspace is drawn.
|
|
154
|
+
* @param {Element} [options.rail] The host's right rail; without it the rail content follows the workspace inside `root`.
|
|
155
|
+
* @param {{ crumbs?: string[], source?: string }} [options.page] Breadcrumb and source label of the page head.
|
|
156
|
+
* @param {Document} [options.document] The document to build elements with (default: the global one).
|
|
157
|
+
* @param {Function} [options.fetcher] `fetch`-compatible function for the Graph routes.
|
|
158
|
+
* @param {string} [options.basePath='/api/graph'] Where the Graph routes are served.
|
|
159
|
+
* @param {string} [options.token] Launch token sent with a correction.
|
|
160
|
+
* @param {string} [options.tokenHeader] Header name for the token.
|
|
161
|
+
* @param {boolean} [options.autoLoad=true] Reads the list on mount.
|
|
162
|
+
* @param {() => Date} [options.now] The owner's clock (end-date default).
|
|
163
|
+
* @param {boolean} [options.canCorrect=true] When false, no correction control is drawn anywhere
|
|
164
|
+
* (関係者を加える, 役割を直す, 関わりを終える / 終了日を直す, プロジェクトを直す, and the pointer to
|
|
165
|
+
* corrections under そのほかの関係), and the client refuses every correction before sending it.
|
|
166
|
+
* @param {{ entityTypes?: string[], fields?: string[], edges?: boolean, createEdges?: boolean }} [options.correctionScope]
|
|
167
|
+
* With corrections allowed, only these (see `graphCorrectionScope`): `entityTypes: ['project']` keeps
|
|
168
|
+
* プロジェクトを直す, `fields` limits its form, `edges: false` withholds 役割を直す and 関わりを終える /
|
|
169
|
+
* 終了日を直す, and `createEdges: false` withholds 関係者を加える. A withheld control is not drawn and the
|
|
170
|
+
* client refuses such a correction before sending it. Without it every correction is allowed.
|
|
171
|
+
* @param {string} [options.readOnlyNote] With `canCorrect: false`, or where `correctionScope` withholds a
|
|
172
|
+
* control of the selected project, a note shown in the rail where the correction buttons would be.
|
|
173
|
+
* Without it the rail says nothing about corrections.
|
|
174
|
+
* @param {{ label?: string, text: string | Element }} [options.emptyNotice] Replaces the 未登録 notice
|
|
175
|
+
* and its `brainbase onboard:*` commands when the host's Graph has nothing registered.
|
|
176
|
+
* @param {{ label?: string, text: string | Element }} [options.sourceNotice] Replaces the default 出典
|
|
177
|
+
* notice. The rail's 概要 then drops its default 出典 row, which names this Mac's Graph.
|
|
178
|
+
* @param {Array<{ text: string, variant?: 'default' | 'primary' | 'danger' | 'quiet', onClick?: Function, disabled?: boolean }>} [options.pageActions]
|
|
179
|
+
* Extra buttons in the page head, owned by the host. Read on every render, so a host may change
|
|
180
|
+
* an item and call `render()`.
|
|
181
|
+
* @param {(listPayload: object) => Array<{ label: string, value: string | number | null, note?: string }>} [options.extraMetrics]
|
|
182
|
+
* Metrics appended after the built-in ones while the list is shown; `value: null` shows 未確認.
|
|
183
|
+
* A throw is shown as a notice instead of breaking the workspace.
|
|
184
|
+
* @param {(projectId: string, detailPayload: object | null, helpers: { document: Document }) => Node | Node[] | null} [options.renderRailExtensions]
|
|
185
|
+
* Host-owned blocks appended in the rail after 関係者 (before そのほかの関係 and the corrections
|
|
186
|
+
* history). Called synchronously on every rail render while a project is selected, with the read
|
|
187
|
+
* project detail, or null while it loads or when it could not be read. A throw or a value that is
|
|
188
|
+
* not a node is shown as a small danger notice instead of breaking the rail.
|
|
189
|
+
* @param {string} [options.selectedId] The project to select first. When absent or not in the
|
|
190
|
+
* list, the first project is selected, as by default.
|
|
191
|
+
* @param {(id: string) => void} [options.onSelect] Called when the view changes the selection itself
|
|
192
|
+
* (a ledger row, or the automatic first project). Not called for `select(id)`.
|
|
193
|
+
* @param {{ basePath: string, fetcher?: Function, label?: string }} [options.ownShare]
|
|
194
|
+
* The owner's own snapshot the host keeps for them (`graph-own-share.js`): its projects are read
|
|
195
|
+
* from `{basePath}/projects` and shown read-only in a separate section after the host's content,
|
|
196
|
+
* labelled 「引き継いだ自分の分(組織には未反映)」 unless `label` says otherwise. They are never
|
|
197
|
+
* added to the ledger, the metrics or the rail, and a failed read shows only in that section.
|
|
198
|
+
* Without it nothing of the kind is drawn or read.
|
|
199
|
+
* @returns The controller: `state`, `correction`, `render()`, `load()`, `loadDetail(id)`,
|
|
200
|
+
* `selectProject(id)`, `openProject(id)`, `select(id)` for the host's own navigation, and with
|
|
201
|
+
* `ownShare` `loadOwn()` and `selectOwnProject(id)`.
|
|
202
|
+
*/
|
|
203
|
+
export function createGraphProjectsView({
|
|
204
|
+
root,
|
|
205
|
+
rail,
|
|
206
|
+
page,
|
|
207
|
+
document: explicitDocument,
|
|
208
|
+
fetcher,
|
|
209
|
+
basePath = '/api/graph',
|
|
210
|
+
token,
|
|
211
|
+
tokenHeader,
|
|
212
|
+
autoLoad = true,
|
|
213
|
+
now = () => new Date(),
|
|
214
|
+
canCorrect = true,
|
|
215
|
+
correctionScope,
|
|
216
|
+
readOnlyNote,
|
|
217
|
+
sourceNotice,
|
|
218
|
+
emptyNotice,
|
|
219
|
+
pageActions,
|
|
220
|
+
extraMetrics,
|
|
221
|
+
renderRailExtensions,
|
|
222
|
+
selectedId: initialSelectedId,
|
|
223
|
+
onSelect,
|
|
224
|
+
ownShare,
|
|
225
|
+
} = {}) {
|
|
226
|
+
if (!root) throw new TypeError('root is required');
|
|
227
|
+
const doc = getDocument(explicitDocument);
|
|
228
|
+
const graphClient = createGraphClient({ fetcher, basePath, token, ...(tokenHeader ? { tokenHeader } : {}) });
|
|
229
|
+
const own = graphOwnShareSource(ownShare, { fetcher });
|
|
230
|
+
const writable = canCorrect !== false;
|
|
231
|
+
const client = writable ? graphClient : readOnlyGraphClient(graphClient);
|
|
232
|
+
const scope = graphCorrectionScope(correctionScope);
|
|
233
|
+
// Relations are corrected only when the host allows it (here and under そのほかの関係).
|
|
234
|
+
const edgesWritable = writable && scope.edges;
|
|
235
|
+
const context = graphPageContext(page);
|
|
236
|
+
const layout = createGraphLayout(doc, {
|
|
237
|
+
root,
|
|
238
|
+
rail,
|
|
239
|
+
label: 'プロジェクトと関係者',
|
|
240
|
+
className: 'bb-gp',
|
|
241
|
+
contractVersion: GRAPH_PROJECTS_VIEW_CONTRACT_VERSION,
|
|
242
|
+
});
|
|
243
|
+
// `panelAt` is where the open correction shows: a relation id (inside that
|
|
244
|
+
// 関係者) or anything else (below the project actions).
|
|
245
|
+
const state = {
|
|
246
|
+
list: { state: 'loading' },
|
|
247
|
+
selectedId: textOrNull(initialSelectedId),
|
|
248
|
+
detail: null,
|
|
249
|
+
panelAt: null,
|
|
250
|
+
// The owner's own share: its own list and selection, apart from the host's.
|
|
251
|
+
own: own ? { list: { state: 'loading' }, selectedId: null, detail: null } : null,
|
|
252
|
+
};
|
|
253
|
+
let focusRow = null;
|
|
254
|
+
|
|
255
|
+
const correction = createGraphCorrection({
|
|
256
|
+
client,
|
|
257
|
+
now,
|
|
258
|
+
scope: correctionScope,
|
|
259
|
+
rerender: () => controller.render(),
|
|
260
|
+
onSaved: async () => {
|
|
261
|
+
await Promise.all([
|
|
262
|
+
state.detail ? controller.loadDetail(state.detail.id, { keep: true }) : null,
|
|
263
|
+
controller.load({ keep: true }),
|
|
264
|
+
]);
|
|
265
|
+
},
|
|
266
|
+
onConflict: async () => {
|
|
267
|
+
if (state.detail) await controller.loadDetail(state.detail.id, { keep: true });
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
function listItem(id) {
|
|
272
|
+
return state.list.state === 'ok' ? state.list.payload.projects.find((project) => project.id === id) ?? null : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function openCorrection(at, open) {
|
|
276
|
+
state.panelAt = at;
|
|
277
|
+
open();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function notifySelect(id) {
|
|
281
|
+
if (typeof onSelect === 'function' && id) onSelect(id);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// -------------------------------------------------------------------------
|
|
285
|
+
// Host extensions
|
|
286
|
+
|
|
287
|
+
/** The host's extra metrics; a throw or a malformed answer becomes a notice. */
|
|
288
|
+
function hostMetrics(payload) {
|
|
289
|
+
if (typeof extraMetrics !== 'function') return { metrics: [], failure: null };
|
|
290
|
+
try {
|
|
291
|
+
const metrics = extraMetrics(payload);
|
|
292
|
+
if (!Array.isArray(metrics) || !metrics.every((item) => isRecord(item) && textOrNull(item.label))) {
|
|
293
|
+
throw new Error('集計の形式が正しくありません');
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
metrics: metrics.map((item) => ({ label: item.label, value: item.value ?? null, note: textOrNull(item.note) ?? undefined })),
|
|
297
|
+
failure: null,
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
metrics: [],
|
|
302
|
+
failure: workspaceNotice(doc, { label: '表示できません', text: `ホストが加えた集計を表示できませんでした(${failureText(error)})。`, tone: 'danger' }),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The host's rail blocks for the selected project; a throw or a value that is not a node becomes a notice. */
|
|
308
|
+
function railExtensions(projectId, payload) {
|
|
309
|
+
if (typeof renderRailExtensions !== 'function') return [];
|
|
310
|
+
try {
|
|
311
|
+
const result = renderRailExtensions(projectId, payload, { document: doc });
|
|
312
|
+
const nodes = (Array.isArray(result) ? result : [result]).filter((node) => node !== null && node !== undefined && node !== false);
|
|
313
|
+
if (!nodes.every(isNode)) throw new Error('欄の形式が正しくありません');
|
|
314
|
+
return nodes;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
return [workspaceNotice(doc, { label: '表示できません', text: `ホストが加えた欄を表示できませんでした(${failureText(error)})。`, tone: 'danger' })];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
// Workspace
|
|
322
|
+
|
|
323
|
+
/** The owner's own share, after everything of the host's (whatever state the host's list is in). */
|
|
324
|
+
function renderWorkspace() {
|
|
325
|
+
const children = renderHostWorkspace();
|
|
326
|
+
if (own) children.push(renderOwnShare());
|
|
327
|
+
return children;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function renderOwnShare() {
|
|
331
|
+
const ownState = state.own;
|
|
332
|
+
const notice = ownShareStateNotice(doc, ownState.list, { onRetry: () => controller.loadOwn(), loadingText: '引き継いだ自分の分を読み込んでいます。' });
|
|
333
|
+
if (notice) return ownShareSection(doc, { label: own.label, children: [notice] });
|
|
334
|
+
const { payload } = ownState.list;
|
|
335
|
+
const children = [];
|
|
336
|
+
if (payload.projects.length === 0) {
|
|
337
|
+
children.push(makeElement(doc, 'p', { className: 'bb-graph-empty', text: 'この束には、プロジェクトの記録がありません。' }));
|
|
338
|
+
} else {
|
|
339
|
+
children.push(workspaceLedger(doc, {
|
|
340
|
+
className: 'bb-graph-project-ledger bb-graph-own-ledger',
|
|
341
|
+
ariaLabel: `${own.label}のプロジェクト`,
|
|
342
|
+
columns: GRAPH_PROJECT_LEDGER_COLUMNS,
|
|
343
|
+
rows: payload.projects.map((project) => ({
|
|
344
|
+
key: project.id,
|
|
345
|
+
selected: project.id === ownState.selectedId,
|
|
346
|
+
className: project.active ? '' : 'is-ended',
|
|
347
|
+
onSelect: (id) => { void controller.selectOwnProject(id); },
|
|
348
|
+
cells: [
|
|
349
|
+
{ primary: project.name, secondary: project.id },
|
|
350
|
+
textCell(project.goal, '目的未記入'),
|
|
351
|
+
accountableCell(project),
|
|
352
|
+
textCell(project.status, '未記入'),
|
|
353
|
+
`${project.participantCount}人`,
|
|
354
|
+
validityLabel(project, payload.asOf),
|
|
355
|
+
],
|
|
356
|
+
})),
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
if (ownState.selectedId) {
|
|
360
|
+
children.push(ownProjectDetail(doc, ownState.detail?.id === ownState.selectedId ? ownState.detail : { id: ownState.selectedId, state: 'loading' }, {
|
|
361
|
+
onClose: () => controller.selectOwnProject(null),
|
|
362
|
+
onRetry: () => controller.selectOwnProject(ownState.selectedId),
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
return ownShareSection(doc, { label: own.label, handover: payload.handover, children });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function renderHostWorkspace() {
|
|
369
|
+
const children = [workspacePageHeader(doc, {
|
|
370
|
+
crumbs: context.crumbs,
|
|
371
|
+
title: 'プロジェクトと関係者',
|
|
372
|
+
lead: writable ? 'どのプロジェクトに誰がどう関わっているかを確かめ、誤りを直します。' : 'どのプロジェクトに誰がどう関わっているかを確かめます。',
|
|
373
|
+
source: context.source,
|
|
374
|
+
actions: workspaceHostActions(pageActions),
|
|
375
|
+
})];
|
|
376
|
+
const dir = state.list.state === 'ok' ? textOrNull(state.list.payload.source?.dataDir) : null;
|
|
377
|
+
children.push(workspaceHostNotice(doc, sourceNotice) ?? workspaceNotice(doc, {
|
|
378
|
+
label: '出典',
|
|
379
|
+
text: `このMacのGraph${dir ? `(${dir})` : ''}から読み、ここで直した内容もそこへ保存します。関係者は、プロジェクトに参加している人や責任を持つ人の記録で、ログインや共有の設定ではありません。`,
|
|
380
|
+
}));
|
|
381
|
+
if (state.detail?.state === 'ok') children.push(renderGraphIssues(doc, state.detail.payload.issues));
|
|
382
|
+
|
|
383
|
+
const stateNotice = graphStateNotice(doc, state.list, { onRetry: () => controller.load(), loadingText: 'プロジェクトを読み込んでいます。' });
|
|
384
|
+
if (stateNotice) {
|
|
385
|
+
children.push(stateNotice);
|
|
386
|
+
return children;
|
|
387
|
+
}
|
|
388
|
+
const { payload } = state.list;
|
|
389
|
+
const summary = summarizeProjects(payload.projects);
|
|
390
|
+
const extras = hostMetrics(payload);
|
|
391
|
+
children.push(workspaceMetrics(doc, [
|
|
392
|
+
{ label: 'プロジェクト', value: summary.active, note: `有効なもの(全${summary.total}件)` },
|
|
393
|
+
{ label: '進行中', value: summary.inProgress, note: '状態が「進行中」の有効なもの' },
|
|
394
|
+
{ label: '関係者', value: summary.people, note: summary.people === null ? '人数を確かめられません' : '有効な関わりがある人(重複なし)' },
|
|
395
|
+
...extras.metrics,
|
|
396
|
+
], { ariaLabel: 'プロジェクトの集計' }));
|
|
397
|
+
if (extras.failure) children.push(extras.failure);
|
|
398
|
+
|
|
399
|
+
if (payload.projects.length === 0) {
|
|
400
|
+
const hostEmpty = graphHostEmptyNotice(doc, emptyNotice);
|
|
401
|
+
if (hostEmpty) {
|
|
402
|
+
children.push(hostEmpty);
|
|
403
|
+
return children;
|
|
404
|
+
}
|
|
405
|
+
const body = makeElement(doc, 'div', { className: 'bb-graph-notice-body' });
|
|
406
|
+
body.append(
|
|
407
|
+
makeElement(doc, 'p', { text: 'まだ登録がありません。このGraphには、プロジェクトがまだ登録されていません。次のコマンドで登録できます。CodexやClaude Codeからは、MCPのオンボーディング(brainbase_onboarding_start)で資料から候補を作り、確かめてから登録できます。' }),
|
|
408
|
+
graphCommandList(doc, [`brainbase onboard:projects --name <名前> --goal <目的> --write${dir ? ` --dir ${shellArg(dir)}` : ''}`]),
|
|
409
|
+
);
|
|
410
|
+
children.push(workspaceNotice(doc, { label: '未登録', text: body, tone: 'info', role: 'status' }));
|
|
411
|
+
return children;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
children.push(workspaceLedger(doc, {
|
|
415
|
+
className: 'bb-graph-project-ledger',
|
|
416
|
+
ariaLabel: 'プロジェクトの一覧',
|
|
417
|
+
columns: GRAPH_PROJECT_LEDGER_COLUMNS,
|
|
418
|
+
rows: payload.projects.map((project) => ({
|
|
419
|
+
key: project.id,
|
|
420
|
+
selected: project.id === state.selectedId,
|
|
421
|
+
className: project.active ? '' : 'is-ended',
|
|
422
|
+
onSelect: (id) => {
|
|
423
|
+
focusRow = id;
|
|
424
|
+
const changed = id !== state.selectedId;
|
|
425
|
+
const reading = controller.selectProject(id);
|
|
426
|
+
if (changed) notifySelect(id);
|
|
427
|
+
void reading;
|
|
428
|
+
},
|
|
429
|
+
cells: [
|
|
430
|
+
{ primary: project.name, secondary: project.id },
|
|
431
|
+
textCell(project.goal, '目的未記入'),
|
|
432
|
+
accountableCell(project),
|
|
433
|
+
textCell(project.status, '未記入'),
|
|
434
|
+
`${project.participantCount}人`,
|
|
435
|
+
validityLabel(project, payload.asOf),
|
|
436
|
+
],
|
|
437
|
+
})),
|
|
438
|
+
}));
|
|
439
|
+
return children;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// -------------------------------------------------------------------------
|
|
443
|
+
// Right rail
|
|
444
|
+
|
|
445
|
+
function participantItem(edge, project, asOf) {
|
|
446
|
+
const item = makeElement(doc, 'li', { className: `bb-graph-compact${edge.active ? '' : ' is-ended'}` });
|
|
447
|
+
const head = makeElement(doc, 'div', { className: 'bb-graph-compact-head' });
|
|
448
|
+
const how = ownLabel(PARTICIPATION_LABELS, edge.relation) ?? relationLabel(edge.relation);
|
|
449
|
+
head.append(
|
|
450
|
+
makeElement(doc, 'strong', { className: 'bb-graph-compact-title', text: edge.counterpart.name }),
|
|
451
|
+
badge(doc, how, edge.relation === 'accountable_for' ? 'accent' : ''),
|
|
452
|
+
activityBadge(doc, edge, asOf),
|
|
453
|
+
);
|
|
454
|
+
item.append(head);
|
|
455
|
+
if (edge.counterpart.active === false) item.append(makeElement(doc, 'small', { className: 'bb-graph-help', text: '人物の記録は終了しています' }));
|
|
456
|
+
item.append(workspaceDefinition(doc, [
|
|
457
|
+
['役割', textOrNull(edge.role)],
|
|
458
|
+
...(textOrNull(edge.context) ? [['文脈', edge.context]] : []),
|
|
459
|
+
['有効期間', validityText(edge.validFrom, edge.validTo)],
|
|
460
|
+
['出典', renderProvenance(doc, edge.provenance)],
|
|
461
|
+
]));
|
|
462
|
+
const subject = `${edge.counterpart.name}(${how}、${project.name})`;
|
|
463
|
+
if (edgesWritable) {
|
|
464
|
+
item.append(workspaceActions(doc, [
|
|
465
|
+
workspaceButton(doc, { text: '役割を直す', variant: 'quiet', onClick: () => openCorrection(edge.id, () => correction.openEdge(edge, { mode: 'role', subject })) }),
|
|
466
|
+
edge.active
|
|
467
|
+
? workspaceButton(doc, { text: '関わりを終える', variant: 'quiet', onClick: () => openCorrection(edge.id, () => correction.openEdge(edge, { mode: 'end', subject })) })
|
|
468
|
+
: workspaceButton(doc, { text: '終了日を直す', variant: 'quiet', onClick: () => openCorrection(edge.id, () => correction.openEdge(edge, { mode: 'end', title: '終了日を直す', subject })) }),
|
|
469
|
+
]));
|
|
470
|
+
}
|
|
471
|
+
item.append(recordDetails(doc, [['関係ID', edge.id], ['人物ID', edge.counterpart.id], ['digest', edge.digest]]));
|
|
472
|
+
if (edgesWritable && state.panelAt === edge.id) {
|
|
473
|
+
const panel = correction.render(doc);
|
|
474
|
+
if (panel) item.append(panel);
|
|
475
|
+
}
|
|
476
|
+
return item;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function renderParticipants(payload) {
|
|
480
|
+
const { project } = payload;
|
|
481
|
+
const content = [makeElement(doc, 'p', { className: 'bb-graph-block-lead', text: '参加している人と、責任を持つ人です。終わった関わりも、終了日つきで残ります。' })];
|
|
482
|
+
if (payload.participants.length === 0) {
|
|
483
|
+
content.push(makeElement(doc, 'p', { className: 'bb-graph-empty', text: 'このプロジェクトには、まだ関係者の登録がありません。' }));
|
|
484
|
+
} else {
|
|
485
|
+
const list = makeElement(doc, 'ul', { className: 'bb-graph-compact-list' });
|
|
486
|
+
for (const edge of payload.participants) list.append(participantItem(edge, project, payload.asOf));
|
|
487
|
+
content.push(list);
|
|
488
|
+
}
|
|
489
|
+
return workspaceRailBlock(doc, { title: '関係者', content });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function renderOtherRelations(payload) {
|
|
493
|
+
const relations = Array.isArray(payload.relations) ? payload.relations : [];
|
|
494
|
+
if (relations.length === 0) return null;
|
|
495
|
+
const list = makeElement(doc, 'ul', { className: 'bb-graph-plain-list' });
|
|
496
|
+
for (const edge of relations) {
|
|
497
|
+
const labels = ownLabel(GRAPH_RELATION_LABELS, edge.relation);
|
|
498
|
+
const phrase = labels ? (edge.direction === 'outgoing' ? labels.outgoing : labels.incoming) : '';
|
|
499
|
+
const item = makeElement(doc, 'li');
|
|
500
|
+
item.append(badge(doc, relationLabel(edge.relation), 'muted'), makeElement(doc, 'span', { text: ` ${edge.counterpart.name}${phrase} ` }), activityBadge(doc, edge, payload.asOf));
|
|
501
|
+
list.append(item);
|
|
502
|
+
}
|
|
503
|
+
return workspaceRailBlock(doc, {
|
|
504
|
+
title: 'そのほかの関係',
|
|
505
|
+
content: [makeElement(doc, 'p', {
|
|
506
|
+
className: 'bb-graph-block-lead',
|
|
507
|
+
text: edgesWritable ? '所有する組織や、進め方を決める判断などです。直すときは「情報と関係」を使います。' : '所有する組織や、進め方を決める判断などです。',
|
|
508
|
+
}), list],
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function statusValue(project, asOf) {
|
|
513
|
+
const wrap = makeElement(doc, 'span', { className: 'bb-graph-inline-value' });
|
|
514
|
+
wrap.append(makeElement(doc, 'span', { text: textOrNull(project.status) ?? '未記入' }), activityBadge(doc, project, asOf));
|
|
515
|
+
return wrap;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderRail() {
|
|
519
|
+
// The first project is selected once the list is read; until then the rail stays empty
|
|
520
|
+
// (a project the host asked for is shown only once its detail is being read).
|
|
521
|
+
if (state.list.state === 'loading' && state.detail?.id !== state.selectedId) return [];
|
|
522
|
+
if (!state.selectedId) {
|
|
523
|
+
return [workspaceDetailEmpty(doc, {
|
|
524
|
+
mark: 'P',
|
|
525
|
+
title: 'プロジェクトを選択',
|
|
526
|
+
text: '一覧から選ぶと、目的・判断の原則と、関係者の役割・有効期間・出典を表示します。',
|
|
527
|
+
})];
|
|
528
|
+
}
|
|
529
|
+
const detail = state.detail;
|
|
530
|
+
const loaded = detail?.id === state.selectedId && detail.state === 'ok' ? detail.payload : null;
|
|
531
|
+
const project = loaded?.project ?? listItem(state.selectedId) ?? { id: state.selectedId, name: state.selectedId };
|
|
532
|
+
const children = [workspaceRailHead(doc, { kicker: 'プロジェクト', title: project.name, sub: project.id })];
|
|
533
|
+
if (!loaded) {
|
|
534
|
+
children.push(graphStateNotice(doc, detail?.id === state.selectedId ? detail : { state: 'loading' }, {
|
|
535
|
+
onRetry: () => controller.loadDetail(state.selectedId),
|
|
536
|
+
loadingText: 'このプロジェクトの関係者を読み込んでいます。',
|
|
537
|
+
}));
|
|
538
|
+
children.push(...railExtensions(state.selectedId, null));
|
|
539
|
+
return children;
|
|
540
|
+
}
|
|
541
|
+
const principles = Array.isArray(project.decisionPrinciples) ? project.decisionPrinciples.filter((item) => typeof item === 'string' && item.trim()) : [];
|
|
542
|
+
let principleList = null;
|
|
543
|
+
if (principles.length > 0) {
|
|
544
|
+
principleList = makeElement(doc, 'ul', { className: 'bb-graph-plain-list' });
|
|
545
|
+
for (const principle of principles) principleList.append(makeElement(doc, 'li', { text: principle }));
|
|
546
|
+
}
|
|
547
|
+
children.push(workspaceRailBlock(doc, {
|
|
548
|
+
title: '概要',
|
|
549
|
+
content: [
|
|
550
|
+
workspaceDefinition(doc, [
|
|
551
|
+
['状態', statusValue(project, loaded.asOf)],
|
|
552
|
+
['目的', textOrNull(project.goal)],
|
|
553
|
+
['判断の原則', principleList ?? { text: 'まだ登録がありません', className: 'is-unrecorded' }],
|
|
554
|
+
['有効期間', validityText(project.validFrom, project.validTo)],
|
|
555
|
+
// A host with its own source names it in its notice; this row names only this Mac's Graph.
|
|
556
|
+
...(isRecord(sourceNotice) ? [] : [['出典', 'このMacのGraph(graph.json)']]),
|
|
557
|
+
]),
|
|
558
|
+
recordDetails(doc, [['プロジェクトID', project.id], ['digest', project.digest], ['読み取った時点', loaded.asOf]]),
|
|
559
|
+
],
|
|
560
|
+
}));
|
|
561
|
+
children.push(renderParticipants(loaded));
|
|
562
|
+
if (writable) {
|
|
563
|
+
const canAdd = scope.createEdges;
|
|
564
|
+
const projectFields = scope.entity('project') ? scope.fields(['name', 'goal', 'status']) : [];
|
|
565
|
+
const actions = [
|
|
566
|
+
canAdd ? workspaceButton(doc, {
|
|
567
|
+
text: '関係者を加える',
|
|
568
|
+
variant: 'primary',
|
|
569
|
+
onClick: () => openCorrection('project', () => correction.openCreate(project, {
|
|
570
|
+
relations: ['participates_in', 'accountable_for'],
|
|
571
|
+
relationNames: PARTICIPATION_LABELS,
|
|
572
|
+
relationLabel: '関わり方',
|
|
573
|
+
title: '関係者を加える',
|
|
574
|
+
subject: `プロジェクト「${project.name}」`,
|
|
575
|
+
})),
|
|
576
|
+
}) : null,
|
|
577
|
+
projectFields.length > 0 ? workspaceButton(doc, {
|
|
578
|
+
text: 'プロジェクトを直す',
|
|
579
|
+
onClick: () => openCorrection('project', () => correction.openEntity(project, {
|
|
580
|
+
fields: projectFields,
|
|
581
|
+
title: 'プロジェクトを直す',
|
|
582
|
+
subject: `プロジェクト「${project.name}」の${projectFields.map((field) => GRAPH_CORRECTION_FIELDS[field].label).join('・')}`,
|
|
583
|
+
})),
|
|
584
|
+
}) : null,
|
|
585
|
+
].filter(Boolean);
|
|
586
|
+
if (actions.length > 0) children.push(workspaceActions(doc, actions));
|
|
587
|
+
// A correction opened on a 関係者 shows inside that item; any other shows here.
|
|
588
|
+
if (!loaded.participants.some((edge) => edge.id === state.panelAt)) children.push(correction.render(doc));
|
|
589
|
+
// The host's reason for a control it withholds, where that control would be.
|
|
590
|
+
const withheld = !canAdd || projectFields.length === 0 || (!scope.edges && loaded.participants.length > 0);
|
|
591
|
+
if (withheld) children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
592
|
+
} else {
|
|
593
|
+
children.push(hostReadOnlyNote(doc, readOnlyNote));
|
|
594
|
+
}
|
|
595
|
+
children.push(...railExtensions(state.selectedId, loaded));
|
|
596
|
+
children.push(renderOtherRelations(loaded));
|
|
597
|
+
children.push(renderCorrectionHistory(doc, loaded.history));
|
|
598
|
+
return children;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Reads the host's project list (see `load`). */
|
|
602
|
+
async function loadHostList({ keep = false } = {}) {
|
|
603
|
+
if (!(keep && state.list.state === 'ok')) {
|
|
604
|
+
state.list = { state: 'loading' };
|
|
605
|
+
controller.render();
|
|
606
|
+
}
|
|
607
|
+
state.list = normalizeProjectList(await client.read('/projects'));
|
|
608
|
+
if (state.list.state !== 'ok') {
|
|
609
|
+
if (!keep) state.selectedId = null;
|
|
610
|
+
controller.render();
|
|
611
|
+
return state.list;
|
|
612
|
+
}
|
|
613
|
+
const { projects } = state.list.payload;
|
|
614
|
+
if (!state.selectedId || !projects.some((project) => project.id === state.selectedId)) {
|
|
615
|
+
if (correction.form) correction.close();
|
|
616
|
+
state.panelAt = null;
|
|
617
|
+
state.detail = null;
|
|
618
|
+
state.selectedId = projects[0]?.id ?? null;
|
|
619
|
+
if (state.selectedId) {
|
|
620
|
+
const reading = controller.loadDetail(state.selectedId);
|
|
621
|
+
notifySelect(state.selectedId);
|
|
622
|
+
await reading;
|
|
623
|
+
return state.list;
|
|
624
|
+
}
|
|
625
|
+
} else if (state.detail?.id !== state.selectedId) {
|
|
626
|
+
// The project the host asked for is listed; read it for the rail.
|
|
627
|
+
await controller.loadDetail(state.selectedId);
|
|
628
|
+
return state.list;
|
|
629
|
+
}
|
|
630
|
+
controller.render();
|
|
631
|
+
return state.list;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const controller = {
|
|
635
|
+
get state() { return state; },
|
|
636
|
+
get correction() { return correction; },
|
|
637
|
+
render() {
|
|
638
|
+
layout.render(renderWorkspace(), renderRail());
|
|
639
|
+
if (focusRow) {
|
|
640
|
+
focusLedgerRow(root, focusRow);
|
|
641
|
+
focusRow = null;
|
|
642
|
+
}
|
|
643
|
+
return controller;
|
|
644
|
+
},
|
|
645
|
+
/**
|
|
646
|
+
* Reads the list; the first project is selected when none is. `keep` shows the current list until
|
|
647
|
+
* the new one has been read. The owner's own share is read alongside, unless `keep` finds it read.
|
|
648
|
+
*/
|
|
649
|
+
async load({ keep = false } = {}) {
|
|
650
|
+
const owning = own && !(keep && state.own.list.state === 'ok') ? controller.loadOwn() : null;
|
|
651
|
+
const [list] = await Promise.all([loadHostList({ keep }), owning]);
|
|
652
|
+
return list;
|
|
653
|
+
},
|
|
654
|
+
/** Reads the owner's own projects (with `ownShare`); a failure stays in that section. */
|
|
655
|
+
async loadOwn() {
|
|
656
|
+
if (!own) return null;
|
|
657
|
+
state.own.list = { state: 'loading' };
|
|
658
|
+
controller.render();
|
|
659
|
+
const result = normalizeProjectList(await own.client.read('/projects'));
|
|
660
|
+
// `not_handed_over` passes through as its own state.
|
|
661
|
+
state.own.list = result;
|
|
662
|
+
if (result.state !== 'ok' || !result.payload.projects.some((project) => project.id === state.own.selectedId)) {
|
|
663
|
+
state.own.selectedId = null;
|
|
664
|
+
state.own.detail = null;
|
|
665
|
+
}
|
|
666
|
+
controller.render();
|
|
667
|
+
return state.own.list;
|
|
668
|
+
},
|
|
669
|
+
/** Opens (or with null closes) a project of the owner's own share below its ledger. */
|
|
670
|
+
async selectOwnProject(id) {
|
|
671
|
+
if (!own) return null;
|
|
672
|
+
const target = textOrNull(id);
|
|
673
|
+
state.own.selectedId = target;
|
|
674
|
+
if (!target) {
|
|
675
|
+
state.own.detail = null;
|
|
676
|
+
controller.render();
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
state.own.detail = { id: target, state: 'loading' };
|
|
680
|
+
controller.render();
|
|
681
|
+
const result = await own.client.read(`/projects/${encodeURIComponent(target)}`);
|
|
682
|
+
if (state.own.selectedId !== target) return state.own.detail;
|
|
683
|
+
state.own.detail = result.state === 'error' && result.status === 404
|
|
684
|
+
? { id: target, state: 'not_found', reason: 'このプロジェクトは束の中に見つかりません。' }
|
|
685
|
+
: { id: target, ...normalizeProjectDetail(result) };
|
|
686
|
+
controller.render();
|
|
687
|
+
return state.own.detail;
|
|
688
|
+
},
|
|
689
|
+
/** `keep` shows the current detail until the new one has been read. */
|
|
690
|
+
async loadDetail(id, { keep = false } = {}) {
|
|
691
|
+
const previous = state.detail;
|
|
692
|
+
if (!(keep && previous?.id === id && previous.state === 'ok')) {
|
|
693
|
+
state.detail = { id, state: 'loading' };
|
|
694
|
+
controller.render();
|
|
695
|
+
}
|
|
696
|
+
const result = await client.read(`/projects/${encodeURIComponent(id)}`);
|
|
697
|
+
if (state.selectedId !== id) return state.detail;
|
|
698
|
+
state.detail = result.state === 'error' && result.status === 404
|
|
699
|
+
? { id, state: 'not_found', reason: 'このプロジェクトは見つかりません。一覧を読み直してください。' }
|
|
700
|
+
: { id, ...normalizeProjectDetail(result) };
|
|
701
|
+
controller.render();
|
|
702
|
+
return state.detail;
|
|
703
|
+
},
|
|
704
|
+
/** Selects a row: the rail shows that project, and an open correction is closed. */
|
|
705
|
+
async selectProject(id) {
|
|
706
|
+
if (correction.form) correction.close();
|
|
707
|
+
state.panelAt = null;
|
|
708
|
+
state.selectedId = id;
|
|
709
|
+
return controller.loadDetail(id, { keep: true });
|
|
710
|
+
},
|
|
711
|
+
/** The same as selecting the project's row. */
|
|
712
|
+
async openProject(id) {
|
|
713
|
+
return controller.selectProject(id);
|
|
714
|
+
},
|
|
715
|
+
/**
|
|
716
|
+
* Selects a project from the host's own navigation without calling `onSelect`.
|
|
717
|
+
* Selecting the project already selected changes nothing (an open correction stays).
|
|
718
|
+
*/
|
|
719
|
+
async select(id) {
|
|
720
|
+
const target = textOrNull(id);
|
|
721
|
+
if (!target || target === state.selectedId) return state.detail;
|
|
722
|
+
return controller.selectProject(target);
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
controller.render();
|
|
726
|
+
if (autoLoad) void controller.load();
|
|
727
|
+
return controller;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export default createGraphProjectsView;
|