diff --git a/lib/client.js b/lib/client.js index 2397c52..a8c28bd 100644 --- a/lib/client.js +++ b/lib/client.js @@ -162,6 +162,15 @@ window.__ModuleLoader__.load({ const API = '/dsh-cron-tasks' const LOCALE_NS = 'settings.dshCronTasks' const TITLE_PREFIX = '定时任务 · ' + const listSnapshot = { jobs: [], runs: [], workspaces: [], catalog: { groups: [], current: null } } + + function jobStamp(rows) { + return (rows || []).map((row) => `${row.id}:${row.updatedAt}:${row.nextRunAt}:${row.enabled}:${row.lastStatus}:${row.model}:${row.provider}`).join('|') + } + + function runStamp(rows) { + return (rows || []).map((row) => `${row.id}:${row.status}:${row.actualAt}:${row.summary || ''}`).join('|') + } const DEFAULTS = { enabled: true, timezone: 'Asia/Shanghai', @@ -201,7 +210,7 @@ window.__ModuleLoader__.load({ [class*="_collapsed"] .dsh-ct-entry .dsh-ct-entryLabel,.dsh-ct-entry[data-collapsed=true] .dsh-ct-entryLabel{display:none!important;max-width:0;min-width:0;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none} [class*="_collapsed"] .dsh-ct-entry svg,.dsh-ct-entry[data-collapsed=true] svg{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);margin:0} .dsh-ct-region{display:none !important;flex-direction:column;flex:1;min-height:0;overflow:hidden;padding-right:var(--dsh-sidebar-inline-padding,12px);box-sizing:border-box} -[data-dsh-ct-mode=on] .dsh-ct-region{display:flex !important;animation:dsh-ct-fade-in .18s var(--ds-ease-in-out,ease)} +[data-dsh-ct-mode=on] .dsh-ct-region{display:flex !important} [class*="_collapsed"][data-dsh-ct-mode=on] .dsh-ct-region,[class*="_collapsed"] .dsh-ct-region{display:none !important} [data-dsh-ct-mode=on] [class*="regionArea"] > :not(.dsh-ct-region){display:none !important} @keyframes dsh-ct-fade-in{0%{opacity:0;transform:translateY(4px)}100%{opacity:1;transform:none}} @@ -870,15 +879,15 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba } function CronApp({ t, faces }) { - const [jobs, setJobs] = useState([]) - const [runs, setRuns] = useState([]) + const [jobs, setJobs] = useState(() => listSnapshot.jobs) + const [runs, setRuns] = useState(() => listSnapshot.runs) const [selection, setSelection] = useState({ type: 'new' }) const [expanded, setExpanded] = useState({}) const [form, setForm] = useState(emptyForm()) const [error, setError] = useState('') const [cronMode, setCronMode] = useState(false) - const [workspaces, setWorkspaces] = useState([]) - const [catalog, setCatalog] = useState({ groups: [], current: null }) + const [workspaces, setWorkspaces] = useState(() => listSnapshot.workspaces) + const [catalog, setCatalog] = useState(() => listSnapshot.catalog) const [paneHost, setPaneHost] = useState(() => (typeof document === 'undefined' ? null : document.body)) const skipAutoSelect = useRef(false) @@ -909,11 +918,23 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba api('/workspaces').catch(() => ({ workspaces: [] })), api('/models').catch(() => ({ groups: [], current: null })), ]) - setJobs(jobBody.jobs || []) - setRuns(histBody.runs || []) - setWorkspaces(wsBody.workspaces || []) - setCatalog({ groups: modelBody.groups || [], current: modelBody.current || null }) - faces.cronSessionIds = new Set((histBody.runs || []).map((run) => run.sessionId).filter(Boolean)) + const nextJobs = jobBody.jobs || [] + const nextRuns = histBody.runs || [] + const nextWorkspaces = wsBody.workspaces || [] + const nextCatalog = { groups: modelBody.groups || [], current: modelBody.current || null } + if (jobStamp(listSnapshot.jobs) !== jobStamp(nextJobs)) { + listSnapshot.jobs = nextJobs + setJobs(nextJobs) + } + if (runStamp(listSnapshot.runs) !== runStamp(nextRuns)) { + listSnapshot.runs = nextRuns + setRuns(nextRuns) + } + listSnapshot.workspaces = nextWorkspaces + listSnapshot.catalog = nextCatalog + setWorkspaces(nextWorkspaces) + setCatalog(nextCatalog) + faces.cronSessionIds = new Set(nextRuns.map((run) => run.sessionId).filter(Boolean)) setError('') } catch (err) { setError(err instanceof Error ? err.message : String(err)) @@ -1329,19 +1350,16 @@ body>.dsh-ct-main{position:fixed;top:0;right:0;bottom:0;left:var(--dsh-ct-sideba }) } - if (region && (!listRoot || listRoot.parentElement !== region)) { - if (appRoot) { - try { appRoot.unmount() } catch { /* ignore */ } - appRoot = null + if (region) { + if (!listRoot) { + listRoot = document.createElement('div') + listRoot.className = 'dsh-ct-region' + listRoot.setAttribute('data-plugin', name) + const hostApi = require('react-dom/client') + appRoot = hostApi.createRoot(listRoot) + appRoot.render(h(CronApp, { t, faces })) } - if (listRoot) listRoot.remove() - listRoot = document.createElement('div') - listRoot.className = 'dsh-ct-region' - listRoot.setAttribute('data-plugin', name) - region.appendChild(listRoot) - const hostApi = require('react-dom/client') - appRoot = hostApi.createRoot(listRoot) - appRoot.render(h(CronApp, { t, faces })) + if (listRoot.parentElement !== region) region.appendChild(listRoot) } hideNativeCronRows(sidebar) diff --git a/test/package.test.js b/test/package.test.js index 24f7dc3..6cc6eb4 100644 --- a/test/package.test.js +++ b/test/package.test.js @@ -61,6 +61,9 @@ test('installable bundle declares host apply, client half, unique id, and no @de assert.match(client, /centerCol/) assert.match(client, /data-dsh-ct-host/) assert.match(client, /_collapsed"\]\[data-dsh-ct-mode=on\] \.dsh-ct-region/) + assert.match(client, /listRoot\.parentElement !== region\) region\.appendChild\(listRoot\)/) + assert.match(client, /jobStamp/) + assert.doesNotMatch(client, /\[data-dsh-ct-mode=on\] \.dsh-ct-region\{display:flex !important;animation:/) assert.match(client, /visibility:hidden/) assert.match(client, /createPortal\(editor, paneHost\)/) assert.match(client, /sidebarCollapsed/)