@deepblend/dsh-blender-ui 0.3.7 → 0.3.9

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.
Files changed (2) hide show
  1. package/lib/client.js +114 -20
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -542,6 +542,9 @@ window.__ModuleLoader__.load({
542
542
  'revisions.currentSuffix': ' (当前)',
543
543
  'revisions.missing': '没有这个 revision',
544
544
  'preview.noneForRevision': '这个 revision 还没有预览图(渲染一次预览即可)。',
545
+ 'preview.reading': '正在读取所选项目的预览…',
546
+ 'preview.readFailed': '未能读取这个项目的预览。刷新后重试;保存版本和未提交修改仍保留。',
547
+ 'preview.sourceMismatch': '预览回执与所选项目不一致。刷新后重试。',
545
548
  'preview.noPrevious': '还没有上一次渲染:再点一次「渲染预览」,或者先在某个更早的 revision 上渲一次。',
546
549
  'preview.notRenderedHere': '这个 revision 还没有由面板渲过预览(上方的「渲染预览」会生成第一张)。',
547
550
  'preview.rendering': '渲染中…',
@@ -603,7 +606,9 @@ window.__ModuleLoader__.load({
603
606
  'visual.noSecondOpinion': '没有第二意见。',
604
607
  'visual.none': '这个 revision 还没有视觉评审。可以用 blender_visual_review 跑一次。',
605
608
  'revisions.current': '当前',
606
- 'revisions.compare': '对比',
609
+ 'revisions.compare': '与当前版对比',
610
+ 'revisions.compareCurrent': '与另一版对比',
611
+ 'revisions.compareHint': '选择已保存版本与当前版对比;选择当前版会对比相邻的另一版。需要至少两版,查看不会改动作品。',
607
612
  'revisions.restore': '恢复',
608
613
  'host.reading': '读取 Host 状态…',
609
614
  'diagnostics.exportHint': '导出一份可以附在问题里的诊断信息(版本、配置、项目与任务的摘要)',
@@ -1107,6 +1112,9 @@ window.__ModuleLoader__.load({
1107
1112
  'revisions.currentSuffix': ' (current)',
1108
1113
  'revisions.missing': 'no such revision',
1109
1114
  'preview.noneForRevision': 'This revision has no preview image yet (render a preview and it will).',
1115
+ 'preview.reading': 'Reading previews for the selected project…',
1116
+ 'preview.readFailed': 'Could not read this project’s previews. Refresh to retry; saved versions and unsaved edits are kept.',
1117
+ 'preview.sourceMismatch': 'The preview response does not match the selected project. Refresh to retry.',
1110
1118
  'preview.noPrevious': 'No previous render yet: press “Render preview” again, or render once on an earlier revision.',
1111
1119
  'preview.notRenderedHere': 'This revision has no preview rendered from this panel yet (the “Render preview” button above makes the first one).',
1112
1120
  'preview.rendering': 'Rendering…',
@@ -1168,7 +1176,9 @@ window.__ModuleLoader__.load({
1168
1176
  'visual.noSecondOpinion': 'No second opinion.',
1169
1177
  'visual.none': 'This revision has no visual review yet. Run one with blender_visual_review.',
1170
1178
  'revisions.current': 'current',
1171
- 'revisions.compare': 'compare',
1179
+ 'revisions.compare': 'Compare with current',
1180
+ 'revisions.compareCurrent': 'Compare with another version',
1181
+ 'revisions.compareHint': 'Compare a saved version with the current one. The current version uses its nearest other version. At least two versions are needed; viewing does not change the project.',
1172
1182
  'revisions.restore': 'restore',
1173
1183
  'host.reading': 'Reading Host state…',
1174
1184
  'diagnostics.exportHint': 'Export diagnostics you can attach to a bug report (versions, config, a summary of projects and jobs)',
@@ -2518,7 +2528,7 @@ window.__ModuleLoader__.load({
2518
2528
  if (typeof payload?.hostApiVersion === 'number' && payload.hostApiVersion < EXPECTED_HOST_API) {
2519
2529
  return { status: 'stale', payload: null, error: staleHostError(expectedRoute, { payload, status: response.status }) }
2520
2530
  }
2521
- if (payload && payload.ok) return { status: 'ok', payload, error: null }
2531
+ if (payload && payload.ok && response.ok !== false) return { status: 'ok', payload, error: null }
2522
2532
  return {
2523
2533
  status: 'error',
2524
2534
  payload: null,
@@ -2598,6 +2608,8 @@ window.__ModuleLoader__.load({
2598
2608
  unfinishedJobs: [],
2599
2609
  previews: null,
2600
2610
  artifactBase: '',
2611
+ previewContext: null,
2612
+ previewReadStatus: 'idle',
2601
2613
  view: 'projects',
2602
2614
  projectId: null,
2603
2615
  activeProjectId: null,
@@ -2657,6 +2669,22 @@ window.__ModuleLoader__.load({
2657
2669
  try { const value = storage?.getItem(APPEARANCE_KEY); return APPEARANCES.includes(value) ? value : 'system' } catch { return 'system' }
2658
2670
  }
2659
2671
 
2672
+ function previewRevisionPair(state) {
2673
+ const revisions = state.previews?.revisions || []
2674
+ const pick = wanted => revisions.some(entry => entry.revision === wanted) ? wanted : revisions.at(-1)?.revision || null
2675
+ return { left: pick(state.compareLeft || state.currentRevision), right: pick(state.compareRight || state.currentRevision) }
2676
+ }
2677
+ function revisionComparisonTarget(state, revision) {
2678
+ if (!state.activeProjectId) return null
2679
+ const saved = new Set((state.selected?.revisions || []).map(entry => entry.revision))
2680
+ const available = [...new Set((state.previews?.revisions || []).map(entry => entry.revision).filter(id => saved.has(id)))]
2681
+ .sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)))
2682
+ if (!available.includes(revision) || !available.includes(state.currentRevision)) return null
2683
+ const index = available.indexOf(revision)
2684
+ const left = revision === state.currentRevision ? available[index - 1] || available[index + 1] : revision
2685
+ return left ? { left, right: state.currentRevision } : null
2686
+ }
2687
+
2660
2688
  function createWorkbenchStore(options) {
2661
2689
  const settings = options || {}
2662
2690
  const fetchImpl = settings.fetch ?? ((...args) => fetch(...args))
@@ -2674,6 +2702,7 @@ window.__ModuleLoader__.load({
2674
2702
  let tick = 0
2675
2703
  let live = false
2676
2704
  let loadSequence = 0
2705
+ let diffSequence = 0
2677
2706
  let creationAttempt = null
2678
2707
  const draftStorage = creationDraftStorage(settings)
2679
2708
  let creationDraftId = newCreationKey(), creationDraftRoot = null, persistedCreationSignature = null
@@ -2732,7 +2761,27 @@ window.__ModuleLoader__.load({
2732
2761
  for (const listener of [...listeners]) listener(snapshot)
2733
2762
  }
2734
2763
  /** Change the snapshot and tell everyone. @param {object} patch */
2735
- const set = (patch) => { data = { ...data, ...patch }; notify() }
2764
+ const set = (patch) => {
2765
+ // Automatic edit images are source snapshots too. They cannot override
2766
+ // fresh rows from another store, even when its project/revision IDs match.
2767
+ if (Object.hasOwn(patch, 'projectsRoot') && patch.projectsRoot !== data.projectsRoot) {
2768
+ patch = { ...patch, editorComparison: null }
2769
+ }
2770
+ // Retire the old source before selection/creation navigation can expose
2771
+ // it with a new project. A load supplies the complete new source atomically.
2772
+ if (!Object.hasOwn(patch, 'previews') && ['projectId', 'projectsRoot', 'activeProjectId']
2773
+ .some(key => Object.hasOwn(patch, key) && patch[key] !== data[key])) {
2774
+ const requested = Object.hasOwn(patch, 'projectId') ? patch.projectId : data.projectId
2775
+ const active = Object.hasOwn(patch, 'activeProjectId') ? patch.activeProjectId : data.activeProjectId
2776
+ patch = { ...patch, previews: null, artifactBase: '', previewContext: null, previewReadStatus: requested || active ? 'loading' : 'idle' }
2777
+ }
2778
+ if (['projectsRoot', 'activeProjectId', 'currentRevision', 'view', 'compareMode', 'compareLeft', 'compareRight']
2779
+ .some(key => Object.hasOwn(patch, key) && patch[key] !== data[key])) {
2780
+ diffSequence += 1
2781
+ patch = { ...patch, diff: null, diffError: null }
2782
+ }
2783
+ data = { ...data, ...patch }; notify()
2784
+ }
2736
2785
  /** Change one nested table. @param {'forms'|'busy'|'notices'} table @param {string} key @param {any} value */
2737
2786
  const setIn = (table, key, value) => { data = { ...data, [table]: { ...data[table], [key]: value } }; notify() }
2738
2787
 
@@ -2818,6 +2867,7 @@ window.__ModuleLoader__.load({
2818
2867
  currentRevision: payload.selected ? payload.selected.currentRevision : null,
2819
2868
  jobs: payload.selected ? payload.selected.jobs || [] : [],
2820
2869
  unfinishedJobs: payload.selected ? payload.selected.unfinishedJobs || [] : [],
2870
+ previews: null, artifactBase: '', previewContext: null, previewReadStatus: active === null ? 'idle' : 'loading',
2821
2871
  }
2822
2872
  live = patch.unfinishedJobs.length > 0
2823
2873
 
@@ -2833,11 +2883,22 @@ window.__ModuleLoader__.load({
2833
2883
  }
2834
2884
  const previewResult = await readRoute(fetchImpl, `${projectRoute(active, '/previews')}?t=${tick}`, 'project.previews')
2835
2885
  if (previewResult.status === 'ok') {
2836
- patch.previews = previewResult.payload.previews ?? null
2837
- patch.artifactBase = previewResult.payload.artifactBase ?? ''
2838
- } else if (previewResult.status === 'stale') {
2839
- patch.status = 'stale'
2886
+ const result = previewResult.payload
2887
+ if (result.previews?.projectId === active && Array.isArray(result.previews.revisions)
2888
+ && (result.projectId === undefined || result.projectId === active)
2889
+ && result.artifactBase === `/deepblend/artifacts/${encodeURIComponent(active)}/`) {
2890
+ patch.previews = result.previews
2891
+ patch.artifactBase = result.artifactBase
2892
+ patch.previewContext = { projectId: active, projectsRoot: patch.projectsRoot }
2893
+ patch.previewReadStatus = 'ok'
2894
+ } else {
2895
+ patch.previewReadStatus = 'error'
2896
+ patch.error = { code: 'UI_PREVIEW_MISMATCH', message: t('preview.sourceMismatch') }
2897
+ }
2898
+ } else {
2899
+ patch.previewReadStatus = 'error'
2840
2900
  patch.error = previewResult.error
2901
+ if (previewResult.status === 'stale') patch.status = 'stale'
2841
2902
  }
2842
2903
  } else {
2843
2904
  patch.previews = null
@@ -3264,8 +3325,15 @@ window.__ModuleLoader__.load({
3264
3325
  pickCompare: (side, revision) => set(side === 'left'
3265
3326
  ? { compareLeft: revision, diff: null, diffError: null }
3266
3327
  : { compareRight: revision, diff: null, diffError: null }),
3267
- /** Hand off from 版本 to 预览对比 with that revision on the left. */
3268
- compareFrom: (revision) => set({ compareLeft: revision, view: 'preview' }),
3328
+ /** View a saved version against the current one, without changing the project. */
3329
+ compareFrom: (revision) => {
3330
+ const pair = revisionComparisonTarget(data, revision)
3331
+ if (pair) {
3332
+ diffSequence += 1
3333
+ set({ compareLeft: pair.left, compareRight: pair.right, compareMode: 'revisions', view: 'preview',
3334
+ diff: null, diffError: null, editorComparison: null })
3335
+ }
3336
+ },
3269
3337
 
3270
3338
  updateBrief: (field, value, referenceId) => {
3271
3339
  const projectId = data.activeProjectId, current = data.briefDrafts[projectId]
@@ -3565,14 +3633,32 @@ window.__ModuleLoader__.load({
3565
3633
  },
3566
3634
 
3567
3635
  diff: async (from, to) => {
3568
- set({ diffError: null })
3636
+ const projectId = data.activeProjectId, pair = previewRevisionPair(data)
3637
+ if (!running || !projectId || data.view !== 'preview' || data.compareMode !== 'revisions' || pair.left !== from || pair.right !== to) return
3638
+ const sequence = ++diffSequence
3639
+ const stamp = JSON.stringify([data.projectsRoot, projectId, data.currentRevision, from, to])
3640
+ const current = () => {
3641
+ const selected = previewRevisionPair(data)
3642
+ return running && sequence === diffSequence && data.view === 'preview' && data.compareMode === 'revisions'
3643
+ && selected.left === from && selected.right === to
3644
+ && JSON.stringify([data.projectsRoot, data.activeProjectId, data.currentRevision, from, to]) === stamp
3645
+ }
3646
+ set({ diff: null, diffError: null })
3569
3647
  try {
3570
- const response = await fetchImpl(`${projectRoute(target(), '/diff')}?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, { headers: { accept: 'application/json' } })
3648
+ const response = await fetchImpl(`${projectRoute(projectId, '/diff')}?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`, { headers: { accept: 'application/json' } })
3571
3649
  const parsed = await response.json()
3572
- if (parsed && parsed.ok) set({ diff: parsed.diff })
3573
- else set({ diff: null, diffError: (parsed && parsed.error) || { code: `HTTP_${response.status}`, message: t('preview.diffFailed') } })
3650
+ if (!current()) return
3651
+ if (response.ok && parsed?.ok) {
3652
+ if (parsed.projectId !== undefined && parsed.projectId !== projectId
3653
+ || parsed.diff?.projectId !== undefined && parsed.diff.projectId !== projectId
3654
+ || parsed.diff?.fromRevision !== from || parsed.diff?.toRevision !== to
3655
+ || parsed.fromRevision !== undefined && parsed.fromRevision !== from
3656
+ || parsed.toRevision !== undefined && parsed.toRevision !== to) {
3657
+ set({ diff: null, diffError: { code: 'UI_DIFF_MISMATCH', message: t('preview.diffFailed') } })
3658
+ } else set({ diff: parsed.diff })
3659
+ } else set({ diff: null, diffError: parsed?.error || { code: `HTTP_${response.status}`, message: t('preview.diffFailed') } })
3574
3660
  } catch (error) {
3575
- set({ diff: null, diffError: { code: 'UI_FETCH_FAILED', message: String((error && error.message) || error) } })
3661
+ if (current()) set({ diff: null, diffError: { code: 'UI_FETCH_FAILED', message: String((error && error.message) || error) } })
3576
3662
  }
3577
3663
  },
3578
3664
 
@@ -3799,6 +3885,8 @@ window.__ModuleLoader__.load({
3799
3885
  void load()
3800
3886
  },
3801
3887
  stop() {
3888
+ loadSequence += 1
3889
+ diffSequence += 1
3802
3890
  closeCreationDraft()
3803
3891
  draftLifecycle?.removeEventListener?.('pagehide', closeCreationDraft)
3804
3892
  draftLifecycle?.removeEventListener?.('pageshow', reopenCreationDraft)
@@ -4823,11 +4911,15 @@ window.__ModuleLoader__.load({
4823
4911
  const state = ctx.state
4824
4912
  const actions = ctx.actions
4825
4913
  const previews = state.previews
4826
- if (previews === null) return el('div', { 'data-view': 'preview', className: 'db-muted' }, t('common.noProject'))
4914
+ if (previews === null) {
4915
+ if (state.previewReadStatus === 'error') return el('div', { 'data-view': 'preview', 'data-preview-state': 'error', className: 'db-card' },
4916
+ el('div', { className: 'db-muted' }, t('preview.readFailed')),
4917
+ Button({ action: 'preview-reload', onClick: actions.reload, children: t('common.refresh') }))
4918
+ if (state.previewReadStatus === 'loading') return el('div', { 'data-view': 'preview', 'data-preview-state': 'loading', className: 'db-muted' }, t('preview.reading'))
4919
+ return el('div', { 'data-view': 'preview', className: 'db-muted' }, t('common.noProject'))
4920
+ }
4827
4921
  const revisions = previews.revisions
4828
- const pick = wanted => (revisions.some(entry => entry.revision === wanted) ? wanted : (((revisions[revisions.length - 1] || {}).revision) || null))
4829
- const left = pick(state.compareLeft || state.currentRevision)
4830
- const right = pick(state.compareRight || state.currentRevision)
4922
+ const { left, right } = previewRevisionPair(state)
4831
4923
  const entryOf = id => revisions.find(entry => entry.revision === id) || null
4832
4924
 
4833
4925
  /** One image (or the reason there is none), with its digest and its time. */
@@ -5174,6 +5266,7 @@ window.__ModuleLoader__.load({
5174
5266
  ErrorBox({ error: state.error }),
5175
5267
  el('div', { className: 'db-card' },
5176
5268
  el('h4', null, t('revisions.title', { count: revisions.length })),
5269
+ el('p', { className: 'db-muted' }, t('revisions.compareHint')),
5177
5270
  el('ul', { className: 'db-list' }, revisions.map(entry => el('li', { key: entry.revision, 'data-revision': entry.revision },
5178
5271
  el('div', { className: 'db-inline' },
5179
5272
  el('span', { className: 'db-mono' }, entry.revision),
@@ -5181,7 +5274,8 @@ window.__ModuleLoader__.load({
5181
5274
  el('span', { className: 'db-muted' }, entry.kind || '—'),
5182
5275
  el('span', { className: 'db-muted' }, formatTime(entry.createdAt)),
5183
5276
  el('span', { className: 'db-muted db-mono' }, `digest ${shortDigest(entry.digest)}`),
5184
- Button({ action: `compare-from:${entry.revision}`, onClick: () => actions.compareFrom(entry.revision), children: t('revisions.compare') }),
5277
+ Button({ action: `compare-from:${entry.revision}`, disabled: revisionComparisonTarget(state, entry.revision) === null,
5278
+ onClick: () => actions.compareFrom(entry.revision), children: entry.isCurrent ? t('revisions.compareCurrent') : t('revisions.compare') }),
5185
5279
  entry.isCurrent ? null : Button({ action: `restore:${entry.revision}`, disabled: state.busy.restore, onClick: () => actions.restoreRevision(entry.revision), children: t('revisions.restore') }),
5186
5280
  ),
5187
5281
  el('div', { className: 'db-muted' }, entry.summary || t('common.noNote')),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-ui",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
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.7"
22
+ "@deepblend/dsh-blender-contracts": "0.3.9"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {