dsh-homepage 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cards/sidebar-list.js +317 -42
- package/lib/client.js +358 -48
- package/lib/framework.part.js +41 -6
- package/package.json +1 -1
|
@@ -117,6 +117,28 @@
|
|
|
117
117
|
if (value === null || value === undefined) return ''
|
|
118
118
|
return String(value)
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* @param value - a workspace view, a workspace id, or anything else.
|
|
122
|
+
* @returns the workspace id it carries, or '' when it names none. The
|
|
123
|
+
* client service answers `workspaceId`; a host that answers `id` is the
|
|
124
|
+
* one other spelling this region accepts.
|
|
125
|
+
*/
|
|
126
|
+
function sbWorkspaceId(value) {
|
|
127
|
+
if (typeof value === 'string') return value
|
|
128
|
+
if (!value || typeof value !== 'object') return ''
|
|
129
|
+
return sbText(value.workspaceId || value.id)
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* @param value - an ISO-8601 instant, epoch milliseconds, or anything else.
|
|
133
|
+
* @returns epoch milliseconds, or 0 when it names no time.
|
|
134
|
+
*/
|
|
135
|
+
function sbEpoch(value) {
|
|
136
|
+
if (typeof value === 'number') return isFinite(value) && value > 0 ? value : 0
|
|
137
|
+
var text = sbText(value)
|
|
138
|
+
if (text === '') return 0
|
|
139
|
+
var parsed = Date.parse(text)
|
|
140
|
+
return isFinite(parsed) && parsed > 0 ? parsed : 0
|
|
141
|
+
}
|
|
120
142
|
/** @param value - a path. @returns its last segment, both separators accepted. */
|
|
121
143
|
function sbBase(value) {
|
|
122
144
|
var path = sbText(value)
|
|
@@ -223,19 +245,22 @@
|
|
|
223
245
|
return draw(size || 16)
|
|
224
246
|
}
|
|
225
247
|
/**
|
|
226
|
-
* A project's own recency: the newest session it shows
|
|
227
|
-
*
|
|
228
|
-
*
|
|
248
|
+
* A project's own recency: the newest session it shows, or when the
|
|
249
|
+
* workspace itself was created — whichever is later. A project with no
|
|
250
|
+
* sessions is not an old project: registering it is an event, so it must
|
|
251
|
+
* not sink to the bottom of a section ordered by recency the moment it is
|
|
252
|
+
* added. Borrowing a neighbour's time is still wrong, which is what the
|
|
253
|
+
* creation instant keeps from happening.
|
|
229
254
|
*
|
|
230
255
|
* A blank row is skipped: it is the draft the shell holds for a workspace
|
|
231
256
|
* you focused, never sent, so it is not an event and cannot make a project
|
|
232
257
|
* recent. All its `updatedAt` ever says is when the draft was made.
|
|
233
258
|
* @param project - a projected project.
|
|
234
|
-
* @returns the newest
|
|
259
|
+
* @returns the newest instant the project stands for, or 0.
|
|
235
260
|
*/
|
|
236
261
|
function sbProjectAt(project) {
|
|
262
|
+
var at = Number(project && project.createdAt) || 0
|
|
237
263
|
var rows = Array.isArray(project && project.rows) ? project.rows : []
|
|
238
|
-
var at = 0
|
|
239
264
|
for (var i = 0; i < rows.length; i += 1) {
|
|
240
265
|
if (rows[i] && rows[i].blank === true) continue
|
|
241
266
|
var value = Number(rows[i] && rows[i].updatedAt) || 0
|
|
@@ -273,12 +298,19 @@
|
|
|
273
298
|
* ungrouped bucket. Members are the projects the sidebar listed plus any
|
|
274
299
|
* the document still names, so a stale assignment stays visible and
|
|
275
300
|
* removable instead of silently disappearing.
|
|
301
|
+
*
|
|
302
|
+
* A member the projection has not carried yet — one this region created a
|
|
303
|
+
* moment ago — is drawn from the `fresh` memory instead: without it the row
|
|
304
|
+
* has no title and no path, so it reads as the workspace id, and it is
|
|
305
|
+
* marked as a stale assignment it is not.
|
|
276
306
|
* @param layout - the document, or null.
|
|
277
307
|
* @param items - the projected projects.
|
|
308
|
+
* @param fresh - `{ [id]: { title, path, createdAt } }` for just-created workspaces.
|
|
278
309
|
* @returns the sections, each `{ key, title, color, items, collapsed, sort }`.
|
|
279
310
|
*/
|
|
280
|
-
function sbSections(layout, items) {
|
|
311
|
+
function sbSections(layout, items, fresh) {
|
|
281
312
|
var projects = Array.isArray(items) ? items : []
|
|
313
|
+
var memory = fresh && typeof fresh === 'object' ? fresh : null
|
|
282
314
|
var groups = kit.sidebarGroups(layout).slice().sort(function (left, right) {
|
|
283
315
|
var a = left && typeof left.order === 'number' ? left.order : 0
|
|
284
316
|
var b = right && typeof right.order === 'number' ? right.order : 0
|
|
@@ -310,7 +342,16 @@
|
|
|
310
342
|
if (known[id]) return
|
|
311
343
|
var member = members[id]
|
|
312
344
|
var section = member && member.group && byId[member.group] ? byId[member.group] : loose
|
|
313
|
-
|
|
345
|
+
var made = memory && memory[id] ? memory[id] : null
|
|
346
|
+
section.items.push({
|
|
347
|
+
id: id,
|
|
348
|
+
title: made ? sbText(made.title) : '',
|
|
349
|
+
path: made ? sbText(made.path) : '',
|
|
350
|
+
sessions: 0,
|
|
351
|
+
rows: [],
|
|
352
|
+
createdAt: made ? Number(made.createdAt) || 0 : 0,
|
|
353
|
+
orphan: !made,
|
|
354
|
+
})
|
|
314
355
|
})
|
|
315
356
|
var out = sections.slice()
|
|
316
357
|
if (loose.items.length > 0) out.push(loose)
|
|
@@ -937,11 +978,25 @@
|
|
|
937
978
|
}
|
|
938
979
|
}, [])
|
|
939
980
|
|
|
940
|
-
/**
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
981
|
+
/**
|
|
982
|
+
* The folder this dialog would register as a workspace: the path in the
|
|
983
|
+
* open field — a typed path is a target even before Enter has walked the
|
|
984
|
+
* list to it — and the level on screen when the field is closed or blank.
|
|
985
|
+
* Without the draft the two disagree: the operator types a folder, clicks
|
|
986
|
+
* the confirm, and the workspace is made in whatever level happened to be
|
|
987
|
+
* listed underneath, which reads as "it ignored what I typed".
|
|
988
|
+
* @returns an absolute path, or '' when there is nothing to name.
|
|
989
|
+
*/
|
|
990
|
+
function targetPath() {
|
|
991
|
+
var draft = state.pathDraft === null ? '' : sbText(state.pathDraft).trim()
|
|
992
|
+
if (draft !== '') return draft
|
|
993
|
+
return sbText(state.listing && state.listing.path)
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Register one directory as a workspace and hand it back to the caller.
|
|
997
|
+
* @param path - an absolute directory the Host has already accepted.
|
|
998
|
+
*/
|
|
999
|
+
function commit(path) {
|
|
945
1000
|
var projects = sbService('workspaces')
|
|
946
1001
|
if (!projects || typeof projects.create !== 'function') {
|
|
947
1002
|
patch({ error: sbT('sidebar.list.readonly'), phase: 'error' })
|
|
@@ -956,16 +1011,68 @@
|
|
|
956
1011
|
return
|
|
957
1012
|
}
|
|
958
1013
|
if (!outcome || typeof outcome.then !== 'function') {
|
|
959
|
-
onPicked()
|
|
1014
|
+
onPicked(outcome)
|
|
960
1015
|
return
|
|
961
1016
|
}
|
|
962
|
-
outcome.then(function () {
|
|
963
|
-
if (alive.current) onPicked()
|
|
1017
|
+
outcome.then(function (created) {
|
|
1018
|
+
if (alive.current) onPicked(created)
|
|
964
1019
|
}, function (error) {
|
|
965
1020
|
if (!alive.current) return
|
|
966
1021
|
patch({ busy: false, phase: 'error', error: sbT('sidebar.list.failed', { reason: sbReason(error) }) })
|
|
967
1022
|
})
|
|
968
1023
|
}
|
|
1024
|
+
/**
|
|
1025
|
+
* The confirm action. It commits the folder it names, and a typed path is
|
|
1026
|
+
* read through the Host first: the listing proves the directory exists
|
|
1027
|
+
* and answers with its canonical spelling, which is what the workspace has
|
|
1028
|
+
* to store (`..`, a trailing separator, a Windows forward slash).
|
|
1029
|
+
*/
|
|
1030
|
+
function adopt() {
|
|
1031
|
+
var target = targetPath()
|
|
1032
|
+
if (target === '') return
|
|
1033
|
+
if (state.busy) return
|
|
1034
|
+
if (target === sbText(state.listing && state.listing.path)) {
|
|
1035
|
+
commit(target)
|
|
1036
|
+
return
|
|
1037
|
+
}
|
|
1038
|
+
var service = sbService('uiWorkspace')
|
|
1039
|
+
if (!service || typeof service.listDirectory !== 'function') {
|
|
1040
|
+
// No browsing capability behind the field: the Host still owns
|
|
1041
|
+
// `create`, and it is the one that validates the path.
|
|
1042
|
+
commit(target)
|
|
1043
|
+
return
|
|
1044
|
+
}
|
|
1045
|
+
patch({ busy: true, error: '' })
|
|
1046
|
+
var outcome = null
|
|
1047
|
+
try {
|
|
1048
|
+
outcome = service.listDirectory(target)
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
patch({
|
|
1051
|
+
busy: false,
|
|
1052
|
+
phase: 'error',
|
|
1053
|
+
pathDraft: target,
|
|
1054
|
+
error: sbT('sidebar.list.dirFailed', { reason: sbReason(error) }),
|
|
1055
|
+
})
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
1058
|
+
if (!outcome || typeof outcome.then !== 'function') {
|
|
1059
|
+
var resolved = sbText(outcome && outcome.path) || target
|
|
1060
|
+
commit(resolved)
|
|
1061
|
+
return
|
|
1062
|
+
}
|
|
1063
|
+
outcome.then(function (listing) {
|
|
1064
|
+
if (!alive.current) return
|
|
1065
|
+
commit(sbText(listing && listing.path) || target)
|
|
1066
|
+
}, function (error) {
|
|
1067
|
+
if (!alive.current) return
|
|
1068
|
+
patch({
|
|
1069
|
+
busy: false,
|
|
1070
|
+
phase: 'error',
|
|
1071
|
+
pathDraft: target,
|
|
1072
|
+
error: sbT('sidebar.list.dirFailed', { reason: sbReason(error) }),
|
|
1073
|
+
})
|
|
1074
|
+
})
|
|
1075
|
+
}
|
|
969
1076
|
/** Create the folder named in the field, then open it. */
|
|
970
1077
|
function mint() {
|
|
971
1078
|
var name = sbText(state.folder).trim()
|
|
@@ -1067,8 +1174,17 @@
|
|
|
1067
1174
|
patch({ pathDraft: here })
|
|
1068
1175
|
}, { key: 'edit', sb: 'dirEditPath', size: 14, className: 'ph-sb-dir-edit' }),
|
|
1069
1176
|
])
|
|
1177
|
+
// What the confirm action will register, said out loud under the path it
|
|
1178
|
+
// reads: a field whose contents are not the folder that will be created is
|
|
1179
|
+
// the trap this line exists to remove.
|
|
1180
|
+
var target = targetPath()
|
|
1181
|
+
var targetLine = target === ''
|
|
1182
|
+
? null
|
|
1183
|
+
: h('div', { className: 'ph-sb-dir-note ph-sb-dir-target', key: 'target', 'data-sb': 'dirTarget' },
|
|
1184
|
+
sbT('sidebar.list.dirTarget', { path: target }))
|
|
1070
1185
|
var body = h('div', { className: 'ph-sb-dir', key: 'dir' }, [
|
|
1071
1186
|
pathRow,
|
|
1187
|
+
targetLine,
|
|
1072
1188
|
state.phase === 'loading'
|
|
1073
1189
|
? h('div', { className: 'ph-sb-dir-note', key: 'loading' }, sbT('sidebar.list.dirLoading'))
|
|
1074
1190
|
: null,
|
|
@@ -1134,7 +1250,9 @@
|
|
|
1134
1250
|
? h('div', { className: 'ph-sb-form-error', key: 'error', 'data-sb': 'dirError' }, state.error)
|
|
1135
1251
|
: null,
|
|
1136
1252
|
])
|
|
1137
|
-
|
|
1253
|
+
// A typed path is a target of its own: the confirm action stays live for
|
|
1254
|
+
// it even when the level underneath could not be listed at all.
|
|
1255
|
+
var choose = target !== ''
|
|
1138
1256
|
// The same footer the shipped browser draws: the two actions that change
|
|
1139
1257
|
// the folder on the left, the confirm/cancel pair on the right, and the
|
|
1140
1258
|
// hidden-items toggle between them — its label never shifts, because the
|
|
@@ -1312,7 +1430,18 @@
|
|
|
1312
1430
|
var store = kit.useSidebarLayout()
|
|
1313
1431
|
var layout = store.layout || null
|
|
1314
1432
|
var items = Array.isArray(store.workspaces) ? store.workspaces : []
|
|
1315
|
-
|
|
1433
|
+
/**
|
|
1434
|
+
* The workspaces this region created a moment ago, keyed by id, with the
|
|
1435
|
+
* title, path and creation instant the Host answered with. The projection
|
|
1436
|
+
* that carries the same facts is published by an effect in another React
|
|
1437
|
+
* root, so for the first render after a create the document is all this
|
|
1438
|
+
* region has — and a member with no title and no path draws as its
|
|
1439
|
+
* workspace id, which is not the folder the reader just picked.
|
|
1440
|
+
*/
|
|
1441
|
+
var freshPair = sbReact.useState({})
|
|
1442
|
+
var fresh = freshPair[0]
|
|
1443
|
+
var setFresh = freshPair[1]
|
|
1444
|
+
var sections = sbSections(layout, items, fresh)
|
|
1316
1445
|
var current = sbText(store.current)
|
|
1317
1446
|
var ready = sbKitReady()
|
|
1318
1447
|
|
|
@@ -1357,9 +1486,11 @@
|
|
|
1357
1486
|
* The region's own directory browser, when it is up. The Host's native
|
|
1358
1487
|
* chooser cannot exist for a browser connected to another address, so the
|
|
1359
1488
|
* picker that is missing its Host half falls back to this dialog instead
|
|
1360
|
-
* of reporting that the control does nothing.
|
|
1489
|
+
* of reporting that the control does nothing. `null` is closed and a
|
|
1490
|
+
* string is the group the flow started from ('' for the plain add), so the
|
|
1491
|
+
* workspace it makes lands where the reader asked for it.
|
|
1361
1492
|
*/
|
|
1362
|
-
var pickingPair = sbReact.useState(
|
|
1493
|
+
var pickingPair = sbReact.useState(null)
|
|
1363
1494
|
var picking = pickingPair[0]
|
|
1364
1495
|
var setPicking = pickingPair[1]
|
|
1365
1496
|
|
|
@@ -1376,6 +1507,23 @@
|
|
|
1376
1507
|
}
|
|
1377
1508
|
}, [])
|
|
1378
1509
|
|
|
1510
|
+
// A workspace stops being "fresh" the moment the projection carries it:
|
|
1511
|
+
// from then on the Host's own title and path are what the row reads.
|
|
1512
|
+
sbReact.useEffect(function () {
|
|
1513
|
+
var listed = {}
|
|
1514
|
+
for (var i = 0; i < items.length; i += 1) listed[sbText(items[i] && items[i].id)] = true
|
|
1515
|
+
setFresh(function (live) {
|
|
1516
|
+
var kept = {}
|
|
1517
|
+
var changed = false
|
|
1518
|
+
for (var key in live) {
|
|
1519
|
+
if (!Object.prototype.hasOwnProperty.call(live, key)) continue
|
|
1520
|
+
if (listed[key]) { changed = true; continue }
|
|
1521
|
+
kept[key] = live[key]
|
|
1522
|
+
}
|
|
1523
|
+
return changed ? kept : live
|
|
1524
|
+
})
|
|
1525
|
+
}, [items])
|
|
1526
|
+
|
|
1379
1527
|
/**
|
|
1380
1528
|
* The header's narrow shape, as the stylesheet drew it. Both shapes are in
|
|
1381
1529
|
* the tree and the container query hides one of them, so this is the only
|
|
@@ -1414,12 +1562,12 @@
|
|
|
1414
1562
|
// this is the region's own card's way out; closing twice is the same as
|
|
1415
1563
|
// closing once, and a dialog with no way out is the defect being fixed.
|
|
1416
1564
|
sbReact.useEffect(function () {
|
|
1417
|
-
if (!editor &&
|
|
1565
|
+
if (!editor && picking === null && !confirming) return undefined
|
|
1418
1566
|
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined
|
|
1419
1567
|
function onKey(event) {
|
|
1420
1568
|
if (!event || event.key !== 'Escape') return
|
|
1421
1569
|
setEditor(null)
|
|
1422
|
-
setPicking(
|
|
1570
|
+
setPicking(null)
|
|
1423
1571
|
setConfirming(null)
|
|
1424
1572
|
}
|
|
1425
1573
|
document.addEventListener('keydown', onKey)
|
|
@@ -1530,21 +1678,120 @@
|
|
|
1530
1678
|
if (!projects) return null
|
|
1531
1679
|
return projects.create({ path: path })
|
|
1532
1680
|
}
|
|
1533
|
-
/**
|
|
1534
|
-
|
|
1535
|
-
|
|
1681
|
+
/**
|
|
1682
|
+
* What this region knows about a workspace the Host just answered with,
|
|
1683
|
+
* in the shape the list draws a member it has no projection for: the id
|
|
1684
|
+
* it files, the name and path the row needs, and when it was created.
|
|
1685
|
+
* @param workspace - a WorkspaceView from `workspaces.create`, or an id.
|
|
1686
|
+
* @returns `{ id, title, path, createdAt }`, or null when there is no id.
|
|
1687
|
+
*/
|
|
1688
|
+
function freshOf(workspace) {
|
|
1689
|
+
var id = sbWorkspaceId(workspace)
|
|
1690
|
+
if (id === '') return null
|
|
1691
|
+
var view = workspace && typeof workspace === 'object' ? workspace : {}
|
|
1692
|
+
var path = sbText(view.path)
|
|
1693
|
+
return {
|
|
1694
|
+
id: id,
|
|
1695
|
+
// The Host titles a Workspace after its folder; a workspace it did
|
|
1696
|
+
// not title still has the folder to fall back to, and the id is the
|
|
1697
|
+
// one thing this row must never be named after.
|
|
1698
|
+
title: sbText(view.title).trim() || sbBase(path),
|
|
1699
|
+
path: path,
|
|
1700
|
+
createdAt: sbEpoch(view.createdAt) || Date.now(),
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/** @param entry - one `freshOf` record, remembered until the projection carries it. */
|
|
1704
|
+
function rememberFresh(entry) {
|
|
1705
|
+
setFresh(function (live) {
|
|
1706
|
+
var next = {}
|
|
1707
|
+
for (var key in live) {
|
|
1708
|
+
if (Object.prototype.hasOwnProperty.call(live, key)) next[key] = live[key]
|
|
1709
|
+
}
|
|
1710
|
+
next[entry.id] = entry
|
|
1711
|
+
return next
|
|
1712
|
+
})
|
|
1713
|
+
}
|
|
1714
|
+
/** @param workspaceId - the workspace whose remembered name is no longer wanted. */
|
|
1715
|
+
function forgetFresh(workspaceId) {
|
|
1716
|
+
var id = sbText(workspaceId)
|
|
1717
|
+
if (id === '') return
|
|
1718
|
+
setFresh(function (live) {
|
|
1719
|
+
if (!Object.prototype.hasOwnProperty.call(live, id)) return live
|
|
1720
|
+
var next = {}
|
|
1721
|
+
for (var key in live) {
|
|
1722
|
+
if (Object.prototype.hasOwnProperty.call(live, key) && key !== id) next[key] = live[key]
|
|
1723
|
+
}
|
|
1724
|
+
return next
|
|
1725
|
+
})
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* File a workspace the reader just added into the group whose own control
|
|
1729
|
+
* started the flow, and unfold that group so the new row is on screen.
|
|
1730
|
+
* A project added from "实验室" that surfaces in the ungrouped bucket reads
|
|
1731
|
+
* as "it did not land in the category at all", which is the report this
|
|
1732
|
+
* answers.
|
|
1733
|
+
*
|
|
1734
|
+
* The member record is written even when there is no group: it is what
|
|
1735
|
+
* puts the row in the list at once instead of waiting for the Host's
|
|
1736
|
+
* projection, and the remembered name is what keeps that row from
|
|
1737
|
+
* appearing as its workspace id for the first render.
|
|
1738
|
+
* @param workspace - the workspace the Host answered with, or its id.
|
|
1739
|
+
* @param groupKey - the group the flow started from; '' for the plain add.
|
|
1740
|
+
* @returns the save promise, or null when there is nothing to file.
|
|
1741
|
+
*/
|
|
1742
|
+
function fileNewWorkspace(workspace, groupKey) {
|
|
1743
|
+
var entry = freshOf(workspace)
|
|
1744
|
+
if (!entry) return null
|
|
1745
|
+
rememberFresh(entry)
|
|
1746
|
+
var key = sbText(groupKey)
|
|
1747
|
+
return kit.mutateSidebarLayout(function (live) {
|
|
1748
|
+
var next = kit.withWorkspace(live, entry.id, { group: key === '' ? null : key })
|
|
1749
|
+
if (key === '') return next
|
|
1750
|
+
var groups = kit.sidebarGroups(next)
|
|
1751
|
+
for (var i = 0; i < groups.length; i += 1) {
|
|
1752
|
+
var group = groups[i]
|
|
1753
|
+
if (group && sbText(group.id) === key && group.collapsed === true) {
|
|
1754
|
+
return kit.withGroupPatch(next, key, { collapsed: false })
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return next
|
|
1758
|
+
})
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Take whatever the Host's chooser resolved and register it, filing it
|
|
1762
|
+
* into the group the flow started from.
|
|
1763
|
+
* @param picked - the path the Host resolved, or a workspace it already made.
|
|
1764
|
+
* @param groupKey - the group the flow started from; '' for the plain add.
|
|
1765
|
+
* @returns the created workspace (or its save), or null.
|
|
1766
|
+
*/
|
|
1767
|
+
function adoptPicked(picked, groupKey) {
|
|
1768
|
+
var home = sbText(groupKey)
|
|
1769
|
+
// A workspace the Host already made, as opposed to a path it resolved:
|
|
1770
|
+
// a path is a string, and a workspace is the view that carries its id.
|
|
1771
|
+
if (picked && typeof picked === 'object' && sbWorkspaceId(picked) !== '') {
|
|
1772
|
+
return fileNewWorkspace(picked, home)
|
|
1773
|
+
}
|
|
1536
1774
|
var path = pickedPath(picked)
|
|
1537
1775
|
if (path === '') return null
|
|
1538
|
-
|
|
1776
|
+
var created = createWorkspace(path)
|
|
1777
|
+
if (!created || typeof created.then !== 'function') {
|
|
1778
|
+
return fileNewWorkspace(created, home)
|
|
1779
|
+
}
|
|
1780
|
+
return created.then(function (workspace) {
|
|
1781
|
+
return fileNewWorkspace(workspace, home)
|
|
1782
|
+
})
|
|
1539
1783
|
}
|
|
1540
|
-
/**
|
|
1541
|
-
|
|
1784
|
+
/**
|
|
1785
|
+
* Put the region's own directory browser up.
|
|
1786
|
+
* @param groupKey - the group the flow started from; '' for the plain add.
|
|
1787
|
+
*/
|
|
1788
|
+
function openDirectoryPicker(groupKey) {
|
|
1542
1789
|
if (!mountedRef.current) return
|
|
1543
1790
|
setActionError('')
|
|
1544
1791
|
setMenu(null)
|
|
1545
1792
|
setEditor(null)
|
|
1546
1793
|
setConfirming(null)
|
|
1547
|
-
setPicking(
|
|
1794
|
+
setPicking(sbText(groupKey))
|
|
1548
1795
|
}
|
|
1549
1796
|
/**
|
|
1550
1797
|
* Add a project. The Host's chooser is asked first: on a local connection
|
|
@@ -1554,21 +1801,36 @@
|
|
|
1554
1801
|
* then opens its own browser (see `SidebarDirectoryDialog`), which lists
|
|
1555
1802
|
* directories through the Host the way the shipped sidebar does, instead
|
|
1556
1803
|
* of leaving the control dead.
|
|
1804
|
+
* @param section - the group whose control started the flow, or null.
|
|
1557
1805
|
*/
|
|
1558
|
-
function newProject() {
|
|
1806
|
+
function newProject(section) {
|
|
1807
|
+
var home = sbText(section && section.key)
|
|
1559
1808
|
act(function () {
|
|
1560
|
-
var service =
|
|
1561
|
-
|
|
1809
|
+
var service = sbService('uiWorkspace')
|
|
1810
|
+
var picks = service && typeof service.pickDirectory === 'function'
|
|
1811
|
+
var browses = service && typeof service.listDirectory === 'function'
|
|
1812
|
+
if (!picks && !browses) {
|
|
1813
|
+
setActionError(sbT('sidebar.list.readonly'))
|
|
1814
|
+
return null
|
|
1815
|
+
}
|
|
1816
|
+
// A Host with listing but no chooser is exactly the remote case; the
|
|
1817
|
+
// region's own browser is the only interaction it can offer.
|
|
1818
|
+
if (!picks) {
|
|
1819
|
+
openDirectoryPicker(home)
|
|
1820
|
+
return null
|
|
1821
|
+
}
|
|
1562
1822
|
var outcome = null
|
|
1563
1823
|
try {
|
|
1564
1824
|
outcome = service.pickDirectory()
|
|
1565
1825
|
} catch (error) {
|
|
1566
|
-
openDirectoryPicker()
|
|
1826
|
+
openDirectoryPicker(home)
|
|
1567
1827
|
return null
|
|
1568
1828
|
}
|
|
1569
|
-
if (!outcome || typeof outcome.then !== 'function') return adoptPicked(outcome)
|
|
1570
|
-
return outcome.then(
|
|
1571
|
-
|
|
1829
|
+
if (!outcome || typeof outcome.then !== 'function') return adoptPicked(outcome, home)
|
|
1830
|
+
return outcome.then(function (picked) {
|
|
1831
|
+
return adoptPicked(picked, home)
|
|
1832
|
+
}, function () {
|
|
1833
|
+
openDirectoryPicker(home)
|
|
1572
1834
|
return null
|
|
1573
1835
|
})
|
|
1574
1836
|
})
|
|
@@ -1602,15 +1864,20 @@
|
|
|
1602
1864
|
}
|
|
1603
1865
|
/** @param project - the project to unregister. */
|
|
1604
1866
|
function removeProject(project) {
|
|
1867
|
+
var id = sbText(project && project.id)
|
|
1605
1868
|
act(function () {
|
|
1606
1869
|
var service = need('workspaces', 'delete')
|
|
1607
1870
|
if (!service) return null
|
|
1608
|
-
|
|
1871
|
+
// Its remembered name goes with it: the row it belongs to is about to
|
|
1872
|
+
// stop existing, and a later workspace never reuses the id.
|
|
1873
|
+
forgetFresh(id)
|
|
1874
|
+
return service.delete(id)
|
|
1609
1875
|
})
|
|
1610
1876
|
}
|
|
1611
1877
|
/** @param id - a workspace the document still names but the sidebar dropped. */
|
|
1612
1878
|
function forgetProject(id) {
|
|
1613
1879
|
act(function () {
|
|
1880
|
+
forgetFresh(id)
|
|
1614
1881
|
return kit.mutateSidebarLayout(function (live) { return kit.withoutWorkspace(live, id) })
|
|
1615
1882
|
})
|
|
1616
1883
|
}
|
|
@@ -1963,7 +2230,7 @@
|
|
|
1963
2230
|
}
|
|
1964
2231
|
/** @param choice - a group menu id. @param section - the group section. */
|
|
1965
2232
|
function runGroupChoice(choice, section) {
|
|
1966
|
-
if (choice === 'newProject') return newProject()
|
|
2233
|
+
if (choice === 'newProject') return newProject(section)
|
|
1967
2234
|
if (choice === 'rename') return openEditor('group', sbText(section && section.key), section && section.title)
|
|
1968
2235
|
if (choice.indexOf('color:') === 0) return markSection(section, choice.slice(6))
|
|
1969
2236
|
if (choice === 'sort') return setSectionSort(section, section && section.sort === 'recent' ? 'manual' : 'recent')
|
|
@@ -2293,8 +2560,12 @@
|
|
|
2293
2560
|
head.push(h('span', { className: 'ph-sb-actions', key: 'actions' }, [
|
|
2294
2561
|
sbIconButton('plus', sbT('sidebar.list.newProject'), function (event) {
|
|
2295
2562
|
if (event && event.stopPropagation) event.stopPropagation()
|
|
2296
|
-
newProject()
|
|
2297
|
-
}, {
|
|
2563
|
+
newProject(section)
|
|
2564
|
+
}, {
|
|
2565
|
+
key: 'new',
|
|
2566
|
+
sb: 'newProject',
|
|
2567
|
+
disabled: !sbHas('uiWorkspace', 'pickDirectory') && !sbHas('uiWorkspace', 'listDirectory'),
|
|
2568
|
+
}),
|
|
2298
2569
|
sbMenu('group:' + key, open, anchor, entries, function (choice) { runGroupChoice(choice, section) }, closeMenu),
|
|
2299
2570
|
]))
|
|
2300
2571
|
}
|
|
@@ -2501,11 +2772,15 @@
|
|
|
2501
2772
|
cancelEditor,
|
|
2502
2773
|
)
|
|
2503
2774
|
}
|
|
2504
|
-
if (picking) {
|
|
2775
|
+
if (picking !== null) {
|
|
2505
2776
|
layer = h(SidebarDirectoryDialog, {
|
|
2506
2777
|
key: 'picker',
|
|
2507
|
-
onCancel: function () { setPicking(
|
|
2508
|
-
onPicked: function () {
|
|
2778
|
+
onCancel: function () { setPicking(null) },
|
|
2779
|
+
onPicked: function (workspace) {
|
|
2780
|
+
var home = picking
|
|
2781
|
+
setPicking(null)
|
|
2782
|
+
attempt(function () { return fileNewWorkspace(workspace, home) })
|
|
2783
|
+
},
|
|
2509
2784
|
})
|
|
2510
2785
|
}
|
|
2511
2786
|
if (confirming) {
|
package/lib/client.js
CHANGED
|
@@ -3284,10 +3284,10 @@ var GRAIN = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org
|
|
|
3284
3284
|
// The region's own directory browser (the shell's `Modal` draws the card
|
|
3285
3285
|
// when the kit is there; these are the rows both draw).
|
|
3286
3286
|
'.ph-sb-dir{display:flex;flex-direction:column;gap:6px;min-height:180px;max-height:46vh}',
|
|
3287
|
-
'.ph-sb-dir-crumbs{display:flex;align-items:center;flex-wrap:wrap;gap:0 2px;min-height:22px}',
|
|
3287
|
+
'.ph-sb-dir-crumbs{display:flex;align-items:center;flex-wrap:wrap;gap:0 2px;min-height:22px;flex:none}',
|
|
3288
3288
|
// The trail turns into a path field: the shipped browser lets a reader
|
|
3289
3289
|
// type an absolute path instead of walking to it.
|
|
3290
|
-
'.ph-sb-dir-path{display:flex;align-items:center;gap:6px;min-height:22px}',
|
|
3290
|
+
'.ph-sb-dir-path{display:flex;align-items:center;gap:6px;min-height:22px;flex:none}',
|
|
3291
3291
|
'.ph-sb-dir-path-field{flex:1 1 auto;width:auto}',
|
|
3292
3292
|
'.ph-sb-dir-edit{margin-left:auto}',
|
|
3293
3293
|
'.ph-sb-dir-seat{display:inline-flex;align-items:center;gap:2px}',
|
|
@@ -3303,7 +3303,11 @@ var GRAIN = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org
|
|
|
3303
3303
|
'.ph-sb-dir-crumb:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid ' +
|
|
3304
3304
|
'var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#3b6fe0));outline-offset:1px}',
|
|
3305
3305
|
'.ph-sb-dir-crumb.is-current{color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
3306
|
-
|
|
3306
|
+
// The list is the one child that gives way when the dialog hits its
|
|
3307
|
+
// height: a row of text shrunk to fit its box, which is how the target
|
|
3308
|
+
// line lost its lower half. Every other child keeps its height, and the
|
|
3309
|
+
// list scrolls instead.
|
|
3310
|
+
'.ph-sb-dir-list{display:flex;flex-direction:column;gap:1px;min-height:0;overflow:auto;flex:1 1 auto}',
|
|
3307
3311
|
'.ph-sb-dir-row{display:flex;align-items:center;gap:4px;width:100%;box-sizing:border-box;height:28px;padding:0 4px;' +
|
|
3308
3312
|
'border:0;border-radius:var(--dsw-radius-sm,6px);background:transparent;cursor:pointer;text-align:left;' +
|
|
3309
3313
|
'font-family:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
@@ -3315,8 +3319,18 @@ var GRAIN = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org
|
|
|
3315
3319
|
'.ph-sb-dir-row.is-hidden{opacity:.62}',
|
|
3316
3320
|
'.ph-sb-dir-icon{flex:none;display:inline-flex;opacity:.7}',
|
|
3317
3321
|
'.ph-sb-dir-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
3318
|
-
'.ph-sb-dir-note{padding:4px;font-size:12px;line-height:18px;
|
|
3319
|
-
'
|
|
3322
|
+
'.ph-sb-dir-note{padding:4px;font-size:12px;line-height:18px;flex:none;' +
|
|
3323
|
+
'color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
|
3324
|
+
// The target is a path, and the part of it that tells two folders apart is
|
|
3325
|
+
// the end: it wraps rather than ellipsizing the folder away, and it never
|
|
3326
|
+
// gives up height to the scrolling list below it (a clipped line is what a
|
|
3327
|
+
// shrunk flex item with `overflow:hidden` looks like).
|
|
3328
|
+
'.ph-sb-dir-target{flex:none;overflow-wrap:anywhere;' +
|
|
3329
|
+
'color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
3330
|
+
'.ph-sb-dir-newfold{display:flex;align-items:center;gap:6px;flex:none}',
|
|
3331
|
+
// The error line sits in the same column: it is the one thing the reader
|
|
3332
|
+
// must be able to read, so it keeps its height like the notes above it.
|
|
3333
|
+
'.ph-sb-dir .ph-sb-form-error{flex:none}',
|
|
3320
3334
|
'.ph-sb-dir-newfold .ph-sb-field{flex:1 1 auto}',
|
|
3321
3335
|
// The footer toggle from the shipped browser: plain text at control size,
|
|
3322
3336
|
// no border, and the pressed state marked by a trailing check so the
|
|
@@ -4938,6 +4952,7 @@ var STYLES = [
|
|
|
4938
4952
|
'sidebar.list.dirEmpty': '没有子文件夹',
|
|
4939
4953
|
'sidebar.list.dirHiddenOnly': '没有可见的子文件夹,隐藏项已折叠',
|
|
4940
4954
|
'sidebar.list.dirTruncated': '文件夹太多,只列出了前一部分',
|
|
4955
|
+
'sidebar.list.dirTarget': '将在此创建工作区:{path}',
|
|
4941
4956
|
'sidebar.list.dirFailed': '读不到这个目录:{reason}',
|
|
4942
4957
|
'sidebar.list.apply': '确定',
|
|
4943
4958
|
'sidebar.list.more': '更多',
|
|
@@ -5168,6 +5183,7 @@ var STYLES = [
|
|
|
5168
5183
|
'sidebar.list.dirEmpty': 'No subfolders',
|
|
5169
5184
|
'sidebar.list.dirHiddenOnly': 'No visible subfolders; hidden items are folded',
|
|
5170
5185
|
'sidebar.list.dirTruncated': 'Too many folders; only the first ones are listed',
|
|
5186
|
+
'sidebar.list.dirTarget': 'Creates the workspace in: {path}',
|
|
5171
5187
|
'sidebar.list.dirFailed': 'Cannot read this folder: {reason}',
|
|
5172
5188
|
'sidebar.list.apply': 'Apply',
|
|
5173
5189
|
'sidebar.list.more': 'More',
|
|
@@ -5401,6 +5417,20 @@ var STYLES = [
|
|
|
5401
5417
|
updatedAt: Number(row.updatedAt) || 0,
|
|
5402
5418
|
}
|
|
5403
5419
|
}
|
|
5420
|
+
/**
|
|
5421
|
+
* Read one workspace instant: the Host stamps Workspace rows with ISO-8601
|
|
5422
|
+
* strings, while a projection that was already reduced can carry epoch
|
|
5423
|
+
* milliseconds. Both are wanted by the list, which orders by them.
|
|
5424
|
+
* @param value - an ISO-8601 instant, epoch milliseconds, or anything else.
|
|
5425
|
+
* @returns epoch milliseconds, or 0 when it names no time.
|
|
5426
|
+
*/
|
|
5427
|
+
function epochOf(value) {
|
|
5428
|
+
if (typeof value === 'number') return isFinite(value) && value > 0 ? value : 0
|
|
5429
|
+
var text = String(value === undefined || value === null ? '' : value)
|
|
5430
|
+
if (text === '') return 0
|
|
5431
|
+
var parsed = Date.parse(text)
|
|
5432
|
+
return isFinite(parsed) && parsed > 0 ? parsed : 0
|
|
5433
|
+
}
|
|
5404
5434
|
/**
|
|
5405
5435
|
* @param value - a workspace projection array.
|
|
5406
5436
|
* @returns a clean projection array.
|
|
@@ -5415,6 +5445,10 @@ var STYLES = [
|
|
|
5415
5445
|
title: String(item.title === undefined || item.title === null ? '' : item.title),
|
|
5416
5446
|
path: String(item.path === undefined || item.path === null ? '' : item.path),
|
|
5417
5447
|
sessions: rows.length || (Number(item.sessions) || 0),
|
|
5448
|
+
// When the Workspace itself was registered. A project with no Sessions
|
|
5449
|
+
// is not old — creating it is an event — so the list's recency order
|
|
5450
|
+
// counts this alongside its newest Session.
|
|
5451
|
+
createdAt: epochOf(item.createdAt),
|
|
5418
5452
|
rows: rows,
|
|
5419
5453
|
}
|
|
5420
5454
|
}).filter(function (entry) { return entry.id !== '' })
|
|
@@ -5598,6 +5632,7 @@ var STYLES = [
|
|
|
5598
5632
|
id: view.workspaceId,
|
|
5599
5633
|
title: view.title,
|
|
5600
5634
|
path: view.path,
|
|
5635
|
+
createdAt: epochOf(view.createdAt),
|
|
5601
5636
|
rows: rows,
|
|
5602
5637
|
}
|
|
5603
5638
|
})
|
|
@@ -5657,7 +5692,7 @@ var STYLES = [
|
|
|
5657
5692
|
var parts = [String(current || ''), Object.keys(subagents).sort().join(',')]
|
|
5658
5693
|
for (var i = 0; i < items.length; i += 1) {
|
|
5659
5694
|
var item = items[i]
|
|
5660
|
-
parts.push(item.id, item.title, item.path, String(item.sessions))
|
|
5695
|
+
parts.push(item.id, item.title, item.path, String(item.sessions), String(item.createdAt))
|
|
5661
5696
|
for (var j = 0; j < item.rows.length; j += 1) {
|
|
5662
5697
|
var row = item.rows[j]
|
|
5663
5698
|
parts.push(
|
|
@@ -10579,6 +10614,28 @@ var STYLES = [
|
|
|
10579
10614
|
if (value === null || value === undefined) return ''
|
|
10580
10615
|
return String(value)
|
|
10581
10616
|
}
|
|
10617
|
+
/**
|
|
10618
|
+
* @param value - a workspace view, a workspace id, or anything else.
|
|
10619
|
+
* @returns the workspace id it carries, or '' when it names none. The
|
|
10620
|
+
* client service answers `workspaceId`; a host that answers `id` is the
|
|
10621
|
+
* one other spelling this region accepts.
|
|
10622
|
+
*/
|
|
10623
|
+
function sbWorkspaceId(value) {
|
|
10624
|
+
if (typeof value === 'string') return value
|
|
10625
|
+
if (!value || typeof value !== 'object') return ''
|
|
10626
|
+
return sbText(value.workspaceId || value.id)
|
|
10627
|
+
}
|
|
10628
|
+
/**
|
|
10629
|
+
* @param value - an ISO-8601 instant, epoch milliseconds, or anything else.
|
|
10630
|
+
* @returns epoch milliseconds, or 0 when it names no time.
|
|
10631
|
+
*/
|
|
10632
|
+
function sbEpoch(value) {
|
|
10633
|
+
if (typeof value === 'number') return isFinite(value) && value > 0 ? value : 0
|
|
10634
|
+
var text = sbText(value)
|
|
10635
|
+
if (text === '') return 0
|
|
10636
|
+
var parsed = Date.parse(text)
|
|
10637
|
+
return isFinite(parsed) && parsed > 0 ? parsed : 0
|
|
10638
|
+
}
|
|
10582
10639
|
/** @param value - a path. @returns its last segment, both separators accepted. */
|
|
10583
10640
|
function sbBase(value) {
|
|
10584
10641
|
var path = sbText(value)
|
|
@@ -10685,19 +10742,22 @@ var STYLES = [
|
|
|
10685
10742
|
return draw(size || 16)
|
|
10686
10743
|
}
|
|
10687
10744
|
/**
|
|
10688
|
-
* A project's own recency: the newest session it shows
|
|
10689
|
-
*
|
|
10690
|
-
*
|
|
10745
|
+
* A project's own recency: the newest session it shows, or when the
|
|
10746
|
+
* workspace itself was created — whichever is later. A project with no
|
|
10747
|
+
* sessions is not an old project: registering it is an event, so it must
|
|
10748
|
+
* not sink to the bottom of a section ordered by recency the moment it is
|
|
10749
|
+
* added. Borrowing a neighbour's time is still wrong, which is what the
|
|
10750
|
+
* creation instant keeps from happening.
|
|
10691
10751
|
*
|
|
10692
10752
|
* A blank row is skipped: it is the draft the shell holds for a workspace
|
|
10693
10753
|
* you focused, never sent, so it is not an event and cannot make a project
|
|
10694
10754
|
* recent. All its `updatedAt` ever says is when the draft was made.
|
|
10695
10755
|
* @param project - a projected project.
|
|
10696
|
-
* @returns the newest
|
|
10756
|
+
* @returns the newest instant the project stands for, or 0.
|
|
10697
10757
|
*/
|
|
10698
10758
|
function sbProjectAt(project) {
|
|
10759
|
+
var at = Number(project && project.createdAt) || 0
|
|
10699
10760
|
var rows = Array.isArray(project && project.rows) ? project.rows : []
|
|
10700
|
-
var at = 0
|
|
10701
10761
|
for (var i = 0; i < rows.length; i += 1) {
|
|
10702
10762
|
if (rows[i] && rows[i].blank === true) continue
|
|
10703
10763
|
var value = Number(rows[i] && rows[i].updatedAt) || 0
|
|
@@ -10735,12 +10795,19 @@ var STYLES = [
|
|
|
10735
10795
|
* ungrouped bucket. Members are the projects the sidebar listed plus any
|
|
10736
10796
|
* the document still names, so a stale assignment stays visible and
|
|
10737
10797
|
* removable instead of silently disappearing.
|
|
10798
|
+
*
|
|
10799
|
+
* A member the projection has not carried yet — one this region created a
|
|
10800
|
+
* moment ago — is drawn from the `fresh` memory instead: without it the row
|
|
10801
|
+
* has no title and no path, so it reads as the workspace id, and it is
|
|
10802
|
+
* marked as a stale assignment it is not.
|
|
10738
10803
|
* @param layout - the document, or null.
|
|
10739
10804
|
* @param items - the projected projects.
|
|
10805
|
+
* @param fresh - `{ [id]: { title, path, createdAt } }` for just-created workspaces.
|
|
10740
10806
|
* @returns the sections, each `{ key, title, color, items, collapsed, sort }`.
|
|
10741
10807
|
*/
|
|
10742
|
-
function sbSections(layout, items) {
|
|
10808
|
+
function sbSections(layout, items, fresh) {
|
|
10743
10809
|
var projects = Array.isArray(items) ? items : []
|
|
10810
|
+
var memory = fresh && typeof fresh === 'object' ? fresh : null
|
|
10744
10811
|
var groups = kit.sidebarGroups(layout).slice().sort(function (left, right) {
|
|
10745
10812
|
var a = left && typeof left.order === 'number' ? left.order : 0
|
|
10746
10813
|
var b = right && typeof right.order === 'number' ? right.order : 0
|
|
@@ -10772,7 +10839,16 @@ var STYLES = [
|
|
|
10772
10839
|
if (known[id]) return
|
|
10773
10840
|
var member = members[id]
|
|
10774
10841
|
var section = member && member.group && byId[member.group] ? byId[member.group] : loose
|
|
10775
|
-
|
|
10842
|
+
var made = memory && memory[id] ? memory[id] : null
|
|
10843
|
+
section.items.push({
|
|
10844
|
+
id: id,
|
|
10845
|
+
title: made ? sbText(made.title) : '',
|
|
10846
|
+
path: made ? sbText(made.path) : '',
|
|
10847
|
+
sessions: 0,
|
|
10848
|
+
rows: [],
|
|
10849
|
+
createdAt: made ? Number(made.createdAt) || 0 : 0,
|
|
10850
|
+
orphan: !made,
|
|
10851
|
+
})
|
|
10776
10852
|
})
|
|
10777
10853
|
var out = sections.slice()
|
|
10778
10854
|
if (loose.items.length > 0) out.push(loose)
|
|
@@ -11399,11 +11475,25 @@ var STYLES = [
|
|
|
11399
11475
|
}
|
|
11400
11476
|
}, [])
|
|
11401
11477
|
|
|
11402
|
-
/**
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11406
|
-
|
|
11478
|
+
/**
|
|
11479
|
+
* The folder this dialog would register as a workspace: the path in the
|
|
11480
|
+
* open field — a typed path is a target even before Enter has walked the
|
|
11481
|
+
* list to it — and the level on screen when the field is closed or blank.
|
|
11482
|
+
* Without the draft the two disagree: the operator types a folder, clicks
|
|
11483
|
+
* the confirm, and the workspace is made in whatever level happened to be
|
|
11484
|
+
* listed underneath, which reads as "it ignored what I typed".
|
|
11485
|
+
* @returns an absolute path, or '' when there is nothing to name.
|
|
11486
|
+
*/
|
|
11487
|
+
function targetPath() {
|
|
11488
|
+
var draft = state.pathDraft === null ? '' : sbText(state.pathDraft).trim()
|
|
11489
|
+
if (draft !== '') return draft
|
|
11490
|
+
return sbText(state.listing && state.listing.path)
|
|
11491
|
+
}
|
|
11492
|
+
/**
|
|
11493
|
+
* Register one directory as a workspace and hand it back to the caller.
|
|
11494
|
+
* @param path - an absolute directory the Host has already accepted.
|
|
11495
|
+
*/
|
|
11496
|
+
function commit(path) {
|
|
11407
11497
|
var projects = sbService('workspaces')
|
|
11408
11498
|
if (!projects || typeof projects.create !== 'function') {
|
|
11409
11499
|
patch({ error: sbT('sidebar.list.readonly'), phase: 'error' })
|
|
@@ -11418,16 +11508,68 @@ var STYLES = [
|
|
|
11418
11508
|
return
|
|
11419
11509
|
}
|
|
11420
11510
|
if (!outcome || typeof outcome.then !== 'function') {
|
|
11421
|
-
onPicked()
|
|
11511
|
+
onPicked(outcome)
|
|
11422
11512
|
return
|
|
11423
11513
|
}
|
|
11424
|
-
outcome.then(function () {
|
|
11425
|
-
if (alive.current) onPicked()
|
|
11514
|
+
outcome.then(function (created) {
|
|
11515
|
+
if (alive.current) onPicked(created)
|
|
11426
11516
|
}, function (error) {
|
|
11427
11517
|
if (!alive.current) return
|
|
11428
11518
|
patch({ busy: false, phase: 'error', error: sbT('sidebar.list.failed', { reason: sbReason(error) }) })
|
|
11429
11519
|
})
|
|
11430
11520
|
}
|
|
11521
|
+
/**
|
|
11522
|
+
* The confirm action. It commits the folder it names, and a typed path is
|
|
11523
|
+
* read through the Host first: the listing proves the directory exists
|
|
11524
|
+
* and answers with its canonical spelling, which is what the workspace has
|
|
11525
|
+
* to store (`..`, a trailing separator, a Windows forward slash).
|
|
11526
|
+
*/
|
|
11527
|
+
function adopt() {
|
|
11528
|
+
var target = targetPath()
|
|
11529
|
+
if (target === '') return
|
|
11530
|
+
if (state.busy) return
|
|
11531
|
+
if (target === sbText(state.listing && state.listing.path)) {
|
|
11532
|
+
commit(target)
|
|
11533
|
+
return
|
|
11534
|
+
}
|
|
11535
|
+
var service = sbService('uiWorkspace')
|
|
11536
|
+
if (!service || typeof service.listDirectory !== 'function') {
|
|
11537
|
+
// No browsing capability behind the field: the Host still owns
|
|
11538
|
+
// `create`, and it is the one that validates the path.
|
|
11539
|
+
commit(target)
|
|
11540
|
+
return
|
|
11541
|
+
}
|
|
11542
|
+
patch({ busy: true, error: '' })
|
|
11543
|
+
var outcome = null
|
|
11544
|
+
try {
|
|
11545
|
+
outcome = service.listDirectory(target)
|
|
11546
|
+
} catch (error) {
|
|
11547
|
+
patch({
|
|
11548
|
+
busy: false,
|
|
11549
|
+
phase: 'error',
|
|
11550
|
+
pathDraft: target,
|
|
11551
|
+
error: sbT('sidebar.list.dirFailed', { reason: sbReason(error) }),
|
|
11552
|
+
})
|
|
11553
|
+
return
|
|
11554
|
+
}
|
|
11555
|
+
if (!outcome || typeof outcome.then !== 'function') {
|
|
11556
|
+
var resolved = sbText(outcome && outcome.path) || target
|
|
11557
|
+
commit(resolved)
|
|
11558
|
+
return
|
|
11559
|
+
}
|
|
11560
|
+
outcome.then(function (listing) {
|
|
11561
|
+
if (!alive.current) return
|
|
11562
|
+
commit(sbText(listing && listing.path) || target)
|
|
11563
|
+
}, function (error) {
|
|
11564
|
+
if (!alive.current) return
|
|
11565
|
+
patch({
|
|
11566
|
+
busy: false,
|
|
11567
|
+
phase: 'error',
|
|
11568
|
+
pathDraft: target,
|
|
11569
|
+
error: sbT('sidebar.list.dirFailed', { reason: sbReason(error) }),
|
|
11570
|
+
})
|
|
11571
|
+
})
|
|
11572
|
+
}
|
|
11431
11573
|
/** Create the folder named in the field, then open it. */
|
|
11432
11574
|
function mint() {
|
|
11433
11575
|
var name = sbText(state.folder).trim()
|
|
@@ -11529,8 +11671,17 @@ var STYLES = [
|
|
|
11529
11671
|
patch({ pathDraft: here })
|
|
11530
11672
|
}, { key: 'edit', sb: 'dirEditPath', size: 14, className: 'ph-sb-dir-edit' }),
|
|
11531
11673
|
])
|
|
11674
|
+
// What the confirm action will register, said out loud under the path it
|
|
11675
|
+
// reads: a field whose contents are not the folder that will be created is
|
|
11676
|
+
// the trap this line exists to remove.
|
|
11677
|
+
var target = targetPath()
|
|
11678
|
+
var targetLine = target === ''
|
|
11679
|
+
? null
|
|
11680
|
+
: h('div', { className: 'ph-sb-dir-note ph-sb-dir-target', key: 'target', 'data-sb': 'dirTarget' },
|
|
11681
|
+
sbT('sidebar.list.dirTarget', { path: target }))
|
|
11532
11682
|
var body = h('div', { className: 'ph-sb-dir', key: 'dir' }, [
|
|
11533
11683
|
pathRow,
|
|
11684
|
+
targetLine,
|
|
11534
11685
|
state.phase === 'loading'
|
|
11535
11686
|
? h('div', { className: 'ph-sb-dir-note', key: 'loading' }, sbT('sidebar.list.dirLoading'))
|
|
11536
11687
|
: null,
|
|
@@ -11596,7 +11747,9 @@ var STYLES = [
|
|
|
11596
11747
|
? h('div', { className: 'ph-sb-form-error', key: 'error', 'data-sb': 'dirError' }, state.error)
|
|
11597
11748
|
: null,
|
|
11598
11749
|
])
|
|
11599
|
-
|
|
11750
|
+
// A typed path is a target of its own: the confirm action stays live for
|
|
11751
|
+
// it even when the level underneath could not be listed at all.
|
|
11752
|
+
var choose = target !== ''
|
|
11600
11753
|
// The same footer the shipped browser draws: the two actions that change
|
|
11601
11754
|
// the folder on the left, the confirm/cancel pair on the right, and the
|
|
11602
11755
|
// hidden-items toggle between them — its label never shifts, because the
|
|
@@ -11774,7 +11927,18 @@ var STYLES = [
|
|
|
11774
11927
|
var store = kit.useSidebarLayout()
|
|
11775
11928
|
var layout = store.layout || null
|
|
11776
11929
|
var items = Array.isArray(store.workspaces) ? store.workspaces : []
|
|
11777
|
-
|
|
11930
|
+
/**
|
|
11931
|
+
* The workspaces this region created a moment ago, keyed by id, with the
|
|
11932
|
+
* title, path and creation instant the Host answered with. The projection
|
|
11933
|
+
* that carries the same facts is published by an effect in another React
|
|
11934
|
+
* root, so for the first render after a create the document is all this
|
|
11935
|
+
* region has — and a member with no title and no path draws as its
|
|
11936
|
+
* workspace id, which is not the folder the reader just picked.
|
|
11937
|
+
*/
|
|
11938
|
+
var freshPair = sbReact.useState({})
|
|
11939
|
+
var fresh = freshPair[0]
|
|
11940
|
+
var setFresh = freshPair[1]
|
|
11941
|
+
var sections = sbSections(layout, items, fresh)
|
|
11778
11942
|
var current = sbText(store.current)
|
|
11779
11943
|
var ready = sbKitReady()
|
|
11780
11944
|
|
|
@@ -11819,9 +11983,11 @@ var STYLES = [
|
|
|
11819
11983
|
* The region's own directory browser, when it is up. The Host's native
|
|
11820
11984
|
* chooser cannot exist for a browser connected to another address, so the
|
|
11821
11985
|
* picker that is missing its Host half falls back to this dialog instead
|
|
11822
|
-
* of reporting that the control does nothing.
|
|
11986
|
+
* of reporting that the control does nothing. `null` is closed and a
|
|
11987
|
+
* string is the group the flow started from ('' for the plain add), so the
|
|
11988
|
+
* workspace it makes lands where the reader asked for it.
|
|
11823
11989
|
*/
|
|
11824
|
-
var pickingPair = sbReact.useState(
|
|
11990
|
+
var pickingPair = sbReact.useState(null)
|
|
11825
11991
|
var picking = pickingPair[0]
|
|
11826
11992
|
var setPicking = pickingPair[1]
|
|
11827
11993
|
|
|
@@ -11838,6 +12004,23 @@ var STYLES = [
|
|
|
11838
12004
|
}
|
|
11839
12005
|
}, [])
|
|
11840
12006
|
|
|
12007
|
+
// A workspace stops being "fresh" the moment the projection carries it:
|
|
12008
|
+
// from then on the Host's own title and path are what the row reads.
|
|
12009
|
+
sbReact.useEffect(function () {
|
|
12010
|
+
var listed = {}
|
|
12011
|
+
for (var i = 0; i < items.length; i += 1) listed[sbText(items[i] && items[i].id)] = true
|
|
12012
|
+
setFresh(function (live) {
|
|
12013
|
+
var kept = {}
|
|
12014
|
+
var changed = false
|
|
12015
|
+
for (var key in live) {
|
|
12016
|
+
if (!Object.prototype.hasOwnProperty.call(live, key)) continue
|
|
12017
|
+
if (listed[key]) { changed = true; continue }
|
|
12018
|
+
kept[key] = live[key]
|
|
12019
|
+
}
|
|
12020
|
+
return changed ? kept : live
|
|
12021
|
+
})
|
|
12022
|
+
}, [items])
|
|
12023
|
+
|
|
11841
12024
|
/**
|
|
11842
12025
|
* The header's narrow shape, as the stylesheet drew it. Both shapes are in
|
|
11843
12026
|
* the tree and the container query hides one of them, so this is the only
|
|
@@ -11876,12 +12059,12 @@ var STYLES = [
|
|
|
11876
12059
|
// this is the region's own card's way out; closing twice is the same as
|
|
11877
12060
|
// closing once, and a dialog with no way out is the defect being fixed.
|
|
11878
12061
|
sbReact.useEffect(function () {
|
|
11879
|
-
if (!editor &&
|
|
12062
|
+
if (!editor && picking === null && !confirming) return undefined
|
|
11880
12063
|
if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') return undefined
|
|
11881
12064
|
function onKey(event) {
|
|
11882
12065
|
if (!event || event.key !== 'Escape') return
|
|
11883
12066
|
setEditor(null)
|
|
11884
|
-
setPicking(
|
|
12067
|
+
setPicking(null)
|
|
11885
12068
|
setConfirming(null)
|
|
11886
12069
|
}
|
|
11887
12070
|
document.addEventListener('keydown', onKey)
|
|
@@ -11992,21 +12175,120 @@ var STYLES = [
|
|
|
11992
12175
|
if (!projects) return null
|
|
11993
12176
|
return projects.create({ path: path })
|
|
11994
12177
|
}
|
|
11995
|
-
/**
|
|
11996
|
-
|
|
11997
|
-
|
|
12178
|
+
/**
|
|
12179
|
+
* What this region knows about a workspace the Host just answered with,
|
|
12180
|
+
* in the shape the list draws a member it has no projection for: the id
|
|
12181
|
+
* it files, the name and path the row needs, and when it was created.
|
|
12182
|
+
* @param workspace - a WorkspaceView from `workspaces.create`, or an id.
|
|
12183
|
+
* @returns `{ id, title, path, createdAt }`, or null when there is no id.
|
|
12184
|
+
*/
|
|
12185
|
+
function freshOf(workspace) {
|
|
12186
|
+
var id = sbWorkspaceId(workspace)
|
|
12187
|
+
if (id === '') return null
|
|
12188
|
+
var view = workspace && typeof workspace === 'object' ? workspace : {}
|
|
12189
|
+
var path = sbText(view.path)
|
|
12190
|
+
return {
|
|
12191
|
+
id: id,
|
|
12192
|
+
// The Host titles a Workspace after its folder; a workspace it did
|
|
12193
|
+
// not title still has the folder to fall back to, and the id is the
|
|
12194
|
+
// one thing this row must never be named after.
|
|
12195
|
+
title: sbText(view.title).trim() || sbBase(path),
|
|
12196
|
+
path: path,
|
|
12197
|
+
createdAt: sbEpoch(view.createdAt) || Date.now(),
|
|
12198
|
+
}
|
|
12199
|
+
}
|
|
12200
|
+
/** @param entry - one `freshOf` record, remembered until the projection carries it. */
|
|
12201
|
+
function rememberFresh(entry) {
|
|
12202
|
+
setFresh(function (live) {
|
|
12203
|
+
var next = {}
|
|
12204
|
+
for (var key in live) {
|
|
12205
|
+
if (Object.prototype.hasOwnProperty.call(live, key)) next[key] = live[key]
|
|
12206
|
+
}
|
|
12207
|
+
next[entry.id] = entry
|
|
12208
|
+
return next
|
|
12209
|
+
})
|
|
12210
|
+
}
|
|
12211
|
+
/** @param workspaceId - the workspace whose remembered name is no longer wanted. */
|
|
12212
|
+
function forgetFresh(workspaceId) {
|
|
12213
|
+
var id = sbText(workspaceId)
|
|
12214
|
+
if (id === '') return
|
|
12215
|
+
setFresh(function (live) {
|
|
12216
|
+
if (!Object.prototype.hasOwnProperty.call(live, id)) return live
|
|
12217
|
+
var next = {}
|
|
12218
|
+
for (var key in live) {
|
|
12219
|
+
if (Object.prototype.hasOwnProperty.call(live, key) && key !== id) next[key] = live[key]
|
|
12220
|
+
}
|
|
12221
|
+
return next
|
|
12222
|
+
})
|
|
12223
|
+
}
|
|
12224
|
+
/**
|
|
12225
|
+
* File a workspace the reader just added into the group whose own control
|
|
12226
|
+
* started the flow, and unfold that group so the new row is on screen.
|
|
12227
|
+
* A project added from "实验室" that surfaces in the ungrouped bucket reads
|
|
12228
|
+
* as "it did not land in the category at all", which is the report this
|
|
12229
|
+
* answers.
|
|
12230
|
+
*
|
|
12231
|
+
* The member record is written even when there is no group: it is what
|
|
12232
|
+
* puts the row in the list at once instead of waiting for the Host's
|
|
12233
|
+
* projection, and the remembered name is what keeps that row from
|
|
12234
|
+
* appearing as its workspace id for the first render.
|
|
12235
|
+
* @param workspace - the workspace the Host answered with, or its id.
|
|
12236
|
+
* @param groupKey - the group the flow started from; '' for the plain add.
|
|
12237
|
+
* @returns the save promise, or null when there is nothing to file.
|
|
12238
|
+
*/
|
|
12239
|
+
function fileNewWorkspace(workspace, groupKey) {
|
|
12240
|
+
var entry = freshOf(workspace)
|
|
12241
|
+
if (!entry) return null
|
|
12242
|
+
rememberFresh(entry)
|
|
12243
|
+
var key = sbText(groupKey)
|
|
12244
|
+
return kit.mutateSidebarLayout(function (live) {
|
|
12245
|
+
var next = kit.withWorkspace(live, entry.id, { group: key === '' ? null : key })
|
|
12246
|
+
if (key === '') return next
|
|
12247
|
+
var groups = kit.sidebarGroups(next)
|
|
12248
|
+
for (var i = 0; i < groups.length; i += 1) {
|
|
12249
|
+
var group = groups[i]
|
|
12250
|
+
if (group && sbText(group.id) === key && group.collapsed === true) {
|
|
12251
|
+
return kit.withGroupPatch(next, key, { collapsed: false })
|
|
12252
|
+
}
|
|
12253
|
+
}
|
|
12254
|
+
return next
|
|
12255
|
+
})
|
|
12256
|
+
}
|
|
12257
|
+
/**
|
|
12258
|
+
* Take whatever the Host's chooser resolved and register it, filing it
|
|
12259
|
+
* into the group the flow started from.
|
|
12260
|
+
* @param picked - the path the Host resolved, or a workspace it already made.
|
|
12261
|
+
* @param groupKey - the group the flow started from; '' for the plain add.
|
|
12262
|
+
* @returns the created workspace (or its save), or null.
|
|
12263
|
+
*/
|
|
12264
|
+
function adoptPicked(picked, groupKey) {
|
|
12265
|
+
var home = sbText(groupKey)
|
|
12266
|
+
// A workspace the Host already made, as opposed to a path it resolved:
|
|
12267
|
+
// a path is a string, and a workspace is the view that carries its id.
|
|
12268
|
+
if (picked && typeof picked === 'object' && sbWorkspaceId(picked) !== '') {
|
|
12269
|
+
return fileNewWorkspace(picked, home)
|
|
12270
|
+
}
|
|
11998
12271
|
var path = pickedPath(picked)
|
|
11999
12272
|
if (path === '') return null
|
|
12000
|
-
|
|
12273
|
+
var created = createWorkspace(path)
|
|
12274
|
+
if (!created || typeof created.then !== 'function') {
|
|
12275
|
+
return fileNewWorkspace(created, home)
|
|
12276
|
+
}
|
|
12277
|
+
return created.then(function (workspace) {
|
|
12278
|
+
return fileNewWorkspace(workspace, home)
|
|
12279
|
+
})
|
|
12001
12280
|
}
|
|
12002
|
-
/**
|
|
12003
|
-
|
|
12281
|
+
/**
|
|
12282
|
+
* Put the region's own directory browser up.
|
|
12283
|
+
* @param groupKey - the group the flow started from; '' for the plain add.
|
|
12284
|
+
*/
|
|
12285
|
+
function openDirectoryPicker(groupKey) {
|
|
12004
12286
|
if (!mountedRef.current) return
|
|
12005
12287
|
setActionError('')
|
|
12006
12288
|
setMenu(null)
|
|
12007
12289
|
setEditor(null)
|
|
12008
12290
|
setConfirming(null)
|
|
12009
|
-
setPicking(
|
|
12291
|
+
setPicking(sbText(groupKey))
|
|
12010
12292
|
}
|
|
12011
12293
|
/**
|
|
12012
12294
|
* Add a project. The Host's chooser is asked first: on a local connection
|
|
@@ -12016,21 +12298,36 @@ var STYLES = [
|
|
|
12016
12298
|
* then opens its own browser (see `SidebarDirectoryDialog`), which lists
|
|
12017
12299
|
* directories through the Host the way the shipped sidebar does, instead
|
|
12018
12300
|
* of leaving the control dead.
|
|
12301
|
+
* @param section - the group whose control started the flow, or null.
|
|
12019
12302
|
*/
|
|
12020
|
-
function newProject() {
|
|
12303
|
+
function newProject(section) {
|
|
12304
|
+
var home = sbText(section && section.key)
|
|
12021
12305
|
act(function () {
|
|
12022
|
-
var service =
|
|
12023
|
-
|
|
12306
|
+
var service = sbService('uiWorkspace')
|
|
12307
|
+
var picks = service && typeof service.pickDirectory === 'function'
|
|
12308
|
+
var browses = service && typeof service.listDirectory === 'function'
|
|
12309
|
+
if (!picks && !browses) {
|
|
12310
|
+
setActionError(sbT('sidebar.list.readonly'))
|
|
12311
|
+
return null
|
|
12312
|
+
}
|
|
12313
|
+
// A Host with listing but no chooser is exactly the remote case; the
|
|
12314
|
+
// region's own browser is the only interaction it can offer.
|
|
12315
|
+
if (!picks) {
|
|
12316
|
+
openDirectoryPicker(home)
|
|
12317
|
+
return null
|
|
12318
|
+
}
|
|
12024
12319
|
var outcome = null
|
|
12025
12320
|
try {
|
|
12026
12321
|
outcome = service.pickDirectory()
|
|
12027
12322
|
} catch (error) {
|
|
12028
|
-
openDirectoryPicker()
|
|
12323
|
+
openDirectoryPicker(home)
|
|
12029
12324
|
return null
|
|
12030
12325
|
}
|
|
12031
|
-
if (!outcome || typeof outcome.then !== 'function') return adoptPicked(outcome)
|
|
12032
|
-
return outcome.then(
|
|
12033
|
-
|
|
12326
|
+
if (!outcome || typeof outcome.then !== 'function') return adoptPicked(outcome, home)
|
|
12327
|
+
return outcome.then(function (picked) {
|
|
12328
|
+
return adoptPicked(picked, home)
|
|
12329
|
+
}, function () {
|
|
12330
|
+
openDirectoryPicker(home)
|
|
12034
12331
|
return null
|
|
12035
12332
|
})
|
|
12036
12333
|
})
|
|
@@ -12064,15 +12361,20 @@ var STYLES = [
|
|
|
12064
12361
|
}
|
|
12065
12362
|
/** @param project - the project to unregister. */
|
|
12066
12363
|
function removeProject(project) {
|
|
12364
|
+
var id = sbText(project && project.id)
|
|
12067
12365
|
act(function () {
|
|
12068
12366
|
var service = need('workspaces', 'delete')
|
|
12069
12367
|
if (!service) return null
|
|
12070
|
-
|
|
12368
|
+
// Its remembered name goes with it: the row it belongs to is about to
|
|
12369
|
+
// stop existing, and a later workspace never reuses the id.
|
|
12370
|
+
forgetFresh(id)
|
|
12371
|
+
return service.delete(id)
|
|
12071
12372
|
})
|
|
12072
12373
|
}
|
|
12073
12374
|
/** @param id - a workspace the document still names but the sidebar dropped. */
|
|
12074
12375
|
function forgetProject(id) {
|
|
12075
12376
|
act(function () {
|
|
12377
|
+
forgetFresh(id)
|
|
12076
12378
|
return kit.mutateSidebarLayout(function (live) { return kit.withoutWorkspace(live, id) })
|
|
12077
12379
|
})
|
|
12078
12380
|
}
|
|
@@ -12425,7 +12727,7 @@ var STYLES = [
|
|
|
12425
12727
|
}
|
|
12426
12728
|
/** @param choice - a group menu id. @param section - the group section. */
|
|
12427
12729
|
function runGroupChoice(choice, section) {
|
|
12428
|
-
if (choice === 'newProject') return newProject()
|
|
12730
|
+
if (choice === 'newProject') return newProject(section)
|
|
12429
12731
|
if (choice === 'rename') return openEditor('group', sbText(section && section.key), section && section.title)
|
|
12430
12732
|
if (choice.indexOf('color:') === 0) return markSection(section, choice.slice(6))
|
|
12431
12733
|
if (choice === 'sort') return setSectionSort(section, section && section.sort === 'recent' ? 'manual' : 'recent')
|
|
@@ -12755,8 +13057,12 @@ var STYLES = [
|
|
|
12755
13057
|
head.push(h('span', { className: 'ph-sb-actions', key: 'actions' }, [
|
|
12756
13058
|
sbIconButton('plus', sbT('sidebar.list.newProject'), function (event) {
|
|
12757
13059
|
if (event && event.stopPropagation) event.stopPropagation()
|
|
12758
|
-
newProject()
|
|
12759
|
-
}, {
|
|
13060
|
+
newProject(section)
|
|
13061
|
+
}, {
|
|
13062
|
+
key: 'new',
|
|
13063
|
+
sb: 'newProject',
|
|
13064
|
+
disabled: !sbHas('uiWorkspace', 'pickDirectory') && !sbHas('uiWorkspace', 'listDirectory'),
|
|
13065
|
+
}),
|
|
12760
13066
|
sbMenu('group:' + key, open, anchor, entries, function (choice) { runGroupChoice(choice, section) }, closeMenu),
|
|
12761
13067
|
]))
|
|
12762
13068
|
}
|
|
@@ -12963,11 +13269,15 @@ var STYLES = [
|
|
|
12963
13269
|
cancelEditor,
|
|
12964
13270
|
)
|
|
12965
13271
|
}
|
|
12966
|
-
if (picking) {
|
|
13272
|
+
if (picking !== null) {
|
|
12967
13273
|
layer = h(SidebarDirectoryDialog, {
|
|
12968
13274
|
key: 'picker',
|
|
12969
|
-
onCancel: function () { setPicking(
|
|
12970
|
-
onPicked: function () {
|
|
13275
|
+
onCancel: function () { setPicking(null) },
|
|
13276
|
+
onPicked: function (workspace) {
|
|
13277
|
+
var home = picking
|
|
13278
|
+
setPicking(null)
|
|
13279
|
+
attempt(function () { return fileNewWorkspace(workspace, home) })
|
|
13280
|
+
},
|
|
12971
13281
|
})
|
|
12972
13282
|
}
|
|
12973
13283
|
if (confirming) {
|
package/lib/framework.part.js
CHANGED
|
@@ -3272,10 +3272,10 @@ var GRAIN = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org
|
|
|
3272
3272
|
// The region's own directory browser (the shell's `Modal` draws the card
|
|
3273
3273
|
// when the kit is there; these are the rows both draw).
|
|
3274
3274
|
'.ph-sb-dir{display:flex;flex-direction:column;gap:6px;min-height:180px;max-height:46vh}',
|
|
3275
|
-
'.ph-sb-dir-crumbs{display:flex;align-items:center;flex-wrap:wrap;gap:0 2px;min-height:22px}',
|
|
3275
|
+
'.ph-sb-dir-crumbs{display:flex;align-items:center;flex-wrap:wrap;gap:0 2px;min-height:22px;flex:none}',
|
|
3276
3276
|
// The trail turns into a path field: the shipped browser lets a reader
|
|
3277
3277
|
// type an absolute path instead of walking to it.
|
|
3278
|
-
'.ph-sb-dir-path{display:flex;align-items:center;gap:6px;min-height:22px}',
|
|
3278
|
+
'.ph-sb-dir-path{display:flex;align-items:center;gap:6px;min-height:22px;flex:none}',
|
|
3279
3279
|
'.ph-sb-dir-path-field{flex:1 1 auto;width:auto}',
|
|
3280
3280
|
'.ph-sb-dir-edit{margin-left:auto}',
|
|
3281
3281
|
'.ph-sb-dir-seat{display:inline-flex;align-items:center;gap:2px}',
|
|
@@ -3291,7 +3291,11 @@ var GRAIN = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org
|
|
|
3291
3291
|
'.ph-sb-dir-crumb:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid ' +
|
|
3292
3292
|
'var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#3b6fe0));outline-offset:1px}',
|
|
3293
3293
|
'.ph-sb-dir-crumb.is-current{color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
3294
|
-
|
|
3294
|
+
// The list is the one child that gives way when the dialog hits its
|
|
3295
|
+
// height: a row of text shrunk to fit its box, which is how the target
|
|
3296
|
+
// line lost its lower half. Every other child keeps its height, and the
|
|
3297
|
+
// list scrolls instead.
|
|
3298
|
+
'.ph-sb-dir-list{display:flex;flex-direction:column;gap:1px;min-height:0;overflow:auto;flex:1 1 auto}',
|
|
3295
3299
|
'.ph-sb-dir-row{display:flex;align-items:center;gap:4px;width:100%;box-sizing:border-box;height:28px;padding:0 4px;' +
|
|
3296
3300
|
'border:0;border-radius:var(--dsw-radius-sm,6px);background:transparent;cursor:pointer;text-align:left;' +
|
|
3297
3301
|
'font-family:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
@@ -3303,8 +3307,18 @@ var GRAIN = 'url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org
|
|
|
3303
3307
|
'.ph-sb-dir-row.is-hidden{opacity:.62}',
|
|
3304
3308
|
'.ph-sb-dir-icon{flex:none;display:inline-flex;opacity:.7}',
|
|
3305
3309
|
'.ph-sb-dir-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
3306
|
-
'.ph-sb-dir-note{padding:4px;font-size:12px;line-height:18px;
|
|
3307
|
-
'
|
|
3310
|
+
'.ph-sb-dir-note{padding:4px;font-size:12px;line-height:18px;flex:none;' +
|
|
3311
|
+
'color:var(--dsw-alias-label-secondary,#9aa4b2)}',
|
|
3312
|
+
// The target is a path, and the part of it that tells two folders apart is
|
|
3313
|
+
// the end: it wraps rather than ellipsizing the folder away, and it never
|
|
3314
|
+
// gives up height to the scrolling list below it (a clipped line is what a
|
|
3315
|
+
// shrunk flex item with `overflow:hidden` looks like).
|
|
3316
|
+
'.ph-sb-dir-target{flex:none;overflow-wrap:anywhere;' +
|
|
3317
|
+
'color:var(--dsw-alias-label-primary,#e6e8ea)}',
|
|
3318
|
+
'.ph-sb-dir-newfold{display:flex;align-items:center;gap:6px;flex:none}',
|
|
3319
|
+
// The error line sits in the same column: it is the one thing the reader
|
|
3320
|
+
// must be able to read, so it keeps its height like the notes above it.
|
|
3321
|
+
'.ph-sb-dir .ph-sb-form-error{flex:none}',
|
|
3308
3322
|
'.ph-sb-dir-newfold .ph-sb-field{flex:1 1 auto}',
|
|
3309
3323
|
// The footer toggle from the shipped browser: plain text at control size,
|
|
3310
3324
|
// no border, and the pressed state marked by a trailing check so the
|
|
@@ -4926,6 +4940,7 @@ var STYLES = [
|
|
|
4926
4940
|
'sidebar.list.dirEmpty': '没有子文件夹',
|
|
4927
4941
|
'sidebar.list.dirHiddenOnly': '没有可见的子文件夹,隐藏项已折叠',
|
|
4928
4942
|
'sidebar.list.dirTruncated': '文件夹太多,只列出了前一部分',
|
|
4943
|
+
'sidebar.list.dirTarget': '将在此创建工作区:{path}',
|
|
4929
4944
|
'sidebar.list.dirFailed': '读不到这个目录:{reason}',
|
|
4930
4945
|
'sidebar.list.apply': '确定',
|
|
4931
4946
|
'sidebar.list.more': '更多',
|
|
@@ -5156,6 +5171,7 @@ var STYLES = [
|
|
|
5156
5171
|
'sidebar.list.dirEmpty': 'No subfolders',
|
|
5157
5172
|
'sidebar.list.dirHiddenOnly': 'No visible subfolders; hidden items are folded',
|
|
5158
5173
|
'sidebar.list.dirTruncated': 'Too many folders; only the first ones are listed',
|
|
5174
|
+
'sidebar.list.dirTarget': 'Creates the workspace in: {path}',
|
|
5159
5175
|
'sidebar.list.dirFailed': 'Cannot read this folder: {reason}',
|
|
5160
5176
|
'sidebar.list.apply': 'Apply',
|
|
5161
5177
|
'sidebar.list.more': 'More',
|
|
@@ -5389,6 +5405,20 @@ var STYLES = [
|
|
|
5389
5405
|
updatedAt: Number(row.updatedAt) || 0,
|
|
5390
5406
|
}
|
|
5391
5407
|
}
|
|
5408
|
+
/**
|
|
5409
|
+
* Read one workspace instant: the Host stamps Workspace rows with ISO-8601
|
|
5410
|
+
* strings, while a projection that was already reduced can carry epoch
|
|
5411
|
+
* milliseconds. Both are wanted by the list, which orders by them.
|
|
5412
|
+
* @param value - an ISO-8601 instant, epoch milliseconds, or anything else.
|
|
5413
|
+
* @returns epoch milliseconds, or 0 when it names no time.
|
|
5414
|
+
*/
|
|
5415
|
+
function epochOf(value) {
|
|
5416
|
+
if (typeof value === 'number') return isFinite(value) && value > 0 ? value : 0
|
|
5417
|
+
var text = String(value === undefined || value === null ? '' : value)
|
|
5418
|
+
if (text === '') return 0
|
|
5419
|
+
var parsed = Date.parse(text)
|
|
5420
|
+
return isFinite(parsed) && parsed > 0 ? parsed : 0
|
|
5421
|
+
}
|
|
5392
5422
|
/**
|
|
5393
5423
|
* @param value - a workspace projection array.
|
|
5394
5424
|
* @returns a clean projection array.
|
|
@@ -5403,6 +5433,10 @@ var STYLES = [
|
|
|
5403
5433
|
title: String(item.title === undefined || item.title === null ? '' : item.title),
|
|
5404
5434
|
path: String(item.path === undefined || item.path === null ? '' : item.path),
|
|
5405
5435
|
sessions: rows.length || (Number(item.sessions) || 0),
|
|
5436
|
+
// When the Workspace itself was registered. A project with no Sessions
|
|
5437
|
+
// is not old — creating it is an event — so the list's recency order
|
|
5438
|
+
// counts this alongside its newest Session.
|
|
5439
|
+
createdAt: epochOf(item.createdAt),
|
|
5406
5440
|
rows: rows,
|
|
5407
5441
|
}
|
|
5408
5442
|
}).filter(function (entry) { return entry.id !== '' })
|
|
@@ -5586,6 +5620,7 @@ var STYLES = [
|
|
|
5586
5620
|
id: view.workspaceId,
|
|
5587
5621
|
title: view.title,
|
|
5588
5622
|
path: view.path,
|
|
5623
|
+
createdAt: epochOf(view.createdAt),
|
|
5589
5624
|
rows: rows,
|
|
5590
5625
|
}
|
|
5591
5626
|
})
|
|
@@ -5645,7 +5680,7 @@ var STYLES = [
|
|
|
5645
5680
|
var parts = [String(current || ''), Object.keys(subagents).sort().join(',')]
|
|
5646
5681
|
for (var i = 0; i < items.length; i += 1) {
|
|
5647
5682
|
var item = items[i]
|
|
5648
|
-
parts.push(item.id, item.title, item.path, String(item.sessions))
|
|
5683
|
+
parts.push(item.id, item.title, item.path, String(item.sessions), String(item.createdAt))
|
|
5649
5684
|
for (var j = 0; j < item.rows.length; j += 1) {
|
|
5650
5685
|
var row = item.rows[j]
|
|
5651
5686
|
parts.push(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-homepage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "DeepSeek Harness plugin: a personal home page in the sidebar for a DSH instance. Review a plugin before installing it, and read sessions, projects, tool usage, device, storage, DSH cores, Skills and cost from one composable, registrable card wall.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|