pinokiod 8.0.105 → 8.0.107
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/kernel/vault/index.js +26 -10
- package/package.json +1 -1
- package/server/public/vault.css +58 -1
- package/server/public/vault.js +111 -19
- package/test/vault-engine.test.js +6 -7
- package/test/vault-ui.test.js +56 -6
package/kernel/vault/index.js
CHANGED
|
@@ -44,6 +44,12 @@ const AUTOMATIC_ACTIONS = new Set([
|
|
|
44
44
|
])
|
|
45
45
|
const PERMISSION_DENIED_CODES = new Set(["EACCES", "EPERM"])
|
|
46
46
|
const BUSY_CODES = new Set(["EBUSY"])
|
|
47
|
+
const PUBLIC_FILE_STATUS = {
|
|
48
|
+
reference: "tracked",
|
|
49
|
+
duplicate: "duplicate",
|
|
50
|
+
unavailable: "unavailable",
|
|
51
|
+
linked: "shared"
|
|
52
|
+
}
|
|
47
53
|
const hardlinkUnavailableCode = (code) =>
|
|
48
54
|
HARDLINK_UNSUPPORTED_CODES.has(code) || PERMISSION_DENIED_CODES.has(code)
|
|
49
55
|
const replacementLockedCode = (code) =>
|
|
@@ -197,7 +203,16 @@ const revealInFileManager = (filePath, platform = process.platform) =>
|
|
|
197
203
|
execFile(command, args, {
|
|
198
204
|
timeout: 10000,
|
|
199
205
|
windowsHide: true
|
|
200
|
-
}, (error) =>
|
|
206
|
+
}, (error) => {
|
|
207
|
+
// Explorer reports a non-zero exit code even when it opens the folder,
|
|
208
|
+
// so an exit status alone is not a failure on Windows. A missing binary
|
|
209
|
+
// or a refused spawn still is.
|
|
210
|
+
if (error && platform === "win32" && Number.isInteger(error.code)) {
|
|
211
|
+
resolve()
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
error ? reject(error) : resolve()
|
|
215
|
+
})
|
|
201
216
|
})
|
|
202
217
|
|
|
203
218
|
class Vault {
|
|
@@ -1359,8 +1374,10 @@ class Vault {
|
|
|
1359
1374
|
if (!entry || entry.unavailable_reason === "stale") {
|
|
1360
1375
|
return { error: "This file is no longer tracked. Scan again to refresh this view." }
|
|
1361
1376
|
}
|
|
1377
|
+
// Opening a file manager mutates nothing, so it is allowed for any tracked
|
|
1378
|
+
// path regardless of the scope the request came from.
|
|
1362
1379
|
const source = this.sourceForPath(entry.path, entry.source_id)
|
|
1363
|
-
if (!source
|
|
1380
|
+
if (!source) {
|
|
1364
1381
|
return { error: "This file is outside the current location." }
|
|
1365
1382
|
}
|
|
1366
1383
|
if (!await this.canonicalPathIsWithinSource(entry.path, source)) {
|
|
@@ -1373,7 +1390,10 @@ class Vault {
|
|
|
1373
1390
|
try {
|
|
1374
1391
|
await this.fileManagerLauncher(entry.path)
|
|
1375
1392
|
} catch (error) {
|
|
1376
|
-
|
|
1393
|
+
// Naming the underlying failure is what makes a report actionable; the
|
|
1394
|
+
// generic sentence hid whether the spawn, the binary, or the path failed.
|
|
1395
|
+
const detail = error && error.message ? ` (${error.message})` : ""
|
|
1396
|
+
return { error: `The file manager could not open this file.${detail}` }
|
|
1377
1397
|
}
|
|
1378
1398
|
return { revealed: true }
|
|
1379
1399
|
}
|
|
@@ -3253,12 +3273,7 @@ class Vault {
|
|
|
3253
3273
|
dev: match.dev,
|
|
3254
3274
|
ino: match.ino
|
|
3255
3275
|
}, this.locationForPath(match.path, match.source_id)))
|
|
3256
|
-
const publicStatus =
|
|
3257
|
-
reference: "tracked",
|
|
3258
|
-
duplicate: "duplicate",
|
|
3259
|
-
unavailable: "unavailable",
|
|
3260
|
-
linked: "shared"
|
|
3261
|
-
}[row.status]
|
|
3276
|
+
const publicStatus = PUBLIC_FILE_STATUS[row.status]
|
|
3262
3277
|
const result = Object.assign({
|
|
3263
3278
|
path: row.path,
|
|
3264
3279
|
hash: row.hash,
|
|
@@ -3388,7 +3403,8 @@ class Vault {
|
|
|
3388
3403
|
return {
|
|
3389
3404
|
path: target,
|
|
3390
3405
|
items: (result.rows || []).map((row) => Object.assign({
|
|
3391
|
-
path: row.path
|
|
3406
|
+
path: row.path,
|
|
3407
|
+
status: PUBLIC_FILE_STATUS[row.status] || null
|
|
3392
3408
|
}, this.locationForPath(row.path, row.source_id))),
|
|
3393
3409
|
total: Number(result.total) || 0,
|
|
3394
3410
|
next_cursor: result.nextCursor || null
|
package/package.json
CHANGED
package/server/public/vault.css
CHANGED
|
@@ -964,6 +964,32 @@ body[data-vault-mode="global"] .vault-explorer {
|
|
|
964
964
|
outline: 2px solid color-mix(in srgb, var(--task-accent) 55%, transparent);
|
|
965
965
|
outline-offset: 1px;
|
|
966
966
|
}
|
|
967
|
+
.vault-confirm-overlay {
|
|
968
|
+
position: fixed;
|
|
969
|
+
z-index: 140;
|
|
970
|
+
inset: 0;
|
|
971
|
+
display: grid;
|
|
972
|
+
place-items: center;
|
|
973
|
+
padding: 24px;
|
|
974
|
+
background: color-mix(in srgb, var(--task-text) 34%, transparent);
|
|
975
|
+
}
|
|
976
|
+
.vault-confirm-overlay[hidden] { display: none; }
|
|
977
|
+
.vault-confirm-dialog {
|
|
978
|
+
width: min(420px, 100%);
|
|
979
|
+
padding: 18px 20px 16px;
|
|
980
|
+
border: 1px solid var(--task-border-strong);
|
|
981
|
+
border-radius: 10px;
|
|
982
|
+
background: var(--task-panel);
|
|
983
|
+
box-shadow: 0 22px 70px color-mix(in srgb, var(--task-text) 24%, transparent);
|
|
984
|
+
}
|
|
985
|
+
.vault-confirm-message {
|
|
986
|
+
margin: 0 0 16px;
|
|
987
|
+
color: var(--task-text);
|
|
988
|
+
font-size: 12.5px;
|
|
989
|
+
line-height: 1.5;
|
|
990
|
+
}
|
|
991
|
+
.vault-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
992
|
+
|
|
967
993
|
.vault-find-overlay {
|
|
968
994
|
position: fixed;
|
|
969
995
|
z-index: 120;
|
|
@@ -2043,9 +2069,40 @@ body.dark .vault-row-action .vault-text-button:hover:not(:disabled) {
|
|
|
2043
2069
|
background: color-mix(in srgb, var(--task-soft) 48%, transparent);
|
|
2044
2070
|
}
|
|
2045
2071
|
.vault-detail-label { margin-bottom: 5px; color: var(--task-muted); font-size: 10px; font-weight: 650; }
|
|
2046
|
-
.vault-location-detail { display: flex; align-items: center; gap: 7px;
|
|
2072
|
+
.vault-location-detail { display: flex; align-items: center; gap: 7px; min-height: 26px; color: var(--task-text); font-size: 10.5px; }
|
|
2047
2073
|
.vault-location-detail i { color: var(--task-muted); }
|
|
2048
2074
|
.vault-location-detail > span { min-width: 0; flex: 1; overflow-wrap: anywhere; }
|
|
2075
|
+
.vault-location-detail > .vault-location-path {
|
|
2076
|
+
min-width: 0;
|
|
2077
|
+
flex: 1;
|
|
2078
|
+
white-space: nowrap;
|
|
2079
|
+
overflow: hidden;
|
|
2080
|
+
text-overflow: ellipsis;
|
|
2081
|
+
}
|
|
2082
|
+
.vault-location-detail > .vault-location-note {
|
|
2083
|
+
flex: 0 0 auto;
|
|
2084
|
+
margin-left: auto;
|
|
2085
|
+
padding-left: 12px;
|
|
2086
|
+
color: var(--task-muted);
|
|
2087
|
+
white-space: nowrap;
|
|
2088
|
+
}
|
|
2089
|
+
.vault-location-detail > .vault-location-reveal-placeholder { flex: 0 0 26px; }
|
|
2090
|
+
.vault-location-link {
|
|
2091
|
+
appearance: none;
|
|
2092
|
+
padding: 0;
|
|
2093
|
+
border: 0;
|
|
2094
|
+
background: none;
|
|
2095
|
+
color: var(--task-accent);
|
|
2096
|
+
font: inherit;
|
|
2097
|
+
text-align: left;
|
|
2098
|
+
cursor: pointer;
|
|
2099
|
+
overflow: hidden;
|
|
2100
|
+
text-overflow: ellipsis;
|
|
2101
|
+
white-space: nowrap;
|
|
2102
|
+
max-width: 100%;
|
|
2103
|
+
}
|
|
2104
|
+
.vault-location-link:hover { text-decoration: underline; }
|
|
2105
|
+
.vault-location-link i { color: inherit; font-size: 9px; margin-left: 5px; }
|
|
2049
2106
|
.vault-duplicate-content-group {
|
|
2050
2107
|
min-height: 52px;
|
|
2051
2108
|
background: color-mix(in srgb, var(--task-soft) 28%, transparent);
|
package/server/public/vault.js
CHANGED
|
@@ -237,6 +237,9 @@ const COPY = {
|
|
|
237
237
|
loading_copies: "Loading copies…",
|
|
238
238
|
show_more_copies: "Show {count} more copies",
|
|
239
239
|
loading_locations: "Loading locations…",
|
|
240
|
+
open_in_disk_saver: "Open this file in Disk Saver",
|
|
241
|
+
open_location_confirm: "Open Disk Saver for {location}?",
|
|
242
|
+
open_location_accept: "Open",
|
|
240
243
|
show_more_locations: "Show {count} more locations",
|
|
241
244
|
making_separate: "Making file separate",
|
|
242
245
|
make_separate: "Make separate",
|
|
@@ -258,7 +261,12 @@ const COPY = {
|
|
|
258
261
|
separation_cancelled: "Separation cancelled after {count}.",
|
|
259
262
|
select_for_separation: "Select to make separate",
|
|
260
263
|
select_all_on_page: "Select all on this page",
|
|
261
|
-
|
|
264
|
+
stored_times: "Stored {count} times · {size} each",
|
|
265
|
+
this_file: "this file",
|
|
266
|
+
copy_that_stays: "the copy that stays",
|
|
267
|
+
already_deduplicated: "already deduplicated",
|
|
268
|
+
not_deduplicated_yet: "not deduplicated yet",
|
|
269
|
+
cannot_be_deduplicated: "cannot be deduplicated",
|
|
262
270
|
no_files: "No files found",
|
|
263
271
|
no_files_hint: "Run a scan to find files that can be deduplicated. Scanning never links files together or replaces them.",
|
|
264
272
|
scan_waiting: "Waiting for scan results",
|
|
@@ -290,6 +298,15 @@ const COPY = {
|
|
|
290
298
|
}
|
|
291
299
|
|
|
292
300
|
const SCOPE_ID = document.body.dataset.vaultScope || null
|
|
301
|
+
const FOCUS_GROUP = (() => {
|
|
302
|
+
const value = new URLSearchParams(window.location.search).get("group")
|
|
303
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value.toLowerCase())
|
|
304
|
+
? value.toLowerCase()
|
|
305
|
+
: null
|
|
306
|
+
})()
|
|
307
|
+
// A scoped page hands one content group off to the global page, which opens
|
|
308
|
+
// on it once its first page of duplicate results arrives.
|
|
309
|
+
let focusGroupPending = !!FOCUS_GROUP
|
|
293
310
|
const IS_APP_MODE = document.body.dataset.vaultMode === "app" && !!SCOPE_ID
|
|
294
311
|
const APP_NAME = IS_APP_MODE ? document.body.dataset.vaultApp || "" : ""
|
|
295
312
|
const HOME_PATH = document.body.dataset.vaultHome || ""
|
|
@@ -410,12 +427,12 @@ const state = {
|
|
|
410
427
|
candidateSize: defaultCandidateSize,
|
|
411
428
|
persistedCandidateSize: defaultCandidateSize,
|
|
412
429
|
candidateSizeInitialized: false,
|
|
413
|
-
view: "all",
|
|
430
|
+
view: FOCUS_GROUP ? "duplicates" : "all",
|
|
414
431
|
sourceId: SCOPE_ID,
|
|
415
432
|
query: "",
|
|
416
433
|
statusFilter: "all",
|
|
417
|
-
displayMode: "folders",
|
|
418
|
-
sizeSort: null,
|
|
434
|
+
displayMode: FOCUS_GROUP ? "files" : "folders",
|
|
435
|
+
sizeSort: FOCUS_GROUP ? "desc" : null,
|
|
419
436
|
collapsedSources: new Set(),
|
|
420
437
|
collapsedDirs: new Set(),
|
|
421
438
|
expandedFiles: new Set(),
|
|
@@ -609,9 +626,56 @@ const saveCandidateSize = (size) => {
|
|
|
609
626
|
})
|
|
610
627
|
return candidateSizeSaveTail
|
|
611
628
|
}
|
|
629
|
+
// Small confirm dialog in the workspace's own visual language, so leaving the
|
|
630
|
+
// page never happens through a native browser prompt.
|
|
631
|
+
const confirmLeave = (message, confirmLabel) => new Promise((resolve) => {
|
|
632
|
+
const overlay = document.createElement("div")
|
|
633
|
+
overlay.className = "vault-confirm-overlay"
|
|
634
|
+
overlay.innerHTML = `<section class="vault-confirm-dialog" role="dialog" aria-modal="true"><p class="vault-confirm-message"></p><div class="vault-confirm-actions"><button class="vault-button" type="button" data-confirm-cancel>${esc(COPY.cancel)}</button><button class="vault-button primary" type="button" data-confirm-accept>${esc(confirmLabel)}</button></div></section>`
|
|
635
|
+
overlay.querySelector(".vault-confirm-message").textContent = message
|
|
636
|
+
const settle = (value) => {
|
|
637
|
+
document.removeEventListener("keydown", onKey, true)
|
|
638
|
+
overlay.remove()
|
|
639
|
+
resolve(value)
|
|
640
|
+
}
|
|
641
|
+
const onKey = (event) => {
|
|
642
|
+
if (event.key !== "Escape") return
|
|
643
|
+
event.preventDefault()
|
|
644
|
+
event.stopPropagation()
|
|
645
|
+
settle(false)
|
|
646
|
+
}
|
|
647
|
+
overlay.addEventListener("click", (event) => {
|
|
648
|
+
if (event.target === overlay) settle(false)
|
|
649
|
+
if (event.target.closest("[data-confirm-cancel]")) settle(false)
|
|
650
|
+
if (event.target.closest("[data-confirm-accept]")) settle(true)
|
|
651
|
+
})
|
|
652
|
+
document.addEventListener("keydown", onKey, true)
|
|
653
|
+
document.body.appendChild(overlay)
|
|
654
|
+
const accept = overlay.querySelector("[data-confirm-accept]")
|
|
655
|
+
if (accept) accept.focus()
|
|
656
|
+
})
|
|
657
|
+
|
|
612
658
|
const openGlobalWorkspace = () => {
|
|
613
659
|
window.parent.location.assign("/vault")
|
|
614
660
|
}
|
|
661
|
+
// A copy that belongs to another app opens that app's own workspace, where it
|
|
662
|
+
// can actually be acted on; caches and external folders have no app page, so
|
|
663
|
+
// they open the global one. Accepting replaces the parent window, because this
|
|
664
|
+
// workspace can be embedded in an app page and navigating in place would nest a
|
|
665
|
+
// second copy of the interface inside that frame.
|
|
666
|
+
const openContentGroup = async (group, kind, label) => {
|
|
667
|
+
// The label and kind travel with the row: an app workspace cannot look up
|
|
668
|
+
// sources belonging to other apps, so a lookup here would name every one of
|
|
669
|
+
// them "unknown" and send them all to the global page.
|
|
670
|
+
const app = kind === "app" && label ? label : null
|
|
671
|
+
const name = label || COPY.unknown_location
|
|
672
|
+
const accepted = await confirmLeave(
|
|
673
|
+
COPY.open_location_confirm.replace("{location}", name),
|
|
674
|
+
COPY.open_location_accept)
|
|
675
|
+
if (!accepted) return
|
|
676
|
+
const base = app ? `/vault/app/${encodeURIComponent(app)}` : "/vault"
|
|
677
|
+
window.parent.location.assign(`${base}?group=${encodeURIComponent(group)}`)
|
|
678
|
+
}
|
|
615
679
|
const applyAutomaticScanSnapshot = (snapshot) => {
|
|
616
680
|
if (!IS_APP_MODE || !AUTOMATIC_SUPPORTED) return
|
|
617
681
|
const settings = snapshot && Array.isArray(snapshot.settings)
|
|
@@ -703,24 +767,15 @@ const onAutomaticScanMessage = (event) => {
|
|
|
703
767
|
}
|
|
704
768
|
const sourceById = (id) => (state.data.sources || []).find((source) => source.id === id)
|
|
705
769
|
const sourceChildren = (id) => (state.data.sources || []).filter((source) => source.parent_id === id)
|
|
706
|
-
const sourceIsWithinScope = (sourceId) => {
|
|
707
|
-
if (!SCOPE_ID) return true
|
|
708
|
-
const seen = new Set()
|
|
709
|
-
let source = sourceById(sourceId)
|
|
710
|
-
while (source && !seen.has(source.id)) {
|
|
711
|
-
if (source.id === SCOPE_ID) return true
|
|
712
|
-
seen.add(source.id)
|
|
713
|
-
source = sourceById(source.parent_id)
|
|
714
|
-
}
|
|
715
|
-
return false
|
|
716
|
-
}
|
|
717
770
|
const revealLabel = document.body.dataset.platform === "darwin"
|
|
718
771
|
? COPY.show_in_finder
|
|
719
772
|
: document.body.dataset.platform === "win32"
|
|
720
773
|
? COPY.show_in_file_explorer
|
|
721
774
|
: COPY.open_containing_folder
|
|
775
|
+
// Revealing changes nothing, so it is offered for every tracked path, not
|
|
776
|
+
// only the ones the current scope can act on.
|
|
722
777
|
const revealButton = (filePath, sourceId, name) => {
|
|
723
|
-
if (!filePath
|
|
778
|
+
if (!filePath) return ""
|
|
724
779
|
const label = `${revealLabel}: ${name || basename(filePath)}`
|
|
725
780
|
return `<button class="vault-reveal-button" type="button" data-reveal-file="${attr(filePath)}" aria-label="${attr(label)}" title="${attr(revealLabel)}"><i class="fa-regular fa-folder-open" aria-hidden="true"></i></button>`
|
|
726
781
|
}
|
|
@@ -1720,13 +1775,37 @@ const fileDetail = (item) => {
|
|
|
1720
1775
|
return `<div class="vault-detail"><div class="vault-detail-label error">${esc(locations.error)}</div></div>`
|
|
1721
1776
|
}
|
|
1722
1777
|
const total = Math.max(locations.items.length, Number(locations.total) || 0)
|
|
1723
|
-
|
|
1778
|
+
// Copies are byte-identical, so the size is stated once for the group.
|
|
1779
|
+
const label = COPY.stored_times
|
|
1780
|
+
.replace("{count}", total)
|
|
1781
|
+
.replace("{size}", fmt(item.size))
|
|
1724
1782
|
const remaining = Math.max(0, total - locations.items.length)
|
|
1725
1783
|
const more = locations.nextCursor
|
|
1726
1784
|
? `<div class="vault-location-detail"><button class="vault-text-button" type="button" data-more-file-locations="${attr(item.path)}" ${locations.loading ? "disabled" : ""}>${esc(COPY.show_more_locations.replace("{count}", Math.min(remaining, DUPLICATE_CHILD_PAGE_SIZE)))}</button></div>`
|
|
1727
1785
|
: ""
|
|
1728
|
-
|
|
1729
|
-
|
|
1786
|
+
const body = locations.items.map((location) => {
|
|
1787
|
+
const where = externalLocation(location) ||
|
|
1788
|
+
[location.source_label, location.relative_path].filter(Boolean).join(" / ")
|
|
1789
|
+
const note = location.path === item.path
|
|
1790
|
+
? COPY.this_file
|
|
1791
|
+
: location.status === "tracked"
|
|
1792
|
+
? COPY.copy_that_stays
|
|
1793
|
+
: location.status === "shared"
|
|
1794
|
+
? COPY.already_deduplicated
|
|
1795
|
+
: location.status === "unavailable"
|
|
1796
|
+
? COPY.cannot_be_deduplicated
|
|
1797
|
+
: COPY.not_deduplicated_yet
|
|
1798
|
+
const outside = SCOPE_ID && item.hash && location.source_id !== SCOPE_ID
|
|
1799
|
+
const text = outside
|
|
1800
|
+
? `<button class="vault-location-link" type="button" data-open-group="${attr(item.hash)}" data-open-kind="${attr(location.source_kind || "")}" data-open-label="${attr(location.source_label || "")}" title="${attr(COPY.open_in_disk_saver)}">${esc(where)}</button>`
|
|
1801
|
+
: esc(where)
|
|
1802
|
+
// Rows keep identical slots whether or not a reveal button exists, so the
|
|
1803
|
+
// notes form a column and every row is the same height.
|
|
1804
|
+
const reveal = revealButton(
|
|
1805
|
+
location.path, location.source_id, basename(location.relative_path))
|
|
1806
|
+
return `<div class="vault-location-detail"><i class="fa-regular fa-file"></i><span class="vault-location-path" title="${attr(where)}">${text}</span><span class="vault-location-note">${esc(note)}</span>${reveal || `<span class="vault-location-reveal-placeholder"></span>`}</div>`
|
|
1807
|
+
}).join("")
|
|
1808
|
+
return `<div class="vault-detail"><div class="vault-detail-label">${esc(label)}</div>${body}${more}</div>`
|
|
1730
1809
|
}
|
|
1731
1810
|
const separateCheckbox = (item) => item.status === "shared"
|
|
1732
1811
|
? `<input class="vault-row-checkbox" type="checkbox" data-select-separate="${attr(item.path)}" aria-label="${attr(`${COPY.select_for_separation}: ${basename(item.relative_path)}`)}" ${state.separateAllMatching || state.selectedSeparateFiles.has(item.path) ? "checked" : ""} />`
|
|
@@ -2942,6 +3021,12 @@ const applyFullData = (data) => {
|
|
|
2942
3021
|
reviewedScan() !== String(data.last_scan.ts) &&
|
|
2943
3022
|
(shareableDuplicateCount > 0 || data.last_scan.partial)
|
|
2944
3023
|
settleFolderDiscoveryStart()
|
|
3024
|
+
if (focusGroupPending &&
|
|
3025
|
+
data.inventory && data.inventory.view === "duplicates") {
|
|
3026
|
+
focusGroupPending = false
|
|
3027
|
+
state.expandedDuplicateGroups.add(FOCUS_GROUP)
|
|
3028
|
+
loadDuplicateGroupChildren(FOCUS_GROUP)
|
|
3029
|
+
}
|
|
2945
3030
|
const publishedScan = data.last_scan ? data.last_scan.ts : null
|
|
2946
3031
|
if (publishedScan !== state.locationsScanTs) {
|
|
2947
3032
|
state.locationsScanTs = publishedScan
|
|
@@ -3400,6 +3485,13 @@ const closeScanSizeMenu = () => {
|
|
|
3400
3485
|
document.addEventListener("click", async (event) => {
|
|
3401
3486
|
const target = event.target.closest("button")
|
|
3402
3487
|
if (!target) return
|
|
3488
|
+
if (target.dataset.openGroup) {
|
|
3489
|
+
await openContentGroup(
|
|
3490
|
+
target.dataset.openGroup,
|
|
3491
|
+
target.dataset.openKind,
|
|
3492
|
+
target.dataset.openLabel)
|
|
3493
|
+
return
|
|
3494
|
+
}
|
|
3403
3495
|
if (target.hasAttribute("data-dismiss-automatic-scan-coachmark")) {
|
|
3404
3496
|
dismissAutomaticScanCoachmark(true)
|
|
3405
3497
|
return
|
|
@@ -1553,7 +1553,7 @@ describe("Save Space engine", () => {
|
|
|
1553
1553
|
await close(vault)
|
|
1554
1554
|
})
|
|
1555
1555
|
|
|
1556
|
-
test("revealing
|
|
1556
|
+
test("revealing works for any tracked path, whatever the scope", async () => {
|
|
1557
1557
|
const { home, vault } = await makeVault()
|
|
1558
1558
|
const pair = await duplicatePair(home, 'model "quoted".bin')
|
|
1559
1559
|
await vault.sweeper.scan()
|
|
@@ -1571,19 +1571,18 @@ describe("Save Space engine", () => {
|
|
|
1571
1571
|
}), { revealed: true })
|
|
1572
1572
|
assert.deepEqual(launched, [pair.first])
|
|
1573
1573
|
|
|
1574
|
-
|
|
1574
|
+
assert.notEqual(first.source_id, second.source_id)
|
|
1575
|
+
assert.deepEqual(await vault.perform("reveal", {
|
|
1575
1576
|
scope_id: first.source_id,
|
|
1576
1577
|
path: pair.second
|
|
1577
|
-
})
|
|
1578
|
-
assert.
|
|
1579
|
-
assert.notEqual(first.source_id, second.source_id)
|
|
1580
|
-
assert.deepEqual(launched, [pair.first])
|
|
1578
|
+
}), { revealed: true })
|
|
1579
|
+
assert.deepEqual(launched, [pair.first, pair.second])
|
|
1581
1580
|
|
|
1582
1581
|
const untracked = await vault.perform("reveal", {
|
|
1583
1582
|
path: path.join(home, "api", "first", "missing.bin")
|
|
1584
1583
|
})
|
|
1585
1584
|
assert.match(untracked.error, /no longer tracked/i)
|
|
1586
|
-
assert.deepEqual(launched, [pair.first])
|
|
1585
|
+
assert.deepEqual(launched, [pair.first, pair.second])
|
|
1587
1586
|
|
|
1588
1587
|
await close(vault)
|
|
1589
1588
|
})
|
package/test/vault-ui.test.js
CHANGED
|
@@ -160,7 +160,9 @@ const makePage = async (status, options = {}) => {
|
|
|
160
160
|
runScripts: "outside-only",
|
|
161
161
|
url: appMode
|
|
162
162
|
? "http://localhost/vault/app/app"
|
|
163
|
-
:
|
|
163
|
+
: `http://localhost/vault${options.focusGroup
|
|
164
|
+
? `?group=${options.focusGroup}`
|
|
165
|
+
: ""}`
|
|
164
166
|
}
|
|
165
167
|
)
|
|
166
168
|
const requests = []
|
|
@@ -811,8 +813,13 @@ describe("Save Space interface", () => {
|
|
|
811
813
|
if (parsed.searchParams.get("locations_path")) {
|
|
812
814
|
return {
|
|
813
815
|
path: parsed.searchParams.get("locations_path"),
|
|
814
|
-
items: [
|
|
816
|
+
items: [
|
|
817
|
+
["duplicate", "duplicate"],
|
|
818
|
+
["first", "tracked"],
|
|
819
|
+
["third", "shared"]
|
|
820
|
+
].map(([name, status]) => ({
|
|
815
821
|
path: `/pinokio/api/app/models/${name}.bin`,
|
|
822
|
+
status,
|
|
816
823
|
source_id: "app:app",
|
|
817
824
|
source_label: "app",
|
|
818
825
|
relative_path: `models/${name}.bin`
|
|
@@ -839,13 +846,18 @@ describe("Save Space interface", () => {
|
|
|
839
846
|
await waitFor(() => getRequests.some((url) =>
|
|
840
847
|
new URL(url, "http://localhost").searchParams
|
|
841
848
|
.get("locations_path") === "/pinokio/api/app/models/duplicate.bin"))
|
|
842
|
-
await waitFor(() => document.querySelector(".vault-
|
|
843
|
-
document.querySelector(".vault-detail").textContent.includes("third.bin"))
|
|
849
|
+
await waitFor(() => document.querySelector(".vault-location-path"))
|
|
844
850
|
const detail = document.querySelector(".vault-detail")
|
|
845
|
-
assert.match(detail.textContent, /
|
|
846
|
-
assert.match(detail.textContent, /first\.bin/)
|
|
851
|
+
assert.match(detail.textContent, /Stored 3 times · 4\.1 KB each/)
|
|
847
852
|
assert.doesNotMatch(detail.textContent, /sample\.bin/)
|
|
848
853
|
assert.doesNotMatch(detail.textContent, /of 3 locations shown/)
|
|
854
|
+
const notes = [...detail.querySelectorAll(".vault-location-note")]
|
|
855
|
+
.map((node) => node.textContent)
|
|
856
|
+
assert.deepEqual(notes, [
|
|
857
|
+
"this file",
|
|
858
|
+
"the copy that stays",
|
|
859
|
+
"already deduplicated"
|
|
860
|
+
])
|
|
849
861
|
await settle()
|
|
850
862
|
dom.window.close()
|
|
851
863
|
})
|
|
@@ -888,6 +900,44 @@ describe("Save Space interface", () => {
|
|
|
888
900
|
dom.window.close()
|
|
889
901
|
})
|
|
890
902
|
|
|
903
|
+
test("a scoped page hands one content group to the global page", async () => {
|
|
904
|
+
const hash = "c".repeat(64)
|
|
905
|
+
const duplicate = item({
|
|
906
|
+
path: "/pinokio/api/app/models/duplicate.bin",
|
|
907
|
+
relative_path: "models/duplicate.bin",
|
|
908
|
+
status: "duplicate",
|
|
909
|
+
shareable: true,
|
|
910
|
+
hash,
|
|
911
|
+
location_count: 3
|
|
912
|
+
})
|
|
913
|
+
const base = fixture([duplicate])
|
|
914
|
+
base.inventory.source_counts.duplicates["app:app"] = 1
|
|
915
|
+
base.inventory.shareable_by_source["app:app"] = 1
|
|
916
|
+
const response = (url) => {
|
|
917
|
+
const parsed = new URL(url, "http://localhost")
|
|
918
|
+
if (parsed.searchParams.get("group_hash")) {
|
|
919
|
+
return { hash, items: [], total: 3, next_cursor: null }
|
|
920
|
+
}
|
|
921
|
+
const result = JSON.parse(JSON.stringify(base))
|
|
922
|
+
result.inventory.view = parsed.searchParams.get("view") || "all"
|
|
923
|
+
result.items = [duplicate]
|
|
924
|
+
return result
|
|
925
|
+
}
|
|
926
|
+
const { dom, getRequests } = await makePage(response, {
|
|
927
|
+
focusGroup: hash
|
|
928
|
+
})
|
|
929
|
+
const document = dom.window.document
|
|
930
|
+
|
|
931
|
+
await waitFor(() => document.querySelector(
|
|
932
|
+
'[data-view="duplicates"].selected'))
|
|
933
|
+
assert.ok(document.querySelector('[data-display-mode="files"].selected'))
|
|
934
|
+
await waitFor(() => getRequests.some((url) =>
|
|
935
|
+
new URL(url, "http://localhost").searchParams
|
|
936
|
+
.get("group_hash") === hash))
|
|
937
|
+
await settle()
|
|
938
|
+
dom.window.close()
|
|
939
|
+
})
|
|
940
|
+
|
|
891
941
|
test("Cannot deduplicate is separate from actionable duplicates", async () => {
|
|
892
942
|
const duplicate = item({
|
|
893
943
|
path: "/pinokio/api/app/models/duplicate.bin",
|