@aphexcms/cms-core 9.5.2 → 9.7.0
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/dist/api/documents.d.ts +28 -0
- package/dist/api/documents.d.ts.map +1 -1
- package/dist/api/documents.js +18 -0
- package/dist/api/schemas/documents.d.ts +19 -0
- package/dist/api/schemas/documents.d.ts.map +1 -1
- package/dist/api/schemas/documents.js +18 -0
- package/dist/client/api.d.ts +2 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +11 -0
- package/dist/client/ui.d.ts +25 -0
- package/dist/client/ui.d.ts.map +1 -0
- package/dist/client/ui.js +53 -0
- package/dist/components/admin/ActivityView.svelte +227 -0
- package/dist/components/admin/ActivityView.svelte.d.ts +4 -0
- package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -0
- package/dist/components/admin/DocumentEditor.svelte +197 -11
- package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
- package/dist/components/admin/ScheduleDialog.svelte +174 -0
- package/dist/components/admin/ScheduleDialog.svelte.d.ts +20 -0
- package/dist/components/admin/ScheduleDialog.svelte.d.ts.map +1 -0
- package/dist/components/admin/fields/ArrayField.svelte +10 -2
- package/dist/components/admin/fields/ArrayField.svelte.d.ts.map +1 -1
- package/dist/components/layout/OrganizationSwitcher.svelte +3 -3
- package/dist/components/layout/sidebar/NavUser.svelte +5 -5
- package/dist/db/interfaces/events.d.ts +55 -0
- package/dist/db/interfaces/events.d.ts.map +1 -0
- package/dist/db/interfaces/events.js +1 -0
- package/dist/db/interfaces/index.d.ts +10 -2
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/plugin-storage.d.ts +39 -0
- package/dist/db/interfaces/plugin-storage.d.ts.map +1 -0
- package/dist/db/interfaces/plugin-storage.js +1 -0
- package/dist/events/catalog.d.ts +8 -0
- package/dist/events/catalog.d.ts.map +1 -0
- package/dist/events/catalog.js +11 -0
- package/dist/events/consumer.d.ts +79 -0
- package/dist/events/consumer.d.ts.map +1 -0
- package/dist/events/consumer.js +76 -0
- package/dist/events/define-event.d.ts +17 -0
- package/dist/events/define-event.d.ts.map +1 -0
- package/dist/events/define-event.js +7 -0
- package/dist/events/emit.d.ts +11 -0
- package/dist/events/emit.d.ts.map +1 -0
- package/dist/events/emit.js +20 -0
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +3 -0
- package/dist/field-validation/rule.d.ts.map +1 -1
- package/dist/field-validation/rule.js +15 -0
- package/dist/field-validation/utils.d.ts.map +1 -1
- package/dist/field-validation/utils.js +7 -1
- package/dist/forms/define-form.d.ts +25 -0
- package/dist/forms/define-form.d.ts.map +1 -0
- package/dist/forms/define-form.js +22 -0
- package/dist/forms/index.d.ts +4 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +5 -0
- package/dist/forms/types.d.ts +49 -0
- package/dist/forms/types.d.ts.map +1 -0
- package/dist/forms/types.js +1 -0
- package/dist/forms/validate.d.ts +9 -0
- package/dist/forms/validate.d.ts.map +1 -0
- package/dist/forms/validate.js +18 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/jobs/document-jobs.d.ts +27 -0
- package/dist/jobs/document-jobs.d.ts.map +1 -0
- package/dist/jobs/document-jobs.js +42 -0
- package/dist/jobs/embedded-runner.d.ts +25 -0
- package/dist/jobs/embedded-runner.d.ts.map +1 -0
- package/dist/jobs/embedded-runner.js +46 -0
- package/dist/jobs/index.d.ts +7 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +8 -0
- package/dist/jobs/relay.d.ts +27 -0
- package/dist/jobs/relay.d.ts.map +1 -0
- package/dist/jobs/relay.js +60 -0
- package/dist/jobs/run-batch.d.ts +52 -0
- package/dist/jobs/run-batch.d.ts.map +1 -0
- package/dist/jobs/run-batch.js +61 -0
- package/dist/jobs/run-due-jobs.d.ts +30 -0
- package/dist/jobs/run-due-jobs.d.ts.map +1 -0
- package/dist/jobs/run-due-jobs.js +69 -0
- package/dist/jobs/types.d.ts +18 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +1 -0
- package/dist/lib/api/documents.d.ts +28 -0
- package/dist/lib/api/documents.d.ts.map +1 -1
- package/dist/lib/api/documents.js +18 -0
- package/dist/lib/api/documents.js.map +1 -1
- package/dist/lib/api/schemas/documents.d.ts +19 -0
- package/dist/lib/api/schemas/documents.d.ts.map +1 -1
- package/dist/lib/api/schemas/documents.js +18 -0
- package/dist/lib/api/schemas/documents.js.map +1 -1
- package/dist/lib/client/api.d.ts +2 -0
- package/dist/lib/client/api.d.ts.map +1 -0
- package/dist/lib/client/api.js +12 -0
- package/dist/lib/client/api.js.map +1 -0
- package/dist/lib/client/ui.d.ts +25 -0
- package/dist/lib/client/ui.d.ts.map +1 -0
- package/dist/lib/client/ui.js +54 -0
- package/dist/lib/client/ui.js.map +1 -0
- package/dist/lib/db/interfaces/events.d.ts +55 -0
- package/dist/lib/db/interfaces/events.d.ts.map +1 -0
- package/dist/lib/db/interfaces/events.js +2 -0
- package/dist/lib/db/interfaces/events.js.map +1 -0
- package/dist/lib/db/interfaces/index.d.ts +10 -2
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/plugin-storage.d.ts +39 -0
- package/dist/lib/db/interfaces/plugin-storage.d.ts.map +1 -0
- package/dist/lib/db/interfaces/plugin-storage.js +2 -0
- package/dist/lib/db/interfaces/plugin-storage.js.map +1 -0
- package/dist/lib/events/catalog.d.ts +8 -0
- package/dist/lib/events/catalog.d.ts.map +1 -0
- package/dist/lib/events/catalog.js +12 -0
- package/dist/lib/events/catalog.js.map +1 -0
- package/dist/lib/events/consumer.d.ts +79 -0
- package/dist/lib/events/consumer.d.ts.map +1 -0
- package/dist/lib/events/consumer.js +77 -0
- package/dist/lib/events/consumer.js.map +1 -0
- package/dist/lib/events/define-event.d.ts +17 -0
- package/dist/lib/events/define-event.d.ts.map +1 -0
- package/dist/lib/events/define-event.js +8 -0
- package/dist/lib/events/define-event.js.map +1 -0
- package/dist/lib/events/emit.d.ts +11 -0
- package/dist/lib/events/emit.d.ts.map +1 -0
- package/dist/lib/events/emit.js +21 -0
- package/dist/lib/events/emit.js.map +1 -0
- package/dist/lib/events/index.d.ts +4 -0
- package/dist/lib/events/index.d.ts.map +1 -0
- package/dist/lib/events/index.js +4 -0
- package/dist/lib/events/index.js.map +1 -0
- package/dist/lib/field-validation/rule.d.ts.map +1 -1
- package/dist/lib/field-validation/rule.js +15 -0
- package/dist/lib/field-validation/rule.js.map +1 -1
- package/dist/lib/field-validation/utils.d.ts.map +1 -1
- package/dist/lib/field-validation/utils.js +7 -1
- package/dist/lib/field-validation/utils.js.map +1 -1
- package/dist/lib/forms/define-form.d.ts +25 -0
- package/dist/lib/forms/define-form.d.ts.map +1 -0
- package/dist/lib/forms/define-form.js +23 -0
- package/dist/lib/forms/define-form.js.map +1 -0
- package/dist/lib/forms/index.d.ts +4 -0
- package/dist/lib/forms/index.d.ts.map +1 -0
- package/dist/lib/forms/index.js +6 -0
- package/dist/lib/forms/index.js.map +1 -0
- package/dist/lib/forms/types.d.ts +49 -0
- package/dist/lib/forms/types.d.ts.map +1 -0
- package/dist/lib/forms/types.js +2 -0
- package/dist/lib/forms/types.js.map +1 -0
- package/dist/lib/forms/validate.d.ts +9 -0
- package/dist/lib/forms/validate.d.ts.map +1 -0
- package/dist/lib/forms/validate.js +19 -0
- package/dist/lib/forms/validate.js.map +1 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/hooks.js +15 -0
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/index.d.ts +2 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +6 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/jobs/document-jobs.d.ts +27 -0
- package/dist/lib/jobs/document-jobs.d.ts.map +1 -0
- package/dist/lib/jobs/document-jobs.js +43 -0
- package/dist/lib/jobs/document-jobs.js.map +1 -0
- package/dist/lib/jobs/embedded-runner.d.ts +25 -0
- package/dist/lib/jobs/embedded-runner.d.ts.map +1 -0
- package/dist/lib/jobs/embedded-runner.js +47 -0
- package/dist/lib/jobs/embedded-runner.js.map +1 -0
- package/dist/lib/jobs/index.d.ts +7 -0
- package/dist/lib/jobs/index.d.ts.map +1 -0
- package/dist/lib/jobs/index.js +9 -0
- package/dist/lib/jobs/index.js.map +1 -0
- package/dist/lib/jobs/relay.d.ts +27 -0
- package/dist/lib/jobs/relay.d.ts.map +1 -0
- package/dist/lib/jobs/relay.js +61 -0
- package/dist/lib/jobs/relay.js.map +1 -0
- package/dist/lib/jobs/run-batch.d.ts +52 -0
- package/dist/lib/jobs/run-batch.d.ts.map +1 -0
- package/dist/lib/jobs/run-batch.js +62 -0
- package/dist/lib/jobs/run-batch.js.map +1 -0
- package/dist/lib/jobs/run-due-jobs.d.ts +30 -0
- package/dist/lib/jobs/run-due-jobs.d.ts.map +1 -0
- package/dist/lib/jobs/run-due-jobs.js +70 -0
- package/dist/lib/jobs/run-due-jobs.js.map +1 -0
- package/dist/lib/jobs/types.d.ts +18 -0
- package/dist/lib/jobs/types.d.ts.map +1 -0
- package/dist/lib/jobs/types.js +2 -0
- package/dist/lib/jobs/types.js.map +1 -0
- package/dist/lib/local-api/collection-api.d.ts +42 -0
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +200 -25
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/hooks.d.ts +7 -0
- package/dist/lib/local-api/hooks.d.ts.map +1 -0
- package/dist/lib/local-api/hooks.js +20 -0
- package/dist/lib/local-api/hooks.js.map +1 -0
- package/dist/lib/mcp/tools.js +1 -1
- package/dist/lib/mcp/tools.js.map +1 -1
- package/dist/lib/plugins/resolver.d.ts +12 -1
- package/dist/lib/plugins/resolver.d.ts.map +1 -1
- package/dist/lib/plugins/resolver.js +4 -1
- package/dist/lib/plugins/resolver.js.map +1 -1
- package/dist/lib/plugins/types.d.ts +47 -2
- package/dist/lib/plugins/types.d.ts.map +1 -1
- package/dist/lib/plugins/types.js.map +1 -1
- package/dist/lib/schema-utils/define-type.d.ts +32 -0
- package/dist/lib/schema-utils/define-type.d.ts.map +1 -0
- package/dist/lib/schema-utils/define-type.js +41 -0
- package/dist/lib/schema-utils/define-type.js.map +1 -0
- package/dist/lib/schema-utils/index.d.ts +1 -0
- package/dist/lib/schema-utils/index.d.ts.map +1 -1
- package/dist/lib/schema-utils/index.js +1 -0
- package/dist/lib/schema-utils/index.js.map +1 -1
- package/dist/lib/server/api/index.d.ts.map +1 -1
- package/dist/lib/server/api/index.js +8 -0
- package/dist/lib/server/api/index.js.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.js +110 -0
- package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
- package/dist/lib/server/api/routes/jobs.d.ts +4 -0
- package/dist/lib/server/api/routes/jobs.d.ts.map +1 -0
- package/dist/lib/server/api/routes/jobs.js +93 -0
- package/dist/lib/server/api/routes/jobs.js.map +1 -0
- package/dist/lib/server/api/routes/workers-run.d.ts +4 -0
- package/dist/lib/server/api/routes/workers-run.d.ts.map +1 -0
- package/dist/lib/server/api/routes/workers-run.js +41 -0
- package/dist/lib/server/api/routes/workers-run.js.map +1 -0
- package/dist/lib/server/index.d.ts +1 -0
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +2 -0
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/services/version-service.d.ts +16 -0
- package/dist/lib/services/version-service.d.ts.map +1 -1
- package/dist/lib/services/version-service.js +72 -48
- package/dist/lib/services/version-service.js.map +1 -1
- package/dist/lib/types/config.d.ts +43 -0
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/events.d.ts +134 -0
- package/dist/lib/types/events.d.ts.map +1 -0
- package/dist/lib/types/events.js +4 -0
- package/dist/lib/types/events.js.map +1 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/schemas.d.ts +71 -0
- package/dist/lib/types/schemas.d.ts.map +1 -1
- package/dist/lib/utils/index.d.ts +1 -1
- package/dist/lib/utils/index.d.ts.map +1 -1
- package/dist/lib/utils/index.js.map +1 -1
- package/dist/local-api/collection-api.d.ts +42 -0
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +200 -25
- package/dist/local-api/hooks.d.ts +7 -0
- package/dist/local-api/hooks.d.ts.map +1 -0
- package/dist/local-api/hooks.js +19 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/plugins/resolver.d.ts +12 -1
- package/dist/plugins/resolver.d.ts.map +1 -1
- package/dist/plugins/resolver.js +4 -1
- package/dist/plugins/types.d.ts +47 -2
- package/dist/plugins/types.d.ts.map +1 -1
- package/dist/schema-utils/define-type.d.ts +32 -0
- package/dist/schema-utils/define-type.d.ts.map +1 -0
- package/dist/schema-utils/define-type.js +40 -0
- package/dist/schema-utils/index.d.ts +1 -0
- package/dist/schema-utils/index.d.ts.map +1 -1
- package/dist/schema-utils/index.js +1 -0
- package/dist/server/api/index.d.ts.map +1 -1
- package/dist/server/api/index.js +8 -0
- package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/server/api/routes/documents-publish.js +110 -0
- package/dist/server/api/routes/jobs.d.ts +4 -0
- package/dist/server/api/routes/jobs.d.ts.map +1 -0
- package/dist/server/api/routes/jobs.js +92 -0
- package/dist/server/api/routes/workers-run.d.ts +4 -0
- package/dist/server/api/routes/workers-run.d.ts.map +1 -0
- package/dist/server/api/routes/workers-run.js +40 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/services/version-service.d.ts +16 -0
- package/dist/services/version-service.d.ts.map +1 -1
- package/dist/services/version-service.js +72 -48
- package/dist/types/config.d.ts +43 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/events.d.ts +134 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/events.js +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/schemas.d.ts +71 -0
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { setSaveStateContext } from '../../save-state-context.svelte';
|
|
15
15
|
import { usePermissions } from '../../permissions-context.svelte';
|
|
16
16
|
import AdminSlot from './AdminSlot.svelte';
|
|
17
|
+
import ScheduleDialog from './ScheduleDialog.svelte';
|
|
17
18
|
import { createPartResolver } from '../../plugins/resolver';
|
|
18
19
|
import type { CMSPlugin, DocumentActionProps } from '../../plugins/types';
|
|
19
20
|
import { getDefaultValueForFieldType } from '../../utils/field-defaults';
|
|
@@ -37,7 +38,10 @@
|
|
|
37
38
|
Smartphone,
|
|
38
39
|
ZoomIn,
|
|
39
40
|
ZoomOut,
|
|
40
|
-
ArrowLeft
|
|
41
|
+
ArrowLeft,
|
|
42
|
+
CalendarClock,
|
|
43
|
+
X,
|
|
44
|
+
Lock
|
|
41
45
|
} from '@lucide/svelte';
|
|
42
46
|
import { stegaEncodeDocument } from '../../preview/stega.js';
|
|
43
47
|
import { collectAssetRefs, injectAssetData, type ResolvedAsset } from '../../preview/assets.js';
|
|
@@ -160,6 +164,14 @@
|
|
|
160
164
|
let publishedData = $state<Record<string, any> | null>(null);
|
|
161
165
|
const isViewingPublished = $derived(perspective === 'published');
|
|
162
166
|
|
|
167
|
+
// The scheduled action follows the active perspective — the tab IS the intent:
|
|
168
|
+
// Draft tab → publish (send this draft live later); Published tab → unpublish (take it
|
|
169
|
+
// down later). No toggle in the dialog. The control is gated on the matching permission.
|
|
170
|
+
const scheduleAction = $derived<'publish' | 'unpublish'>(
|
|
171
|
+
perspective === 'published' ? 'unpublish' : 'publish'
|
|
172
|
+
);
|
|
173
|
+
const canScheduleNow = $derived(scheduleAction === 'publish' ? canPublishDoc : canUnpublishDoc);
|
|
174
|
+
|
|
163
175
|
// Plugin document actions applicable to this type. Capability-gated, with
|
|
164
176
|
// built-in privileged roles bypassing. Cheap to rebuild from the plugin list.
|
|
165
177
|
const pluginDocumentActions = $derived(
|
|
@@ -706,6 +718,53 @@
|
|
|
706
718
|
|
|
707
719
|
// Header options dropdown
|
|
708
720
|
let showHeaderMenu = $state(false);
|
|
721
|
+
let showScheduleDialog = $state(false);
|
|
722
|
+
|
|
723
|
+
// Pending scheduled publish/unpublish for this document (replace semantics → at most one).
|
|
724
|
+
type ScheduledJob = { jobId: string; type: string; runAt: string; status: string };
|
|
725
|
+
let scheduledJobs = $state<ScheduledJob[]>([]);
|
|
726
|
+
// The soonest pending schedule — what the header indicator shows.
|
|
727
|
+
const nextSchedule = $derived(
|
|
728
|
+
[...scheduledJobs].sort(
|
|
729
|
+
(a, b) => new Date(a.runAt).getTime() - new Date(b.runAt).getTime()
|
|
730
|
+
)[0] ?? null
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
async function loadSchedule() {
|
|
734
|
+
if (!documentId) {
|
|
735
|
+
scheduledJobs = [];
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
const res = await documents.getSchedule(documentId);
|
|
740
|
+
scheduledJobs = res.success ? (res.data ?? []) : [];
|
|
741
|
+
} catch {
|
|
742
|
+
scheduledJobs = [];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Optimistic: the schedule call returns the created job, so show the badge immediately.
|
|
747
|
+
// Replace semantics on the server mean this new job supersedes any prior one. Reconcile
|
|
748
|
+
// with a background fetch so we converge on server truth (id, exact runAt).
|
|
749
|
+
function onScheduleCreated(job: ScheduledJob) {
|
|
750
|
+
scheduledJobs = [job];
|
|
751
|
+
loadSchedule();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function cancelSchedule() {
|
|
755
|
+
if (!documentId) return;
|
|
756
|
+
// Optimistically clear the badge; restore from the server if the cancel fails.
|
|
757
|
+
const previous = scheduledJobs;
|
|
758
|
+
scheduledJobs = [];
|
|
759
|
+
const res = await documents.cancelSchedule(documentId);
|
|
760
|
+
if (res.success) {
|
|
761
|
+
toast.success('Schedule cancelled');
|
|
762
|
+
loadSchedule();
|
|
763
|
+
} else {
|
|
764
|
+
scheduledJobs = previous;
|
|
765
|
+
toast.error(res.error || 'Failed to cancel schedule');
|
|
766
|
+
}
|
|
767
|
+
}
|
|
709
768
|
|
|
710
769
|
// Version history
|
|
711
770
|
let showVersionHistory = $state(false);
|
|
@@ -775,6 +834,33 @@
|
|
|
775
834
|
|
|
776
835
|
const savedAgoText = $derived(lastSaved ? `Saved ${timeAgo(lastSaved)}` : null);
|
|
777
836
|
|
|
837
|
+
// Relative future time for the scheduled-publish indicator ("in 3h"). Reads `now` so it ticks.
|
|
838
|
+
function timeUntil(date: Date): string {
|
|
839
|
+
const seconds = Math.floor((date.getTime() - now) / 1000);
|
|
840
|
+
if (seconds <= 0) return 'now';
|
|
841
|
+
if (seconds < 60) return `in ${seconds}s`;
|
|
842
|
+
const minutes = Math.floor(seconds / 60);
|
|
843
|
+
if (minutes < 60) return `in ${minutes}m`;
|
|
844
|
+
const hours = Math.floor(minutes / 60);
|
|
845
|
+
if (hours < 24) return `in ${hours}h`;
|
|
846
|
+
const days = Math.floor(hours / 24);
|
|
847
|
+
return `in ${days}d`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Human phrasing for the schedule banner: "today/tomorrow at 8:00 AM", "on Monday at …",
|
|
851
|
+
// or "on Nov 10 at …" further out. Reads `now` so it stays current.
|
|
852
|
+
function humanizeSchedule(date: Date): string {
|
|
853
|
+
const time = date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
854
|
+
const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
855
|
+
const days = Math.round((startOfDay(date) - startOfDay(new Date(now))) / 86_400_000);
|
|
856
|
+
let day: string;
|
|
857
|
+
if (days <= 0) day = 'today';
|
|
858
|
+
else if (days === 1) day = 'tomorrow';
|
|
859
|
+
else if (days < 7) day = `on ${date.toLocaleDateString(undefined, { weekday: 'long' })}`;
|
|
860
|
+
else day = `on ${date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
|
|
861
|
+
return `${day} at ${time}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
778
864
|
// Auto-save functionality (every 2 seconds when there are changes)
|
|
779
865
|
let hasUnsavedChanges = $state(false);
|
|
780
866
|
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -817,6 +903,14 @@
|
|
|
817
903
|
(hasUnpublishedContent || isUnpublished) && !saving && documentId && !hasValidationErrors
|
|
818
904
|
);
|
|
819
905
|
|
|
906
|
+
// The schedule button mirrors the inline action button's enabled state (permission is checked
|
|
907
|
+
// separately, for visibility): scheduling a PUBLISH is enabled exactly when the Publish button
|
|
908
|
+
// is (`canPublish` — there are unpublished, valid changes), so you can't queue a no-op publish;
|
|
909
|
+
// scheduling an UNPUBLISH mirrors the Unpublish button (enabled whenever not mid-save).
|
|
910
|
+
const canScheduleActionNow = $derived(
|
|
911
|
+
scheduleAction === 'publish' ? Boolean(canPublish) : !saving
|
|
912
|
+
);
|
|
913
|
+
|
|
820
914
|
// Get preview title based on schema config. When viewing in published
|
|
821
915
|
// perspective, read from the loaded published snapshot — otherwise the
|
|
822
916
|
// header shows the draft's title even though the body shows published
|
|
@@ -884,6 +978,14 @@
|
|
|
884
978
|
}
|
|
885
979
|
});
|
|
886
980
|
|
|
981
|
+
// Keep the schedule indicator in sync with the current document.
|
|
982
|
+
$effect(() => {
|
|
983
|
+
// Reference documentId so this re-runs when the editor switches documents.
|
|
984
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reactive dependency
|
|
985
|
+
documentId;
|
|
986
|
+
loadSchedule();
|
|
987
|
+
});
|
|
988
|
+
|
|
887
989
|
// Load existing document data when editing
|
|
888
990
|
$effect(() => {
|
|
889
991
|
if (!isCreating && documentId) {
|
|
@@ -1775,6 +1877,41 @@
|
|
|
1775
1877
|
{/if}
|
|
1776
1878
|
|
|
1777
1879
|
<div class="relative flex h-full w-full min-w-0 flex-col overflow-hidden">
|
|
1880
|
+
<!-- Scheduled publish/unpublish banner — a full-width strip pinned to the very top edge of
|
|
1881
|
+
the editor, above the hero header (breadcrumb + actions), so a pending schedule is the
|
|
1882
|
+
first thing seen. Inner padding matches the hero's (px-4 lg:px-6) so its text lines up
|
|
1883
|
+
with the title below. -->
|
|
1884
|
+
{#if !presentationMode && nextSchedule}
|
|
1885
|
+
{@const isPub = nextSchedule.type === 'document.publish'}
|
|
1886
|
+
{@const runAtDate = new Date(nextSchedule.runAt)}
|
|
1887
|
+
<div
|
|
1888
|
+
class="border-primary/15 bg-primary/5 text-primary flex w-full flex-wrap items-center gap-x-2 gap-y-1 border-b px-4 py-2.5 lg:px-6"
|
|
1889
|
+
>
|
|
1890
|
+
<Lock class="h-4 w-4 shrink-0" />
|
|
1891
|
+
<span class="min-w-0 text-[0.9375rem] leading-tight font-medium">
|
|
1892
|
+
Scheduled to be {isPub ? 'published' : 'unpublished'}
|
|
1893
|
+
{humanizeSchedule(runAtDate)}
|
|
1894
|
+
<span class="text-primary/50 font-normal">· {timeUntil(runAtDate)}</span>
|
|
1895
|
+
</span>
|
|
1896
|
+
<div class="ml-auto flex items-center gap-1">
|
|
1897
|
+
<button
|
|
1898
|
+
type="button"
|
|
1899
|
+
class="hover:bg-primary/10 rounded px-2 py-1 text-xs font-medium transition-colors"
|
|
1900
|
+
onclick={() => (showScheduleDialog = true)}
|
|
1901
|
+
>
|
|
1902
|
+
Reschedule
|
|
1903
|
+
</button>
|
|
1904
|
+
<button
|
|
1905
|
+
type="button"
|
|
1906
|
+
class="hover:bg-primary/10 flex items-center gap-1 rounded px-2 py-1 text-xs font-medium transition-colors"
|
|
1907
|
+
onclick={cancelSchedule}
|
|
1908
|
+
>
|
|
1909
|
+
<X class="h-3 w-3" /> Cancel
|
|
1910
|
+
</button>
|
|
1911
|
+
</div>
|
|
1912
|
+
</div>
|
|
1913
|
+
{/if}
|
|
1914
|
+
|
|
1778
1915
|
<!-- Hero Header -->
|
|
1779
1916
|
{#if !presentationMode}
|
|
1780
1917
|
<div class="bg-background w-full min-w-0 overflow-x-clip px-4 pt-4 pb-5 lg:px-6 lg:pt-5">
|
|
@@ -1795,7 +1932,7 @@
|
|
|
1795
1932
|
<!-- Always one line (`truncate`); font shrinks where space is tight (narrow
|
|
1796
1933
|
editor / reference panel) so more of the title fits before the ellipsis,
|
|
1797
1934
|
and stays large on wide screens. -->
|
|
1798
|
-
<h1 class="block w-full min-w-0 truncate text-
|
|
1935
|
+
<h1 class="block w-full min-w-0 truncate text-2xl font-semibold tracking-tight lg:text-4xl">
|
|
1799
1936
|
{getPreviewTitle()}
|
|
1800
1937
|
</h1>
|
|
1801
1938
|
|
|
@@ -1858,7 +1995,7 @@
|
|
|
1858
1995
|
{#if schema && schema.groups && schema.groups.length > 0}
|
|
1859
1996
|
{@const visibleGroups = schema.groups.filter((g) => !g.hidden)}
|
|
1860
1997
|
<div
|
|
1861
|
-
class="border-rule/60 flex shrink-0 items-center gap-1 overflow-x-auto border-b px-4 py-2
|
|
1998
|
+
class="border-rule/60 flex shrink-0 [scrollbar-width:none] items-center gap-1 overflow-x-auto border-b px-4 py-2 lg:px-6 [&::-webkit-scrollbar]:hidden"
|
|
1862
1999
|
>
|
|
1863
2000
|
<button
|
|
1864
2001
|
type="button"
|
|
@@ -2272,15 +2409,37 @@
|
|
|
2272
2409
|
: 'Unknown'}
|
|
2273
2410
|
{/if}
|
|
2274
2411
|
</p>
|
|
2275
|
-
|
|
2276
|
-
{#if
|
|
2277
|
-
<Button
|
|
2412
|
+
<div class="flex items-center gap-2">
|
|
2413
|
+
{#if documentId && canScheduleNow}
|
|
2414
|
+
<Button
|
|
2415
|
+
variant="ghost"
|
|
2416
|
+
size="icon"
|
|
2417
|
+
disabled={!canScheduleActionNow}
|
|
2418
|
+
onclick={() => (showScheduleDialog = true)}
|
|
2419
|
+
class="h-8 w-8 {canScheduleActionNow ? 'cursor-pointer' : ''} {nextSchedule
|
|
2420
|
+
? 'text-primary'
|
|
2421
|
+
: ''}"
|
|
2422
|
+
title={!canScheduleActionNow
|
|
2423
|
+
? 'Nothing to publish — make a change first'
|
|
2424
|
+
: nextSchedule
|
|
2425
|
+
? 'Reschedule'
|
|
2426
|
+
: scheduleAction === 'publish'
|
|
2427
|
+
? 'Schedule publish'
|
|
2428
|
+
: 'Schedule unpublish'}
|
|
2429
|
+
>
|
|
2430
|
+
<CalendarClock class="h-4 w-4" />
|
|
2431
|
+
</Button>
|
|
2278
2432
|
{/if}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2433
|
+
{#if fullDocument?._meta?.status === 'unpublished'}
|
|
2434
|
+
{#if canPublishDoc}
|
|
2435
|
+
<Button size="sm" onclick={publishDocument} disabled={saving}>Publish</Button>
|
|
2436
|
+
{/if}
|
|
2437
|
+
{:else if canUnpublishDoc}
|
|
2438
|
+
<Button size="sm" variant="secondary" onclick={unpublishDocument} disabled={saving}>
|
|
2439
|
+
Unpublish
|
|
2440
|
+
</Button>
|
|
2441
|
+
{/if}
|
|
2442
|
+
</div>
|
|
2284
2443
|
</div>
|
|
2285
2444
|
{:else}
|
|
2286
2445
|
<div class="flex items-center justify-between">
|
|
@@ -2293,6 +2452,24 @@
|
|
|
2293
2452
|
|
|
2294
2453
|
<!-- Right: Publish button + horizontal three dots menu -->
|
|
2295
2454
|
<div class="flex items-center gap-2">
|
|
2455
|
+
{#if documentId && canScheduleNow}
|
|
2456
|
+
<Button
|
|
2457
|
+
variant="ghost"
|
|
2458
|
+
size="icon"
|
|
2459
|
+
disabled={!canScheduleActionNow}
|
|
2460
|
+
onclick={() => (showScheduleDialog = true)}
|
|
2461
|
+
class="h-8 w-8 {canScheduleActionNow ? 'cursor-pointer' : ''} {nextSchedule
|
|
2462
|
+
? 'text-primary'
|
|
2463
|
+
: ''}"
|
|
2464
|
+
title={!canScheduleActionNow
|
|
2465
|
+
? 'Nothing to publish — make a change first'
|
|
2466
|
+
: nextSchedule
|
|
2467
|
+
? 'Reschedule'
|
|
2468
|
+
: 'Schedule publish'}
|
|
2469
|
+
>
|
|
2470
|
+
<CalendarClock class="h-4 w-4" />
|
|
2471
|
+
</Button>
|
|
2472
|
+
{/if}
|
|
2296
2473
|
{#if canPublishDoc && !isViewingPublished}
|
|
2297
2474
|
<Button
|
|
2298
2475
|
onclick={publishDocument}
|
|
@@ -2428,6 +2605,15 @@
|
|
|
2428
2605
|
{/if}
|
|
2429
2606
|
|
|
2430
2607
|
<!-- Inspect Modal -->
|
|
2608
|
+
{#if documentId}
|
|
2609
|
+
<ScheduleDialog
|
|
2610
|
+
bind:open={showScheduleDialog}
|
|
2611
|
+
{documentId}
|
|
2612
|
+
action={scheduleAction}
|
|
2613
|
+
onScheduled={onScheduleCreated}
|
|
2614
|
+
/>
|
|
2615
|
+
{/if}
|
|
2616
|
+
|
|
2431
2617
|
{#if showInspect}
|
|
2432
2618
|
<div class="absolute inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
2433
2619
|
<div
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"DocumentEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/DocumentEditor.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AASzD,OAAO,KAAK,EAAE,SAAS,EAAuB,MAAM,qBAAqB,CAAC;AAgCzE,UAAU,KAAK;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB;;kDAE8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,oBAAoB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,sBAAsB,CAAC,EAAE;QACxB,aAAa,EAAE,MAAM,CAAC;QACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAC1B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,IAAI,CAAC;IACT,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACtB;AAq1EF,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Schedule a future publish/unpublish. Layout mirrors Sanity's scheduled-drafts dialog
|
|
3
|
+
// (title · description · "Schedule on" field → calendar+time popover · Cancel/Schedule),
|
|
4
|
+
// in Aphex's shadcn skin. Posts to /documents/:id/schedule; the worker runs it at `runAt`
|
|
5
|
+
// (re-validating then), and `runAt` is floored to the minute so 1:30 means 1:30:00.
|
|
6
|
+
import * as Dialog from '@aphexcms/ui/shadcn/dialog';
|
|
7
|
+
import * as Popover from '@aphexcms/ui/shadcn/popover';
|
|
8
|
+
import { Calendar } from '@aphexcms/ui/shadcn/calendar';
|
|
9
|
+
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
10
|
+
import { Label } from '@aphexcms/ui/shadcn/label';
|
|
11
|
+
import { Calendar as CalendarIcon, Globe } from '@lucide/svelte';
|
|
12
|
+
import { CalendarDate, today, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
|
13
|
+
import { documents } from '../../api/documents';
|
|
14
|
+
import { toast } from 'svelte-sonner';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
open: boolean;
|
|
18
|
+
documentId: string;
|
|
19
|
+
/**
|
|
20
|
+
* The action to schedule. Derived from the editor's active perspective by the caller:
|
|
21
|
+
* viewing the Draft tab → 'publish' (send this draft live later); viewing the Published
|
|
22
|
+
* tab → 'unpublish' (take it down later). No in-dialog toggle — the tab is the intent.
|
|
23
|
+
*/
|
|
24
|
+
action: 'publish' | 'unpublish';
|
|
25
|
+
onScheduled?: (job: { jobId: string; type: string; runAt: string; status: string }) => void;
|
|
26
|
+
}
|
|
27
|
+
let { open = $bindable(), documentId, action, onScheduled }: Props = $props();
|
|
28
|
+
|
|
29
|
+
let dateValue = $state<DateValue | undefined>(undefined);
|
|
30
|
+
let timeStr = $state('09:00'); // "HH:MM", 24h internally
|
|
31
|
+
let pickerOpen = $state(false);
|
|
32
|
+
let submitting = $state(false);
|
|
33
|
+
|
|
34
|
+
const actionVerb = $derived(action === 'publish' ? 'Publish' : 'Unpublish');
|
|
35
|
+
|
|
36
|
+
const minDate = today(getLocalTimeZone());
|
|
37
|
+
// Short local timezone label, e.g. "MST" — shown so the user knows what zone they're setting.
|
|
38
|
+
const tzLabel =
|
|
39
|
+
new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' })
|
|
40
|
+
.formatToParts(new Date())
|
|
41
|
+
.find((p) => p.type === 'timeZoneName')?.value ?? '';
|
|
42
|
+
|
|
43
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
44
|
+
|
|
45
|
+
// Reset the date/time each time the dialog opens: default to one hour out, floored to the minute.
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (open) {
|
|
48
|
+
const d = new Date(Date.now() + 60 * 60 * 1000);
|
|
49
|
+
dateValue = new CalendarDate(d.getFullYear(), d.getMonth() + 1, d.getDate());
|
|
50
|
+
timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/** Combine the picked calendar date + time into a local Date floored to the minute. */
|
|
55
|
+
function buildRunAt(): Date | null {
|
|
56
|
+
if (!dateValue) return null;
|
|
57
|
+
const [h, m] = timeStr.split(':').map(Number);
|
|
58
|
+
const d = new Date(dateValue.year, dateValue.month - 1, dateValue.day, h ?? 0, m ?? 0, 0, 0);
|
|
59
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const runAtPreview = $derived(buildRunAt());
|
|
63
|
+
const fieldLabel = $derived(
|
|
64
|
+
runAtPreview
|
|
65
|
+
? runAtPreview.toLocaleString(undefined, {
|
|
66
|
+
month: 'short',
|
|
67
|
+
day: 'numeric',
|
|
68
|
+
year: 'numeric',
|
|
69
|
+
hour: 'numeric',
|
|
70
|
+
minute: '2-digit'
|
|
71
|
+
})
|
|
72
|
+
: 'Select date and time'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
function setToCurrentTime() {
|
|
76
|
+
const now = new Date();
|
|
77
|
+
dateValue = new CalendarDate(now.getFullYear(), now.getMonth() + 1, now.getDate());
|
|
78
|
+
timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function submit() {
|
|
82
|
+
const runAt = buildRunAt();
|
|
83
|
+
if (!runAt) {
|
|
84
|
+
toast.error('Pick a date and time');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
runAt.setSeconds(0, 0);
|
|
88
|
+
// Reject genuinely past minutes; the current minute is allowed (runs on the next tick).
|
|
89
|
+
const currentMinute = new Date();
|
|
90
|
+
currentMinute.setSeconds(0, 0);
|
|
91
|
+
if (runAt.getTime() < currentMinute.getTime()) {
|
|
92
|
+
toast.error('Pick a time in the future');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
submitting = true;
|
|
97
|
+
try {
|
|
98
|
+
const res = await documents.schedule(documentId, { action, runAt: runAt.toISOString() });
|
|
99
|
+
if (res.success) {
|
|
100
|
+
toast.success(
|
|
101
|
+
`${action === 'publish' ? 'Publish' : 'Unpublish'} scheduled for ${runAt.toLocaleString()}`
|
|
102
|
+
);
|
|
103
|
+
open = false;
|
|
104
|
+
if (res.data) onScheduled?.(res.data);
|
|
105
|
+
} else {
|
|
106
|
+
toast.error(res.error || res.message || 'Failed to schedule');
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
toast.error(err instanceof Error ? err.message : 'Failed to schedule');
|
|
110
|
+
} finally {
|
|
111
|
+
submitting = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<Dialog.Root bind:open>
|
|
117
|
+
<Dialog.Content class="max-w-md">
|
|
118
|
+
<Dialog.Header>
|
|
119
|
+
<Dialog.Title>Schedule {actionVerb}</Dialog.Title>
|
|
120
|
+
<Dialog.Description>
|
|
121
|
+
Select when this document should be {action === 'publish' ? 'published' : 'unpublished'}.
|
|
122
|
+
</Dialog.Description>
|
|
123
|
+
</Dialog.Header>
|
|
124
|
+
|
|
125
|
+
<div class="space-y-4 py-2">
|
|
126
|
+
<div class="space-y-1.5">
|
|
127
|
+
<Label>Schedule on</Label>
|
|
128
|
+
<Popover.Root bind:open={pickerOpen}>
|
|
129
|
+
<Popover.Trigger
|
|
130
|
+
class="border-input bg-background hover:bg-muted/40 focus-visible:ring-ring flex w-full items-center justify-between rounded-md border px-3 py-2 text-left text-sm focus-visible:ring-1 focus-visible:outline-none"
|
|
131
|
+
>
|
|
132
|
+
<span>{fieldLabel}</span>
|
|
133
|
+
<CalendarIcon class="text-muted-foreground h-4 w-4" />
|
|
134
|
+
</Popover.Trigger>
|
|
135
|
+
<Popover.Content class="z-[70] w-[19rem] p-0" align="start">
|
|
136
|
+
<Calendar
|
|
137
|
+
type="single"
|
|
138
|
+
bind:value={dateValue}
|
|
139
|
+
minValue={minDate}
|
|
140
|
+
class="w-full rounded-b-none [--cell-size:2.4rem]"
|
|
141
|
+
/>
|
|
142
|
+
<div class="border-rule flex flex-wrap items-center gap-x-2 gap-y-1 border-t p-3">
|
|
143
|
+
<input
|
|
144
|
+
type="time"
|
|
145
|
+
bind:value={timeStr}
|
|
146
|
+
class="border-input bg-background focus-visible:ring-ring rounded-md border px-2 py-1 text-sm focus-visible:ring-1 focus-visible:outline-none"
|
|
147
|
+
/>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
|
|
151
|
+
onclick={setToCurrentTime}
|
|
152
|
+
>
|
|
153
|
+
Set to current time
|
|
154
|
+
</button>
|
|
155
|
+
<span class="text-muted-foreground ml-auto flex items-center gap-1 text-xs">
|
|
156
|
+
<Globe class="h-3 w-3" />
|
|
157
|
+
{tzLabel}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
</Popover.Content>
|
|
161
|
+
</Popover.Root>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<Dialog.Footer>
|
|
166
|
+
<Button variant="ghost" size="sm" onclick={() => (open = false)} disabled={submitting}>
|
|
167
|
+
Cancel
|
|
168
|
+
</Button>
|
|
169
|
+
<Button size="sm" onclick={submit} disabled={submitting}>
|
|
170
|
+
{submitting ? 'Scheduling…' : 'Schedule'}
|
|
171
|
+
</Button>
|
|
172
|
+
</Dialog.Footer>
|
|
173
|
+
</Dialog.Content>
|
|
174
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
open: boolean;
|
|
3
|
+
documentId: string;
|
|
4
|
+
/**
|
|
5
|
+
* The action to schedule. Derived from the editor's active perspective by the caller:
|
|
6
|
+
* viewing the Draft tab → 'publish' (send this draft live later); viewing the Published
|
|
7
|
+
* tab → 'unpublish' (take it down later). No in-dialog toggle — the tab is the intent.
|
|
8
|
+
*/
|
|
9
|
+
action: 'publish' | 'unpublish';
|
|
10
|
+
onScheduled?: (job: {
|
|
11
|
+
jobId: string;
|
|
12
|
+
type: string;
|
|
13
|
+
runAt: string;
|
|
14
|
+
status: string;
|
|
15
|
+
}) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const ScheduleDialog: import("svelte").Component<Props, {}, "open">;
|
|
18
|
+
type ScheduleDialog = ReturnType<typeof ScheduleDialog>;
|
|
19
|
+
export default ScheduleDialog;
|
|
20
|
+
//# sourceMappingURL=ScheduleDialog.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScheduleDialog.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/ScheduleDialog.svelte.ts"],"names":[],"mappings":"AAkBC,UAAU,KAAK;IACd,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,MAAM,EAAE,SAAS,GAAG,WAAW,CAAC;IAChC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC5F;AAwJF,QAAA,MAAM,cAAc,+CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
import { documents } from '../../../api/documents';
|
|
35
35
|
import { SvelteMap } from 'svelte/reactivity';
|
|
36
36
|
import { getDocumentVersion } from '../../../document-refresh.svelte';
|
|
37
|
-
import RichtextField from './richtext/RichtextField.svelte';
|
|
38
37
|
|
|
39
38
|
interface Props {
|
|
40
39
|
field: ArrayFieldType;
|
|
@@ -76,6 +75,11 @@
|
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
const isBlockArray = $derived(field.of.some((ref) => ref.type === 'block'));
|
|
78
|
+
// Rich text (TipTap) is by far the largest field dependency. Load it only when
|
|
79
|
+
// the array actually holds block content, so document editors without a
|
|
80
|
+
// rich-text field — and every admin page that never renders one — stay off
|
|
81
|
+
// that chunk. The import is cached, so a stable isBlockArray never refetches.
|
|
82
|
+
const richtextModule = $derived(isBlockArray ? import('./richtext/RichtextField.svelte') : null);
|
|
79
83
|
const isGridLayout = $derived(field.options?.layout === 'grid');
|
|
80
84
|
const isReferenceArray = $derived(
|
|
81
85
|
field.of && field.of.length > 0 && field.of[0]?.type === 'reference'
|
|
@@ -512,7 +516,11 @@
|
|
|
512
516
|
</script>
|
|
513
517
|
|
|
514
518
|
{#if isBlockArray}
|
|
515
|
-
|
|
519
|
+
{#if richtextModule}
|
|
520
|
+
{#await richtextModule then { default: RichtextField }}
|
|
521
|
+
<RichtextField {field} {value} {onUpdate} {readonly} {onOpenReference} {organizationId} />
|
|
522
|
+
{/await}
|
|
523
|
+
{/if}
|
|
516
524
|
{:else if isReferenceArray && referenceFieldShape}
|
|
517
525
|
<!-- Reference array — each row reuses ReferenceField. Drag handle on the
|
|
518
526
|
left, ReferenceField fills the row, an X button on empty rows lets the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArrayField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ArrayField.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAqB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"ArrayField.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/admin/fields/ArrayField.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,IAAI,cAAc,EAAqB,MAAM,wBAAwB,CAAC;AA+B7F,UAAU,KAAK;IACd,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,GAAG,CAAC;IACX,QAAQ,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAs7BF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
<SidebarMenuButton
|
|
112
112
|
{...props}
|
|
113
113
|
size="lg"
|
|
114
|
-
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
114
|
+
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
|
|
115
115
|
disabled={isSwitching}
|
|
116
116
|
>
|
|
117
117
|
{#if activeOrganization}
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
<img
|
|
120
120
|
src={getOrganizationLogo(activeOrganization)!}
|
|
121
121
|
alt={activeOrganization.name}
|
|
122
|
-
class="aspect-square size-8
|
|
122
|
+
class="aspect-square size-8 object-cover"
|
|
123
123
|
/>
|
|
124
124
|
{:else}
|
|
125
125
|
<div
|
|
@@ -275,7 +275,7 @@
|
|
|
275
275
|
{#if canCreateOrganization}
|
|
276
276
|
<DropdownMenuSeparator />
|
|
277
277
|
<DropdownMenuItem
|
|
278
|
-
class="gap-2 p-2"
|
|
278
|
+
class="cursor-pointer gap-2 p-2"
|
|
279
279
|
onclick={() => goto('/admin/organizations?action=create')}
|
|
280
280
|
>
|
|
281
281
|
<div class="flex size-6 items-center justify-center rounded-md border bg-transparent">
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<SidebarMenuButton
|
|
41
41
|
{...props}
|
|
42
42
|
size="lg"
|
|
43
|
-
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
43
|
+
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground cursor-pointer"
|
|
44
44
|
>
|
|
45
45
|
{#if user.image}
|
|
46
46
|
<img
|
|
@@ -76,22 +76,22 @@
|
|
|
76
76
|
<p class="text-muted-foreground text-xs">{user.email}</p>
|
|
77
77
|
</div>
|
|
78
78
|
<DropdownMenuSeparator />
|
|
79
|
-
<DropdownMenuItem onclick={() => goto('/admin/settings/account')}>
|
|
79
|
+
<DropdownMenuItem class="cursor-pointer" onclick={() => goto('/admin/settings/account')}>
|
|
80
80
|
<Settings class="mr-2 h-4 w-4" />
|
|
81
81
|
<span>Account Settings</span>
|
|
82
82
|
</DropdownMenuItem>
|
|
83
|
-
<DropdownMenuItem onclick={() => goto('/invitations')}>
|
|
83
|
+
<DropdownMenuItem class="cursor-pointer" onclick={() => goto('/invitations')}>
|
|
84
84
|
<Mail class="mr-2 h-4 w-4" />
|
|
85
85
|
<span>Invitations</span>
|
|
86
86
|
</DropdownMenuItem>
|
|
87
87
|
{#if user.role === 'super_admin'}
|
|
88
|
-
<DropdownMenuItem onclick={() => goto('/god-mode')}>
|
|
88
|
+
<DropdownMenuItem class="cursor-pointer" onclick={() => goto('/god-mode')}>
|
|
89
89
|
<Shield class="mr-2 h-4 w-4" />
|
|
90
90
|
<span>God Mode</span>
|
|
91
91
|
</DropdownMenuItem>
|
|
92
92
|
{/if}
|
|
93
93
|
<DropdownMenuSeparator />
|
|
94
|
-
<DropdownMenuItem class="text-destructive" onclick={handleSignOut}>
|
|
94
|
+
<DropdownMenuItem class="text-destructive cursor-pointer" onclick={handleSignOut}>
|
|
95
95
|
<LogOut class="mr-2 h-4 w-4" />
|
|
96
96
|
<span>Sign Out</span>
|
|
97
97
|
</DropdownMenuItem>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DomainEvent, AppendEventInput, Job, ScheduleJobInput, ClaimJobsOptions, ListEventsOptions, ListJobsOptions, OutboxRow, ListUnprocessedOutboxOptions, Page } from '../../types/events.js';
|
|
2
|
+
/**
|
|
3
|
+
* The durable spine: an append-only domain-event log plus a DB-backed job queue.
|
|
4
|
+
* All rows are organization-scoped. (The event-log read API — `listEvents` — lands
|
|
5
|
+
* with the admin history view in a later slice; there's no reader for it yet.)
|
|
6
|
+
*/
|
|
7
|
+
export interface EventJobAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* Append an immutable domain event AND its outbox worklist row, in one insert path.
|
|
10
|
+
* Call on the tx handle (inside `withTransaction`) so both commit atomically with the
|
|
11
|
+
* state change that caused them — the transactional-outbox guarantee. The outbox row is
|
|
12
|
+
* always written (even with no subscribers today): the relay decides fan-out at run time,
|
|
13
|
+
* so emission can't know whether anyone is listening.
|
|
14
|
+
*/
|
|
15
|
+
appendEvent(input: AppendEventInput): Promise<DomainEvent>;
|
|
16
|
+
/** Read a single event by id (org-scoped). */
|
|
17
|
+
getEvent(organizationId: string, id: string): Promise<DomainEvent | null>;
|
|
18
|
+
/** List domain events for the org, newest first (read-only history / observability). */
|
|
19
|
+
listEvents(options: ListEventsOptions): Promise<Page<DomainEvent>>;
|
|
20
|
+
/**
|
|
21
|
+
* Read pending outbox rows (`processedAt IS NULL`), oldest first — the relay's input.
|
|
22
|
+
* No claim/lease: fan-out is idempotent (per event×consumer job key), so concurrent
|
|
23
|
+
* relays are safe. Omit `organizationId` to read across all orgs (worker context).
|
|
24
|
+
*/
|
|
25
|
+
listUnprocessedOutbox(options: ListUnprocessedOutboxOptions): Promise<OutboxRow[]>;
|
|
26
|
+
/** Stamp `processedAt` once an outbox row has been fanned out to every subscriber. */
|
|
27
|
+
markOutboxProcessed(organizationId: string, id: string): Promise<void>;
|
|
28
|
+
/** Schedule a job. Idempotent when `idempotencyKey` is set: a duplicate key returns the existing job. */
|
|
29
|
+
scheduleJob(input: ScheduleJobInput): Promise<Job>;
|
|
30
|
+
/**
|
|
31
|
+
* Atomically claim up to `limit` due jobs with a lease, incrementing `attempts`.
|
|
32
|
+
* Claims jobs that are `pending` and due, or `leased` with an expired lease (crash recovery).
|
|
33
|
+
*/
|
|
34
|
+
claimDueJobs(options: ClaimJobsOptions): Promise<Job[]>;
|
|
35
|
+
/** Mark a claimed job completed. */
|
|
36
|
+
completeJob(organizationId: string, id: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Reschedule a claimed job for a later retry (backoff). Status → `pending`, `runAt`
|
|
39
|
+
* moves forward, the error is recorded, the lease is cleared. The backoff/max-attempts
|
|
40
|
+
* *policy* lives in the core runner; this is just the state transition.
|
|
41
|
+
*/
|
|
42
|
+
retryJob(organizationId: string, id: string, options: {
|
|
43
|
+
runAt: Date;
|
|
44
|
+
error: string;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
/** Dead-letter a claimed job: status → `failed`, record the error, clear the lease. */
|
|
47
|
+
failJob(organizationId: string, id: string, options: {
|
|
48
|
+
error: string;
|
|
49
|
+
}): Promise<void>;
|
|
50
|
+
/** Cancel a job (e.g. a scheduled publish the user called off): status → `cancelled`, clear the lease. */
|
|
51
|
+
cancelJob(organizationId: string, id: string): Promise<void>;
|
|
52
|
+
/** List jobs for the org, newest first (read-only history / observability). */
|
|
53
|
+
listJobs(options: ListJobsOptions): Promise<Page<Job>>;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/lib/db/interfaces/events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,WAAW,EACX,gBAAgB,EAChB,GAAG,EACH,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,4BAA4B,EAC5B,IAAI,EACJ,MAAM,oBAAoB,CAAC;AAE5B;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAE/B;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3D,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,wFAAwF;IACxF,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAGnE;;;;OAIG;IACH,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnF,sFAAsF;IACtF,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAGvE,yGAAyG;IACzG,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACxD,oCAAoC;IACpC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CACP,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACrC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,uFAAuF;IACvF,OAAO,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,0GAA0G;IAC1G,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;CACvD"}
|