@deepblend/dsh-blender-ui 0.3.5 → 0.3.6
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/client.js +258 -8
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -495,6 +495,22 @@ window.__ModuleLoader__.load({
|
|
|
495
495
|
'projects.creationFailed': '未能确认创建结果。输入已保留;可重试上次创建,或刷新列表检查已保存的项目。',
|
|
496
496
|
'projects.retryCreation': '重试上次创建',
|
|
497
497
|
'projects.originalTitle': '上次创建:{title}',
|
|
498
|
+
'projects.drafts': '继续创建草稿',
|
|
499
|
+
'projects.draftHint': '创建草稿保存在此浏览器。恢复只填写输入,不会自动创建项目。',
|
|
500
|
+
'projects.draftSaved': '当前创建草稿已保存到此浏览器。',
|
|
501
|
+
'projects.draftUnavailable': '此浏览器暂时无法保存草稿。当前输入仍可使用,但重载或关闭页面后可能丢失。',
|
|
502
|
+
'projects.draftFull': '创建草稿已满。删除不用的浏览器草稿后可继续保存,当前输入仍保留。',
|
|
503
|
+
'projects.draftTooLarge': '当前创建草稿过大,无法保存到浏览器。当前输入仍保留。',
|
|
504
|
+
'projects.draftInvalid': '这份浏览器草稿无法读取,原记录没有被更改。',
|
|
505
|
+
'projects.draftMissingRecipe': '原配方 {id}@{version} 当前不可用。原创建请求仍可重试;新建作品前请选择当前配方。',
|
|
506
|
+
'projects.draftOldParameters': '原配方参数',
|
|
507
|
+
'projects.draftRestored': '已恢复创建草稿。检查输入后继续;上次请求需要明确重试。',
|
|
508
|
+
'projects.draftRestore': '继续这份草稿',
|
|
509
|
+
'projects.draftDelete': '删除浏览器草稿',
|
|
510
|
+
'projects.draftClear': '清空当前创建草稿',
|
|
511
|
+
'projects.draftUncertain': '创建结果尚未确认',
|
|
512
|
+
'projects.untitledDraft': '未命名创建草稿',
|
|
513
|
+
'projects.damagedDraft': '无法读取的创建草稿',
|
|
498
514
|
'projects.refreshList': '刷新项目列表',
|
|
499
515
|
'projects.creationDetails': '详细诊断',
|
|
500
516
|
'projects.environmentGuide': '查看环境检查指南',
|
|
@@ -1044,6 +1060,22 @@ window.__ModuleLoader__.load({
|
|
|
1044
1060
|
'projects.creationFailed': 'The creation result could not be confirmed. Your inputs are kept; retry the previous creation, or refresh the list to check saved projects.',
|
|
1045
1061
|
'projects.retryCreation': 'Retry previous creation',
|
|
1046
1062
|
'projects.originalTitle': 'Previous creation: {title}',
|
|
1063
|
+
'projects.drafts': 'Continue a creation draft',
|
|
1064
|
+
'projects.draftHint': 'Creation drafts stay in this browser. Restoring fills the inputs and does not create a project automatically.',
|
|
1065
|
+
'projects.draftSaved': 'The current creation draft is saved in this browser.',
|
|
1066
|
+
'projects.draftUnavailable': 'This browser cannot save the draft right now. Current inputs still work, but reloading or closing the page may lose them.',
|
|
1067
|
+
'projects.draftFull': 'Creation draft storage is full. Delete unused browser drafts to save more; current inputs remain.',
|
|
1068
|
+
'projects.draftTooLarge': 'This creation draft is too large to save in the browser. Current inputs remain.',
|
|
1069
|
+
'projects.draftInvalid': 'This browser draft cannot be read. Its original record has not been changed.',
|
|
1070
|
+
'projects.draftMissingRecipe': 'Recipe {id}@{version} is currently unavailable. You can still retry its original creation request; choose a current recipe before starting a new project.',
|
|
1071
|
+
'projects.draftOldParameters': 'Original recipe parameters',
|
|
1072
|
+
'projects.draftRestored': 'Creation draft restored. Check the inputs before continuing; retry the previous request explicitly.',
|
|
1073
|
+
'projects.draftRestore': 'Continue this draft',
|
|
1074
|
+
'projects.draftDelete': 'Delete browser draft',
|
|
1075
|
+
'projects.draftClear': 'Clear current creation draft',
|
|
1076
|
+
'projects.draftUncertain': 'Creation result unconfirmed',
|
|
1077
|
+
'projects.untitledDraft': 'Untitled creation draft',
|
|
1078
|
+
'projects.damagedDraft': 'Unreadable creation draft',
|
|
1047
1079
|
'projects.refreshList': 'Refresh project list',
|
|
1048
1080
|
'projects.creationDetails': 'Diagnostic details',
|
|
1049
1081
|
'projects.environmentGuide': 'Open the environment check guide',
|
|
@@ -1363,6 +1395,75 @@ window.__ModuleLoader__.load({
|
|
|
1363
1395
|
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 4294967296)
|
|
1364
1396
|
return `create-${Date.now().toString(36)}-${(++creationKeySequence).toString(36)}-${[...bytes].map(n => n.toString(16).padStart(8, '0')).join('')}`
|
|
1365
1397
|
}
|
|
1398
|
+
const CREATION_DRAFT_VERSION = 'deepblend.creation-draft/v1'
|
|
1399
|
+
const CREATION_DRAFT_BYTES = 262144
|
|
1400
|
+
const creationDraftPrefix = root => `${CREATION_DRAFT_VERSION}:${encodeURIComponent(root)}:`
|
|
1401
|
+
const creationRecipeReference = recipe => recipe ? { id: recipe.id, version: recipe.version, digest: recipe.digest } : null
|
|
1402
|
+
function creationDraftJson(value) {
|
|
1403
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return
|
|
1404
|
+
if (typeof value === 'number' && Number.isFinite(value)) return
|
|
1405
|
+
if (!value || typeof value !== 'object') throw new Error('Draft input is not JSON')
|
|
1406
|
+
for (const item of Object.values(value)) creationDraftJson(item)
|
|
1407
|
+
}
|
|
1408
|
+
function creationDraftForms(forms) {
|
|
1409
|
+
const snapshot = { title: forms.title, goal: forms.goal,
|
|
1410
|
+
recipe: creationRecipeReference(forms.recipe), recipeParameters: forms.recipeParameters }
|
|
1411
|
+
creationDraftJson(snapshot)
|
|
1412
|
+
return editorClone(snapshot)
|
|
1413
|
+
}
|
|
1414
|
+
const creationRequestBody = forms => editorClone({ title: forms.title.trim(),
|
|
1415
|
+
goal: forms.goal.length > 0 ? forms.goal : undefined, renderPreview: Boolean(forms.recipe),
|
|
1416
|
+
recipe: forms.recipe ? { ...creationRecipeReference(forms.recipe), parameters: forms.recipeParameters } : undefined })
|
|
1417
|
+
function validCreationRecipeReference(recipe) {
|
|
1418
|
+
return recipe === null || Boolean(recipe && typeof recipe === 'object' && !Array.isArray(recipe)
|
|
1419
|
+
&& typeof recipe.id === 'string' && recipe.id && typeof recipe.version === 'string' && recipe.version
|
|
1420
|
+
&& /^[a-f0-9]{64}$/.test(recipe.digest || ''))
|
|
1421
|
+
}
|
|
1422
|
+
function parseCreationDraft(raw, root, id) {
|
|
1423
|
+
try {
|
|
1424
|
+
if (typeof raw !== 'string' || new Blob([raw]).size > CREATION_DRAFT_BYTES) return null
|
|
1425
|
+
const draft = JSON.parse(raw), forms = draft.forms
|
|
1426
|
+
creationDraftJson(forms)
|
|
1427
|
+
if (draft.schemaVersion !== CREATION_DRAFT_VERSION || draft.projectsRoot !== root || draft.id !== id
|
|
1428
|
+
|| (draft.ownerActive !== undefined && typeof draft.ownerActive !== 'boolean')
|
|
1429
|
+
|| !Number.isFinite(Date.parse(draft.savedAt)) || !forms || typeof forms.title !== 'string'
|
|
1430
|
+
|| typeof forms.goal !== 'string' || !validCreationRecipeReference(forms.recipe)
|
|
1431
|
+
|| Object.keys(forms).some(key => !['title', 'goal', 'recipe', 'recipeParameters'].includes(key))
|
|
1432
|
+
|| !forms.recipeParameters || typeof forms.recipeParameters !== 'object' || Array.isArray(forms.recipeParameters)) return null
|
|
1433
|
+
const attempt = draft.attempt
|
|
1434
|
+
if (attempt !== null) {
|
|
1435
|
+
const body = attempt?.body
|
|
1436
|
+
creationDraftJson(body)
|
|
1437
|
+
if (!attempt || typeof attempt.key !== 'string' || !attempt.key.trim() || attempt.key.length > 128 || /[\x00-\x1f\x7f]/.test(attempt.key)
|
|
1438
|
+
|| !body || typeof body.title !== 'string' || !body.title.trim() || typeof body.renderPreview !== 'boolean'
|
|
1439
|
+
|| (body.goal !== undefined && typeof body.goal !== 'string')
|
|
1440
|
+
|| (body.recipe !== undefined && (!body.recipe || !validCreationRecipeReference(body.recipe)
|
|
1441
|
+
|| !body.recipe.parameters || typeof body.recipe.parameters !== 'object' || Array.isArray(body.recipe.parameters)))
|
|
1442
|
+
|| body.renderPreview !== Boolean(body.recipe)
|
|
1443
|
+
|| Object.keys(body).some(key => !['title', 'goal', 'recipe', 'renderPreview'].includes(key))
|
|
1444
|
+
|| attempt.signature !== creationSignature(body)) return null
|
|
1445
|
+
}
|
|
1446
|
+
return draft
|
|
1447
|
+
} catch { return null }
|
|
1448
|
+
}
|
|
1449
|
+
function creationDraftStorage(settings) {
|
|
1450
|
+
try { return settings.creationDraftStorage ?? globalThis.localStorage } catch { return undefined }
|
|
1451
|
+
}
|
|
1452
|
+
function readCreationDrafts(storage, root) {
|
|
1453
|
+
if (!root) return { entries: [], available: false }
|
|
1454
|
+
try {
|
|
1455
|
+
if (!storage || typeof storage.key !== 'function' || !Number.isSafeInteger(storage.length)) return { entries: [], available: false }
|
|
1456
|
+
const prefix = creationDraftPrefix(root), entries = []
|
|
1457
|
+
for (let index = 0; index < storage.length; index++) {
|
|
1458
|
+
const key = storage.key(index)
|
|
1459
|
+
if (typeof key !== 'string' || !key.startsWith(prefix)) continue
|
|
1460
|
+
const id = key.slice(prefix.length), raw = storage.getItem(key), draft = parseCreationDraft(raw, root, id)
|
|
1461
|
+
entries.push({ id, raw, draft })
|
|
1462
|
+
}
|
|
1463
|
+
entries.sort((a, b) => String(b.draft?.savedAt || '').localeCompare(String(a.draft?.savedAt || '')))
|
|
1464
|
+
return { entries, available: true }
|
|
1465
|
+
} catch { return { entries: [], available: false } }
|
|
1466
|
+
}
|
|
1366
1467
|
function creationErrorMessage(code) {
|
|
1367
1468
|
if (code === 'UI_FETCH_FAILED' || /^HTTP_/.test(code)) return t('projects.replyMissing')
|
|
1368
1469
|
if (code === 'BLENDER_NOT_FOUND' || code === 'RUNTIME_UNAVAILABLE') return t('projects.environmentFailed')
|
|
@@ -2480,6 +2581,9 @@ window.__ModuleLoader__.load({
|
|
|
2480
2581
|
imageDownloads: {},
|
|
2481
2582
|
creationWork: null,
|
|
2482
2583
|
creationRequestProtocol: null,
|
|
2584
|
+
creationDrafts: [],
|
|
2585
|
+
creationDraftStorageStatus: null,
|
|
2586
|
+
creationDraftMissingRecipe: null,
|
|
2483
2587
|
inspectionForms: {},
|
|
2484
2588
|
inspectionWork: {},
|
|
2485
2589
|
inspectionRevisions: {},
|
|
@@ -2541,6 +2645,11 @@ window.__ModuleLoader__.load({
|
|
|
2541
2645
|
let live = false
|
|
2542
2646
|
let loadSequence = 0
|
|
2543
2647
|
let creationAttempt = null
|
|
2648
|
+
const draftStorage = creationDraftStorage(settings)
|
|
2649
|
+
let creationDraftId = newCreationKey(), creationDraftRoot = null, persistedCreationSignature = null
|
|
2650
|
+
let restoredCreationSource = null, creationDraftWriting = false
|
|
2651
|
+
let creationDraftClosed = false
|
|
2652
|
+
const draftLifecycle = settings.creationDraftLifecycle ?? globalThis.window
|
|
2544
2653
|
let fileChoiceSignature = null, fileChoicesStopped = false
|
|
2545
2654
|
const photographyControllers = new Map()
|
|
2546
2655
|
const photographyWork = (projectId, changes) => set({ photographyWork: { ...data.photographyWork, [projectId]: { ...data.photographyWork[projectId], ...changes } } })
|
|
@@ -2584,6 +2693,7 @@ window.__ModuleLoader__.load({
|
|
|
2584
2693
|
const assetWork = (projectId, changes) => set({ assetWork: { ...data.assetWork, [projectId]: { ...data.assetWork[projectId], ...changes } } })
|
|
2585
2694
|
|
|
2586
2695
|
const notify = () => {
|
|
2696
|
+
persistCreationDraft()
|
|
2587
2697
|
const signature = JSON.stringify([data.activeProjectId, data.selected?.scene?.revision, data.view, ...FILE_FIELDS.map(field => fileChoiceAllowed(data, field))])
|
|
2588
2698
|
if (signature !== fileChoiceSignature) { data.fileChoiceGeneration += 1; fileChoiceSignature = signature }
|
|
2589
2699
|
snapshot = { ...data, forms: { ...data.forms }, busy: { ...data.busy }, notices: { ...data.notices } }
|
|
@@ -2594,6 +2704,56 @@ window.__ModuleLoader__.load({
|
|
|
2594
2704
|
/** Change one nested table. @param {'forms'|'busy'|'notices'} table @param {string} key @param {any} value */
|
|
2595
2705
|
const setIn = (table, key, value) => { data = { ...data, [table]: { ...data[table], [key]: value } }; notify() }
|
|
2596
2706
|
|
|
2707
|
+
function refreshCreationDrafts() {
|
|
2708
|
+
const reading = readCreationDrafts(draftStorage, creationDraftRoot)
|
|
2709
|
+
data.creationDrafts = reading.entries.filter(entry => entry.id !== creationDraftId).map(entry => ({ id: entry.id,
|
|
2710
|
+
title: entry.draft?.forms.title || t('projects.untitledDraft'), savedAt: entry.draft?.savedAt || null,
|
|
2711
|
+
uncertain: Boolean(entry.draft?.attempt), invalid: !entry.draft }))
|
|
2712
|
+
return reading
|
|
2713
|
+
}
|
|
2714
|
+
function persistCreationDraft() {
|
|
2715
|
+
if (creationDraftClosed || creationDraftWriting || !creationDraftRoot || data.projectsRoot !== creationDraftRoot) return
|
|
2716
|
+
creationDraftWriting = true
|
|
2717
|
+
try {
|
|
2718
|
+
const forms = { ...creationDraftForms(data.forms), recipe: creationRecipeReference(data.forms.recipe) || data.creationDraftMissingRecipe }, signature = JSON.stringify([forms, creationAttempt])
|
|
2719
|
+
if (signature === persistedCreationSignature) return
|
|
2720
|
+
const reading = refreshCreationDrafts()
|
|
2721
|
+
if (!reading.available) { data.creationDraftStorageStatus = 'unavailable'; return }
|
|
2722
|
+
const key = creationDraftPrefix(creationDraftRoot) + creationDraftId
|
|
2723
|
+
if (!forms.title && !forms.goal && !forms.recipe && !Object.keys(forms.recipeParameters).length && !creationAttempt) {
|
|
2724
|
+
draftStorage.removeItem(key); data.creationDraftStorageStatus = null
|
|
2725
|
+
} else {
|
|
2726
|
+
if (!reading.entries.some(entry => entry.id === creationDraftId) && reading.entries.length >= 20) {
|
|
2727
|
+
data.creationDraftStorageStatus = 'full'; return
|
|
2728
|
+
}
|
|
2729
|
+
const raw = JSON.stringify({ schemaVersion: CREATION_DRAFT_VERSION, projectsRoot: creationDraftRoot,
|
|
2730
|
+
id: creationDraftId, ownerActive: true, savedAt: new Date().toISOString(), forms, attempt: creationAttempt })
|
|
2731
|
+
if (new Blob([raw]).size > CREATION_DRAFT_BYTES) { data.creationDraftStorageStatus = 'tooLarge'; return }
|
|
2732
|
+
draftStorage.setItem(key, raw)
|
|
2733
|
+
if (draftStorage.getItem(key) !== raw) throw new Error('Draft write did not persist')
|
|
2734
|
+
data.creationDraftStorageStatus = 'saved'
|
|
2735
|
+
}
|
|
2736
|
+
persistedCreationSignature = signature
|
|
2737
|
+
refreshCreationDrafts()
|
|
2738
|
+
} catch { data.creationDraftStorageStatus = 'unavailable' }
|
|
2739
|
+
finally { creationDraftWriting = false }
|
|
2740
|
+
}
|
|
2741
|
+
function closeCreationDraft() {
|
|
2742
|
+
if (creationDraftClosed) return
|
|
2743
|
+
persistCreationDraft()
|
|
2744
|
+
creationDraftClosed = true
|
|
2745
|
+
if (!creationDraftRoot) return
|
|
2746
|
+
try {
|
|
2747
|
+
const key = creationDraftPrefix(creationDraftRoot) + creationDraftId
|
|
2748
|
+
const saved = parseCreationDraft(draftStorage?.getItem(key), creationDraftRoot, creationDraftId)
|
|
2749
|
+
if (saved) draftStorage.setItem(key, JSON.stringify({ ...saved, ownerActive: false }))
|
|
2750
|
+
} catch { /* Current inputs were already saved or visibly reported as unsaved. */ }
|
|
2751
|
+
}
|
|
2752
|
+
function reopenCreationDraft() {
|
|
2753
|
+
creationDraftClosed = false; persistedCreationSignature = null
|
|
2754
|
+
persistCreationDraft()
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2597
2757
|
/** The project every per-project route is addressed to. */
|
|
2598
2758
|
const target = () => data.projectId ?? data.projects.find(project => !project.creationPending)?.projectId ?? null
|
|
2599
2759
|
|
|
@@ -2668,6 +2828,17 @@ window.__ModuleLoader__.load({
|
|
|
2668
2828
|
const next = { ...comparison, afterPreviews: editorClone(afterPreviews) }
|
|
2669
2829
|
patch.editorComparison = { ...next, ...editorPreviewPair(next), resolved: afterPreviews.length > 0 }
|
|
2670
2830
|
}
|
|
2831
|
+
if (patch.projectsRoot !== creationDraftRoot) {
|
|
2832
|
+
// Switching stores cannot carry an old store's request identity into a new one.
|
|
2833
|
+
if (creationDraftRoot !== null) {
|
|
2834
|
+
creationAttempt = null; restoredCreationSource = null; creationDraftId = newCreationKey()
|
|
2835
|
+
patch.creationWork = null; patch.creationDraftMissingRecipe = null
|
|
2836
|
+
patch.forms = { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }
|
|
2837
|
+
}
|
|
2838
|
+
creationDraftRoot = typeof patch.projectsRoot === 'string' && patch.projectsRoot ? patch.projectsRoot : null
|
|
2839
|
+
persistedCreationSignature = null
|
|
2840
|
+
}
|
|
2841
|
+
refreshCreationDrafts()
|
|
2671
2842
|
set(patch)
|
|
2672
2843
|
arm()
|
|
2673
2844
|
}
|
|
@@ -2684,12 +2855,7 @@ window.__ModuleLoader__.load({
|
|
|
2684
2855
|
/** A write just happened: re-run every read, including the view-specific ones. */
|
|
2685
2856
|
const reload = () => { tick += 1; void load() }
|
|
2686
2857
|
|
|
2687
|
-
const creationBody = () =>
|
|
2688
|
-
goal: data.forms.goal.length > 0 ? data.forms.goal : undefined,
|
|
2689
|
-
renderPreview: Boolean(data.forms.recipe), recipe: data.forms.recipe ? {
|
|
2690
|
-
id: data.forms.recipe.id, version: data.forms.recipe.version, digest: data.forms.recipe.digest,
|
|
2691
|
-
parameters: data.forms.recipeParameters,
|
|
2692
|
-
} : undefined })
|
|
2858
|
+
const creationBody = () => creationRequestBody(data.forms)
|
|
2693
2859
|
|
|
2694
2860
|
async function submitCreation(attempt) {
|
|
2695
2861
|
if (data.creationRequestProtocol !== CREATION_REQUEST_PROTOCOL) {
|
|
@@ -2698,17 +2864,29 @@ window.__ModuleLoader__.load({
|
|
|
2698
2864
|
return
|
|
2699
2865
|
}
|
|
2700
2866
|
setIn('busy', 'create', true)
|
|
2867
|
+
const attemptRoot = creationDraftRoot
|
|
2701
2868
|
set({ creationWork: { status: 'busy', title: attempt.body.title }, previewResult: null })
|
|
2702
2869
|
setIn('notices', 'projects', null)
|
|
2703
2870
|
setIn('notices', 'preview', null)
|
|
2704
2871
|
const outcome = await postJson(fetchImpl, ROUTES.projects, { ...editorClone(attempt.body), creationKey: attempt.key })
|
|
2705
2872
|
setIn('busy', 'create', false)
|
|
2873
|
+
if (attemptRoot !== creationDraftRoot) { reload(); return }
|
|
2706
2874
|
if (outcome.ok) {
|
|
2707
2875
|
const sameInputs = creationSignature(creationBody()) === attempt.signature
|
|
2708
2876
|
set({ ...(sameInputs ? { forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} } } : {}),
|
|
2709
2877
|
creationWork: null, projectId: outcome.payload.project.projectId,
|
|
2710
2878
|
...(attempt.body.renderPreview ? { view: 'preview', compareMode: 'result' } : {}) })
|
|
2711
2879
|
if (creationAttempt === attempt) creationAttempt = null
|
|
2880
|
+
if (restoredCreationSource && creationDraftRoot) {
|
|
2881
|
+
try {
|
|
2882
|
+
const sourceKey = creationDraftPrefix(creationDraftRoot) + restoredCreationSource.id
|
|
2883
|
+
const source = parseCreationDraft(restoredCreationSource.raw, creationDraftRoot, restoredCreationSource.id)
|
|
2884
|
+
if (source && creationSignature(creationRequestBody(source.forms)) === attempt.signature
|
|
2885
|
+
&& draftStorage?.getItem(sourceKey) === restoredCreationSource.raw) draftStorage.removeItem(sourceKey)
|
|
2886
|
+
} catch { data.creationDraftStorageStatus = 'unavailable' }
|
|
2887
|
+
}
|
|
2888
|
+
restoredCreationSource = null
|
|
2889
|
+
persistedCreationSignature = null
|
|
2712
2890
|
setIn('notices', attempt.body.renderPreview ? 'preview' : 'projects', { kind: 'creation', projectId: outcome.payload.project.projectId, ok: true, message: outcome.payload.project.creationReplayed
|
|
2713
2891
|
? t('projects.recovered', { id: outcome.payload.project.projectId })
|
|
2714
2892
|
: t('projects.created', { id: outcome.payload.project.projectId }) })
|
|
@@ -3019,7 +3197,7 @@ window.__ModuleLoader__.load({
|
|
|
3019
3197
|
} else assetWork(projectId, { error: `${outcome.error.code}: ${outcome.error.message}` })
|
|
3020
3198
|
reload()
|
|
3021
3199
|
},
|
|
3022
|
-
selectRecipe: recipe => set({ forms: { ...data.forms, recipe,
|
|
3200
|
+
selectRecipe: recipe => set({ creationDraftMissingRecipe: null, forms: { ...data.forms, recipe,
|
|
3023
3201
|
recipeParameters: recipe ? Object.fromEntries(recipe.parameters.map(parameter => [parameter.id, parameter.default])) : {},
|
|
3024
3202
|
title: !data.forms.title || data.forms.title === data.forms.recipe?.title ? recipe?.title || '' : data.forms.title,
|
|
3025
3203
|
} }),
|
|
@@ -3366,6 +3544,7 @@ window.__ModuleLoader__.load({
|
|
|
3366
3544
|
|
|
3367
3545
|
createProject: async () => {
|
|
3368
3546
|
if (data.busy.create) return
|
|
3547
|
+
if (data.creationDraftMissingRecipe) return
|
|
3369
3548
|
if (!recipeSelectionCurrent(data) || !recipeValuesValid(data.forms.recipe, data.forms.recipeParameters)) return
|
|
3370
3549
|
const body = creationBody()
|
|
3371
3550
|
const signature = creationSignature(body)
|
|
@@ -3373,6 +3552,41 @@ window.__ModuleLoader__.load({
|
|
|
3373
3552
|
return submitCreation(creationAttempt)
|
|
3374
3553
|
},
|
|
3375
3554
|
retryCreation: () => data.busy.create || !creationAttempt ? undefined : submitCreation(creationAttempt),
|
|
3555
|
+
restoreCreationDraft: id => {
|
|
3556
|
+
if (data.busy.create || !creationDraftRoot) return
|
|
3557
|
+
const reading = readCreationDrafts(draftStorage, creationDraftRoot), source = reading.entries.find(entry => entry.id === id)
|
|
3558
|
+
if (!source?.draft) { set({ creationDraftStorageStatus: 'invalid' }); return }
|
|
3559
|
+
const saved = source.draft, ref = saved.forms.recipe
|
|
3560
|
+
const recipe = ref ? data.recipeCatalog.recipes.find(recipe => recipe.id === ref.id && recipe.version === ref.version && recipe.digest === ref.digest) : null
|
|
3561
|
+
creationDraftId = newCreationKey(); restoredCreationSource = { id, raw: source.raw }; persistedCreationSignature = null
|
|
3562
|
+
creationAttempt = saved.attempt ? editorClone(saved.attempt) : null
|
|
3563
|
+
set({ view: 'projects', forms: { ...data.forms, ...editorClone(saved.forms), recipe: recipe || null },
|
|
3564
|
+
creationDraftMissingRecipe: ref && !recipe ? ref : null,
|
|
3565
|
+
creationWork: creationAttempt ? { status: 'error', title: creationAttempt.body.title, code: 'UI_FETCH_FAILED' } : null,
|
|
3566
|
+
notices: { ...data.notices, projects: { kind: 'creation', ok: true, message: t('projects.draftRestored') } } })
|
|
3567
|
+
// A closed page's unchanged record can be consumed after the copy
|
|
3568
|
+
// persisted. Live pages retain their independent saved record.
|
|
3569
|
+
if (saved.ownerActive === false && data.creationDraftStorageStatus === 'saved') {
|
|
3570
|
+
try {
|
|
3571
|
+
const sourceKey = creationDraftPrefix(creationDraftRoot) + id
|
|
3572
|
+
if (draftStorage.getItem(sourceKey) === source.raw) draftStorage.removeItem(sourceKey)
|
|
3573
|
+
refreshCreationDrafts(); notify()
|
|
3574
|
+
} catch { set({ creationDraftStorageStatus: 'unavailable' }) }
|
|
3575
|
+
}
|
|
3576
|
+
},
|
|
3577
|
+
deleteCreationDraft: id => {
|
|
3578
|
+
if (!creationDraftRoot || id === creationDraftId) return
|
|
3579
|
+
const entry = readCreationDrafts(draftStorage, creationDraftRoot).entries.find(entry => entry.id === id)
|
|
3580
|
+
if (!entry) return
|
|
3581
|
+
try { draftStorage.removeItem(creationDraftPrefix(creationDraftRoot) + id); refreshCreationDrafts(); notify() }
|
|
3582
|
+
catch { set({ creationDraftStorageStatus: 'unavailable' }) }
|
|
3583
|
+
},
|
|
3584
|
+
clearCreationDraft: () => {
|
|
3585
|
+
if (data.busy.create) return
|
|
3586
|
+
creationAttempt = null; restoredCreationSource = null; persistedCreationSignature = null
|
|
3587
|
+
set({ forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }, creationWork: null,
|
|
3588
|
+
creationDraftMissingRecipe: null, notices: { ...data.notices, projects: null } })
|
|
3589
|
+
},
|
|
3376
3590
|
|
|
3377
3591
|
applyPatch: async () => {
|
|
3378
3592
|
const projectId = data.activeProjectId
|
|
@@ -3516,10 +3730,16 @@ window.__ModuleLoader__.load({
|
|
|
3516
3730
|
start() {
|
|
3517
3731
|
if (running) return
|
|
3518
3732
|
running = true
|
|
3733
|
+
reopenCreationDraft()
|
|
3734
|
+
draftLifecycle?.addEventListener?.('pagehide', closeCreationDraft)
|
|
3735
|
+
draftLifecycle?.addEventListener?.('pageshow', reopenCreationDraft)
|
|
3519
3736
|
fileChoicesStopped = false
|
|
3520
3737
|
void load()
|
|
3521
3738
|
},
|
|
3522
3739
|
stop() {
|
|
3740
|
+
closeCreationDraft()
|
|
3741
|
+
draftLifecycle?.removeEventListener?.('pagehide', closeCreationDraft)
|
|
3742
|
+
draftLifecycle?.removeEventListener?.('pageshow', reopenCreationDraft)
|
|
3523
3743
|
fileChoicesStopped = true
|
|
3524
3744
|
data = { ...data, fileChoiceGeneration: data.fileChoiceGeneration + 1 }
|
|
3525
3745
|
running = false
|
|
@@ -3549,6 +3769,35 @@ window.__ModuleLoader__.load({
|
|
|
3549
3769
|
// touches React, the DOM, or the network.
|
|
3550
3770
|
|
|
3551
3771
|
/** 项目: the list, the create form, and the selected project's summary. */
|
|
3772
|
+
function CreationDraftsView({ state, actions }) {
|
|
3773
|
+
const messages = { saved: t('projects.draftSaved'), unavailable: t('projects.draftUnavailable'), full: t('projects.draftFull'),
|
|
3774
|
+
tooLarge: t('projects.draftTooLarge'), invalid: t('projects.draftInvalid') }
|
|
3775
|
+
const hasInputs = Boolean(state.forms.title || state.forms.goal || state.forms.recipe || state.creationWork || state.creationDraftMissingRecipe || Object.keys(state.forms.recipeParameters).length)
|
|
3776
|
+
return el('div', { 'data-creation-drafts': true },
|
|
3777
|
+
el('p', { className: 'db-muted' }, t('projects.draftHint')),
|
|
3778
|
+
(hasInputs || state.creationDraftStorageStatus !== 'saved') && messages[state.creationDraftStorageStatus] ? el('p', {
|
|
3779
|
+
className: state.creationDraftStorageStatus === 'saved' ? 'db-muted' : 'db-error',
|
|
3780
|
+
'data-creation-draft-status': state.creationDraftStorageStatus,
|
|
3781
|
+
}, messages[state.creationDraftStorageStatus]) : null,
|
|
3782
|
+
state.creationDraftMissingRecipe ? el('div', { 'data-creation-draft-recipe-missing': true },
|
|
3783
|
+
el('p', { className: 'db-error' }, t('projects.draftMissingRecipe', state.creationDraftMissingRecipe)),
|
|
3784
|
+
el('details', null, el('summary', null, t('projects.draftOldParameters')),
|
|
3785
|
+
el('pre', { className: 'db-pre' }, JSON.stringify(state.forms.recipeParameters, null, 2)))) : null,
|
|
3786
|
+
hasInputs ? Button({ action: 'clear-creation-draft', disabled: state.busy.create, onClick: actions.clearCreationDraft, children: t('projects.draftClear') }) : null,
|
|
3787
|
+
state.creationDrafts.length ? el('div', { style: { marginTop: '8px' } },
|
|
3788
|
+
el('h5', null, t('projects.drafts')),
|
|
3789
|
+
...state.creationDrafts.map(draft => el('div', { className: 'db-card', 'data-creation-draft': draft.id },
|
|
3790
|
+
el('strong', null, draft.invalid ? t('projects.damagedDraft') : draft.title),
|
|
3791
|
+
draft.savedAt ? el('span', { className: 'db-muted', style: { marginLeft: '8px' } }, formatTime(draft.savedAt)) : null,
|
|
3792
|
+
draft.uncertain ? el('p', { className: 'db-muted' }, t('projects.draftUncertain')) : null,
|
|
3793
|
+
el('div', { className: 'db-inline' },
|
|
3794
|
+
Button({ action: `restore-creation-draft:${draft.id}`, disabled: state.busy.create || draft.invalid,
|
|
3795
|
+
onClick: () => actions.restoreCreationDraft(draft.id), children: t('projects.draftRestore') }),
|
|
3796
|
+
Button({ action: `delete-creation-draft:${draft.id}`, onClick: () => actions.deleteCreationDraft(draft.id), children: t('projects.draftDelete') })),
|
|
3797
|
+
))) : null,
|
|
3798
|
+
)
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3552
3801
|
function ProjectsView(ctx) {
|
|
3553
3802
|
const state = ctx.state
|
|
3554
3803
|
const actions = ctx.actions
|
|
@@ -3579,6 +3828,7 @@ window.__ModuleLoader__.load({
|
|
|
3579
3828
|
|
|
3580
3829
|
el('div', { className: 'db-card db-project-create' },
|
|
3581
3830
|
el('h4', null, t('projects.create')),
|
|
3831
|
+
CreationDraftsView(ctx),
|
|
3582
3832
|
!state.projects.length ? el('p', { className: 'db-muted' }, t('projects.startHint')) : null,
|
|
3583
3833
|
RecipesView(ctx),
|
|
3584
3834
|
el('div', { className: 'db-create-footer' },
|
|
@@ -3602,7 +3852,7 @@ window.__ModuleLoader__.load({
|
|
|
3602
3852
|
Button({
|
|
3603
3853
|
tone: 'primary',
|
|
3604
3854
|
action: 'create-project',
|
|
3605
|
-
disabled: state.busy.create || state.creationRequestProtocol !== CREATION_REQUEST_PROTOCOL || state.forms.title.trim().length === 0 || !recipeSelectionCurrent(state) || !recipeValuesValid(state.forms.recipe, state.forms.recipeParameters),
|
|
3855
|
+
disabled: state.busy.create || Boolean(state.creationDraftMissingRecipe) || state.creationRequestProtocol !== CREATION_REQUEST_PROTOCOL || state.forms.title.trim().length === 0 || !recipeSelectionCurrent(state) || !recipeValuesValid(state.forms.recipe, state.forms.recipeParameters),
|
|
3606
3856
|
onClick: actions.createProject,
|
|
3607
3857
|
children: state.busy.create ? (state.forms.recipe ? t('recipes.creatingPreview') : t('projects.creating')) : (state.forms.recipe ? t('recipes.createPreview') : t('projects.createButton')),
|
|
3608
3858
|
}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepBlend workbench UI: Host half (typed HTTP API) and Web Client half (sidebar, panels, tool cards).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@deepblend/dsh-blender-contracts": "0.3.
|
|
22
|
+
"@deepblend/dsh-blender-contracts": "0.3.6"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|