@open-mercato/scheduler 0.6.5-develop.4788.1.a3e2520ec0 → 0.6.5-develop.4838.1.115785d8d7

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.
@@ -1,2 +1,2 @@
1
- [build:scheduler] found 50 entry points
1
+ [build:scheduler] found 51 entry points
2
2
  [build:scheduler] built successfully
@@ -0,0 +1,105 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
+ import { readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
4
+ import {
5
+ runCrudFormRoundTrip,
6
+ skipIfCrudFormExtensionTestsDisabled
7
+ } from "@open-mercato/core/helpers/integration/crudFormPersistence";
8
+ const SCHEDULER_JOBS_PATH = "/api/scheduler/jobs";
9
+ async function readScheduledJobById(request, token, id) {
10
+ const response = await apiRequest(
11
+ request,
12
+ "GET",
13
+ `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}&page=1&pageSize=100`,
14
+ { token }
15
+ );
16
+ expect(response.status(), `read-back scheduler jobs failed: ${response.status()}`).toBe(200);
17
+ const body = await readJsonSafe(response);
18
+ return (body?.items ?? []).find((item) => item.id === id) ?? null;
19
+ }
20
+ test.describe("TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON", () => {
21
+ test.beforeAll(() => {
22
+ skipIfCrudFormExtensionTestsDisabled();
23
+ });
24
+ test("round-trips scalars, scope, target + nested payload JSON on create and update", async ({ request }) => {
25
+ const token = await getAuthToken(request, "admin");
26
+ const stamp = Date.now();
27
+ const createPayload = {
28
+ source: "integration-test",
29
+ attempts: 3,
30
+ retries: { max: 5, backoff: "exponential" },
31
+ tags: ["alpha", "beta"]
32
+ };
33
+ const updatePayload = {
34
+ source: "integration-test-edited",
35
+ attempts: 1,
36
+ retries: { max: 0, backoff: "fixed" },
37
+ tags: ["gamma"]
38
+ };
39
+ await runCrudFormRoundTrip({
40
+ request,
41
+ token,
42
+ collectionPath: SCHEDULER_JOBS_PATH,
43
+ readById: (id) => readScheduledJobById(request, token, id),
44
+ create: {
45
+ payload: {
46
+ name: `QA CRUDFORM Scheduled Job ${stamp}`,
47
+ description: "Original scheduled job description",
48
+ scopeType: "organization",
49
+ scheduleType: "interval",
50
+ scheduleValue: "15m",
51
+ timezone: "UTC",
52
+ targetType: "queue",
53
+ targetQueue: "scheduler-execution",
54
+ targetPayload: createPayload,
55
+ isEnabled: true,
56
+ sourceType: "user"
57
+ }
58
+ },
59
+ expectAfterCreate: {
60
+ scalars: {
61
+ name: `QA CRUDFORM Scheduled Job ${stamp}`,
62
+ description: "Original scheduled job description",
63
+ scopeType: "organization",
64
+ scheduleType: "interval",
65
+ scheduleValue: "15m",
66
+ timezone: "UTC",
67
+ targetType: "queue",
68
+ targetQueue: "scheduler-execution",
69
+ targetCommand: null,
70
+ targetPayload: createPayload,
71
+ isEnabled: true,
72
+ sourceType: "user"
73
+ }
74
+ },
75
+ update: {
76
+ payload: (id) => ({
77
+ id,
78
+ name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,
79
+ description: "Updated scheduled job description",
80
+ scheduleType: "cron",
81
+ scheduleValue: "0 0 * * *",
82
+ timezone: "Europe/Warsaw",
83
+ targetPayload: updatePayload,
84
+ isEnabled: false
85
+ })
86
+ },
87
+ expectAfterUpdate: {
88
+ scalars: {
89
+ name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,
90
+ description: "Updated scheduled job description",
91
+ scopeType: "organization",
92
+ scheduleType: "cron",
93
+ scheduleValue: "0 0 * * *",
94
+ timezone: "Europe/Warsaw",
95
+ targetType: "queue",
96
+ targetQueue: "scheduler-execution",
97
+ targetCommand: null,
98
+ targetPayload: updatePayload,
99
+ isEnabled: false
100
+ }
101
+ }
102
+ });
103
+ });
104
+ });
105
+ //# sourceMappingURL=TC-SCHED-CRUDFORM-001.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts"],
4
+ "sourcesContent": ["import { expect, test, type APIRequestContext } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n runCrudFormRoundTrip,\n skipIfCrudFormExtensionTestsDisabled,\n type CrudRecord,\n} from '@open-mercato/core/helpers/integration/crudFormPersistence';\n\n/**\n * TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON (#2466).\n *\n * The scheduler's scheduled-job is the Tier-B \"JSON-bearing scalar\" surface: scope\n * (scopeType, server-derived org/tenant), schedule (scheduleType/scheduleValue/timezone),\n * target (targetType/targetQueue), and a nested `targetPayload` JSON object. Proves create +\n * update round-trip every value.\n *\n * Verified contract (differs from the makeCrud-default reference specs):\n * - The list GET filters by `?id=` (single uuid), not the `?ids=` other sweep modules use; the\n * explicit `readById` documents that contract and mirrors the sibling specs (the shared\n * harness default reads back via `?id=` too).\n * - Read-back is camelCase: the list `transformItem` maps every column to camelCase\n * (`scopeType`, `scheduleType`, `targetPayload`, `isEnabled`, ...), so the scalar\n * expectations use camelCase keys (not the snake_case of resources/staff).\n * - The scheduled-job declares no custom entity (no `ce.ts`), so there are no `cf_*` fields \u2014\n * the surface is scalars + nested JSON only.\n * - Scope is derived server-side from the caller's auth context and is immutable on update:\n * `scopeType: 'organization'` fills org+tenant from the admin token and survives the edit.\n * - Update is a partial PUT: omitted target fields are retained, so the update changes the\n * payload JSON while proving `targetType`/`targetQueue` survive untouched.\n * - Harness cleanup deletes via `?id=`; the scheduler delete resolves the id from the query\n * string, so the default `finally` cleanup removes the fixture.\n *\n * Self-contained: the job needs no pre-created fixtures (queue target + inline JSON), and the\n * harness deletes it in `finally`.\n *\n * Gated by `OM_INTEGRATION_CRUDFORM_EXTENSION_TESTS_DISABLED` (default off \u2192 runs).\n */\nconst SCHEDULER_JOBS_PATH = '/api/scheduler/jobs';\n\nasync function readScheduledJobById(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<CrudRecord | null> {\n const response = await apiRequest(\n request,\n 'GET',\n `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}&page=1&pageSize=100`,\n { token },\n );\n expect(response.status(), `read-back scheduler jobs failed: ${response.status()}`).toBe(200);\n const body = await readJsonSafe<{ items?: CrudRecord[] }>(response);\n return (body?.items ?? []).find((item) => item.id === id) ?? null;\n}\n\ntest.describe('TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON', () => {\n test.beforeAll(() => {\n skipIfCrudFormExtensionTestsDisabled();\n });\n\n test('round-trips scalars, scope, target + nested payload JSON on create and update', async ({ request }) => {\n const token = await getAuthToken(request, 'admin');\n const stamp = Date.now();\n\n const createPayload = {\n source: 'integration-test',\n attempts: 3,\n retries: { max: 5, backoff: 'exponential' },\n tags: ['alpha', 'beta'],\n };\n const updatePayload = {\n source: 'integration-test-edited',\n attempts: 1,\n retries: { max: 0, backoff: 'fixed' },\n tags: ['gamma'],\n };\n\n await runCrudFormRoundTrip({\n request,\n token,\n collectionPath: SCHEDULER_JOBS_PATH,\n readById: (id) => readScheduledJobById(request, token, id),\n create: {\n payload: {\n name: `QA CRUDFORM Scheduled Job ${stamp}`,\n description: 'Original scheduled job description',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetPayload: createPayload,\n isEnabled: true,\n sourceType: 'user',\n },\n },\n expectAfterCreate: {\n scalars: {\n name: `QA CRUDFORM Scheduled Job ${stamp}`,\n description: 'Original scheduled job description',\n scopeType: 'organization',\n scheduleType: 'interval',\n scheduleValue: '15m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetCommand: null,\n targetPayload: createPayload,\n isEnabled: true,\n sourceType: 'user',\n },\n },\n update: {\n payload: (id) => ({\n id,\n name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,\n description: 'Updated scheduled job description',\n scheduleType: 'cron',\n scheduleValue: '0 0 * * *',\n timezone: 'Europe/Warsaw',\n targetPayload: updatePayload,\n isEnabled: false,\n }),\n },\n expectAfterUpdate: {\n scalars: {\n name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,\n description: 'Updated scheduled job description',\n scopeType: 'organization',\n scheduleType: 'cron',\n scheduleValue: '0 0 * * *',\n timezone: 'Europe/Warsaw',\n targetType: 'queue',\n targetQueue: 'scheduler-execution',\n targetCommand: null,\n targetPayload: updatePayload,\n isEnabled: false,\n },\n },\n });\n });\n});\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,YAAoC;AACrD,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AA+BP,MAAM,sBAAsB;AAE5B,eAAe,qBACb,SACA,OACA,IAC4B;AAC5B,QAAM,WAAW,MAAM;AAAA,IACrB;AAAA,IACA;AAAA,IACA,GAAG,mBAAmB,OAAO,mBAAmB,EAAE,CAAC;AAAA,IACnD,EAAE,MAAM;AAAA,EACV;AACA,SAAO,SAAS,OAAO,GAAG,oCAAoC,SAAS,OAAO,CAAC,EAAE,EAAE,KAAK,GAAG;AAC3F,QAAM,OAAO,MAAM,aAAuC,QAAQ;AAClE,UAAQ,MAAM,SAAS,CAAC,GAAG,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,KAAK;AAC/D;AAEA,KAAK,SAAS,uFAAuF,MAAM;AACzG,OAAK,UAAU,MAAM;AACnB,yCAAqC;AAAA,EACvC,CAAC;AAED,OAAK,iFAAiF,OAAO,EAAE,QAAQ,MAAM;AAC3G,UAAM,QAAQ,MAAM,aAAa,SAAS,OAAO;AACjD,UAAM,QAAQ,KAAK,IAAI;AAEvB,UAAM,gBAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,EAAE,KAAK,GAAG,SAAS,cAAc;AAAA,MAC1C,MAAM,CAAC,SAAS,MAAM;AAAA,IACxB;AACA,UAAM,gBAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,SAAS,EAAE,KAAK,GAAG,SAAS,QAAQ;AAAA,MACpC,MAAM,CAAC,OAAO;AAAA,IAChB;AAEA,UAAM,qBAAqB;AAAA,MACzB;AAAA,MACA;AAAA,MACA,gBAAgB;AAAA,MAChB,UAAU,CAAC,OAAO,qBAAqB,SAAS,OAAO,EAAE;AAAA,MACzD,QAAQ;AAAA,QACN,SAAS;AAAA,UACP,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,WAAW;AAAA,UACX,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,eAAe;AAAA,UACf,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB,SAAS;AAAA,UACP,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,WAAW;AAAA,UACX,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,eAAe;AAAA,UACf,eAAe;AAAA,UACf,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,SAAS,CAAC,QAAQ;AAAA,UAChB;AAAA,UACA,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,eAAe;AAAA,UACf,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,mBAAmB;AAAA,QACjB,SAAS;AAAA,UACP,MAAM,6BAA6B,KAAK;AAAA,UACxC,aAAa;AAAA,UACb,WAAW;AAAA,UACX,cAAc;AAAA,UACd,eAAe;AAAA,UACf,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,aAAa;AAAA,UACb,eAAe;AAAA,UACf,eAAe;AAAA,UACf,WAAW;AAAA,QACb;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH,CAAC;",
6
+ "names": []
7
+ }
@@ -13,6 +13,7 @@ import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
13
13
  import { useT } from "@open-mercato/shared/lib/i18n/context";
14
14
  import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
15
15
  import { formatDateTime } from "@open-mercato/shared/lib/time";
16
+ import { ListEmptyState } from "@open-mercato/ui/backend/filters/ListEmptyState";
16
17
  function mapApiItem(item) {
17
18
  const id = typeof item.id === "string" ? item.id : null;
18
19
  if (!id) return null;
@@ -138,7 +139,7 @@ function SchedulerPage() {
138
139
  cell: ({ row }) => {
139
140
  const target = row.original.targetType === "queue" ? row.original.targetQueue : row.original.targetCommand;
140
141
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
141
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-500 capitalize", children: row.original.targetType }),
142
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground capitalize", children: row.original.targetType }),
142
143
  /* @__PURE__ */ jsx("span", { className: "text-sm", children: target || "-" })
143
144
  ] });
144
145
  }
@@ -203,6 +204,14 @@ function SchedulerPage() {
203
204
  data: rows,
204
205
  onRowClick: (row) => router.push(`/backend/config/scheduled-jobs/${row.id}`),
205
206
  rowActions: (row) => /* @__PURE__ */ jsx(RowActions, { items: rowActions(row) }),
207
+ emptyState: /* @__PURE__ */ jsx(
208
+ ListEmptyState,
209
+ {
210
+ entityName: t("scheduler.title", "Scheduled Jobs"),
211
+ createHref: "/backend/config/scheduled-jobs/new",
212
+ createLabel: t("scheduler.action.create", "New Schedule")
213
+ }
214
+ ),
206
215
  pagination: { page, pageSize, total, totalPages, onPageChange: setPage },
207
216
  isLoading,
208
217
  searchValue: search,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/scheduler/backend/config/scheduled-jobs/page.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\n\ntype ScheduleRow = {\n id: string\n name: string\n description?: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue?: string | null\n targetCommand?: string | null\n isEnabled: boolean\n sourceType: 'user' | 'module'\n sourceModule?: string | null\n nextRunAt?: string | null\n lastRunAt?: string | null\n}\n\ntype SchedulesResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n page?: number\n totalPages?: number\n}\n\nfunction mapApiItem(item: Record<string, unknown>): ScheduleRow | null {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n\n return {\n id,\n name: typeof item.name === 'string' ? item.name : '',\n description: typeof item.description === 'string' ? item.description : null,\n scopeType: (item.scopeType === 'system' || item.scopeType === 'organization' || item.scopeType === 'tenant')\n ? item.scopeType\n : 'tenant',\n scheduleType: (item.scheduleType === 'cron' || item.scheduleType === 'interval')\n ? item.scheduleType\n : 'cron',\n scheduleValue: typeof item.scheduleValue === 'string' ? item.scheduleValue : '',\n timezone: typeof item.timezone === 'string' ? item.timezone : 'UTC',\n targetType: (item.targetType === 'queue' || item.targetType === 'command')\n ? item.targetType\n : 'queue',\n targetQueue: typeof item.targetQueue === 'string' ? item.targetQueue : null,\n targetCommand: typeof item.targetCommand === 'string' ? item.targetCommand : null,\n isEnabled: item.isEnabled === true,\n sourceType: (item.sourceType === 'user' || item.sourceType === 'module')\n ? item.sourceType\n : 'user',\n sourceModule: typeof item.sourceModule === 'string' ? item.sourceModule : null,\n nextRunAt: typeof item.nextRunAt === 'string' ? item.nextRunAt : null,\n lastRunAt: typeof item.lastRunAt === 'string' ? item.lastRunAt : null,\n }\n}\n\n\nexport default function SchedulerPage() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const router = useRouter()\n const [rows, setRows] = React.useState<ScheduleRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [isLoading, setIsLoading] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n\n const fetchSchedules = React.useCallback(async () => {\n setIsLoading(true)\n try {\n const params = new URLSearchParams({\n page: page.toString(),\n pageSize: pageSize.toString(),\n })\n if (search) params.set('search', search)\n\n const { result } = await apiCallOrThrow<SchedulesResponse>(\n `/api/scheduler/jobs?${params.toString()}`\n )\n\n const items = result?.items ?? []\n const mapped = items.map(mapApiItem).filter((x): x is ScheduleRow => x !== null)\n setRows(mapped)\n setTotal(result?.total ?? 0)\n setTotalPages(result?.totalPages ?? 1)\n } catch (error) {\n flash(t('scheduler.error.fetch_failed', 'Failed to load schedules'), 'error')\n setRows([])\n setTotal(0)\n setTotalPages(1)\n } finally {\n setIsLoading(false)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [page, pageSize, search, scopeVersion, t])\n\n React.useEffect(() => {\n fetchSchedules()\n }, [fetchSchedules])\n\n const handleDelete = React.useCallback(\n async (row: ScheduleRow) => {\n const confirmed = await confirm({\n title: t('scheduler.confirm.delete', 'Are you sure you want to delete this schedule?'),\n variant: 'destructive',\n })\n if (!confirmed) {\n return\n }\n\n try {\n await apiCallOrThrow(`/api/scheduler/jobs`, {\n method: 'DELETE',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.deleted', 'Schedule deleted successfully'), 'success')\n fetchSchedules()\n } catch (error) {\n flash(t('scheduler.error.delete_failed', 'Failed to delete schedule'), 'error')\n }\n },\n [confirm, t, fetchSchedules]\n )\n\n const handleTrigger = React.useCallback(\n async (row: ScheduleRow) => {\n try {\n await apiCallOrThrow(`/api/scheduler/trigger`, {\n method: 'POST',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.triggered', 'Schedule triggered successfully'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('scheduler.error.trigger_failed', 'Failed to trigger schedule')\n flash(message, 'error')\n }\n },\n [t]\n )\n\n const columns = React.useMemo<ColumnDef<ScheduleRow>[]>(\n () => [\n {\n id: 'name',\n accessorKey: 'name',\n header: t('scheduler.field.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"font-medium\">{row.original.name}</span>\n ),\n },\n {\n id: 'scheduleType',\n accessorKey: 'scheduleType',\n header: t('scheduler.field.schedule_type', 'Type'),\n cell: ({ row }) => (\n <span className=\"capitalize\">{row.original.scheduleType}</span>\n ),\n },\n {\n id: 'scheduleValue',\n accessorKey: 'scheduleValue',\n header: t('scheduler.field.schedule', 'Schedule'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.scheduleValue}\n </span>\n ),\n },\n {\n id: 'targetType',\n accessorKey: 'targetType',\n header: t('scheduler.field.target', 'Target'),\n cell: ({ row }) => {\n const target = row.original.targetType === 'queue'\n ? row.original.targetQueue\n : row.original.targetCommand\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-xs text-gray-500 capitalize\">{row.original.targetType}</span>\n <span className=\"text-sm\">{target || '-'}</span>\n </div>\n )\n },\n },\n {\n id: 'nextRunAt',\n accessorKey: 'nextRunAt',\n header: t('scheduler.field.next_run', 'Next Run'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {formatDateTime(row.original.nextRunAt) || '-'}\n </span>\n ),\n },\n {\n id: 'isEnabled',\n accessorKey: 'isEnabled',\n header: t('scheduler.field.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isEnabled} />,\n },\n {\n id: 'sourceType',\n accessorKey: 'sourceType',\n header: t('scheduler.field.source', 'Source'),\n cell: ({ row }) => (\n <span className=\"text-xs capitalize\">\n {row.original.sourceType}\n {row.original.sourceModule && ` (${row.original.sourceModule})`}\n </span>\n ),\n },\n ],\n [t]\n )\n\n const rowActions = React.useCallback(\n (row: ScheduleRow): RowActionItem[] => [\n {\n id: 'view',\n label: t('scheduler.action.view', 'View Details'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}`),\n },\n {\n id: 'edit',\n label: t('scheduler.action.edit', 'Edit'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}/edit`),\n },\n {\n id: 'trigger',\n label: t('scheduler.action.trigger', 'Run Now'),\n onSelect: () => handleTrigger(row),\n },\n {\n id: 'delete',\n label: t('scheduler.action.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ],\n [t, router, handleDelete, handleTrigger]\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable<ScheduleRow>\n title={t('scheduler.title', 'Scheduled Jobs')}\n actions={\n <Button onClick={() => router.push('/backend/config/scheduled-jobs/new')}>\n {t('scheduler.action.create', 'New Schedule')}\n </Button>\n }\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/config/scheduled-jobs/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActions(row)} />}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('scheduler.search.placeholder', 'Search schedules...')}\n refreshButton={{\n label: t('scheduler.action.refresh', 'Refresh'),\n onRefresh: fetchSchedules,\n }}\n />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { formatDateTime } from '@open-mercato/shared/lib/time'\nimport { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'\n\ntype ScheduleRow = {\n id: string\n name: string\n description?: string | null\n scopeType: 'system' | 'organization' | 'tenant'\n scheduleType: 'cron' | 'interval'\n scheduleValue: string\n timezone: string\n targetType: 'queue' | 'command'\n targetQueue?: string | null\n targetCommand?: string | null\n isEnabled: boolean\n sourceType: 'user' | 'module'\n sourceModule?: string | null\n nextRunAt?: string | null\n lastRunAt?: string | null\n}\n\ntype SchedulesResponse = {\n items?: Array<Record<string, unknown>>\n total?: number\n page?: number\n totalPages?: number\n}\n\nfunction mapApiItem(item: Record<string, unknown>): ScheduleRow | null {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n\n return {\n id,\n name: typeof item.name === 'string' ? item.name : '',\n description: typeof item.description === 'string' ? item.description : null,\n scopeType: (item.scopeType === 'system' || item.scopeType === 'organization' || item.scopeType === 'tenant')\n ? item.scopeType\n : 'tenant',\n scheduleType: (item.scheduleType === 'cron' || item.scheduleType === 'interval')\n ? item.scheduleType\n : 'cron',\n scheduleValue: typeof item.scheduleValue === 'string' ? item.scheduleValue : '',\n timezone: typeof item.timezone === 'string' ? item.timezone : 'UTC',\n targetType: (item.targetType === 'queue' || item.targetType === 'command')\n ? item.targetType\n : 'queue',\n targetQueue: typeof item.targetQueue === 'string' ? item.targetQueue : null,\n targetCommand: typeof item.targetCommand === 'string' ? item.targetCommand : null,\n isEnabled: item.isEnabled === true,\n sourceType: (item.sourceType === 'user' || item.sourceType === 'module')\n ? item.sourceType\n : 'user',\n sourceModule: typeof item.sourceModule === 'string' ? item.sourceModule : null,\n nextRunAt: typeof item.nextRunAt === 'string' ? item.nextRunAt : null,\n lastRunAt: typeof item.lastRunAt === 'string' ? item.lastRunAt : null,\n }\n}\n\n\nexport default function SchedulerPage() {\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const router = useRouter()\n const [rows, setRows] = React.useState<ScheduleRow[]>([])\n const [page, setPage] = React.useState(1)\n const [pageSize] = React.useState(20)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const [isLoading, setIsLoading] = React.useState(false)\n const scopeVersion = useOrganizationScopeVersion()\n\n const fetchSchedules = React.useCallback(async () => {\n setIsLoading(true)\n try {\n const params = new URLSearchParams({\n page: page.toString(),\n pageSize: pageSize.toString(),\n })\n if (search) params.set('search', search)\n\n const { result } = await apiCallOrThrow<SchedulesResponse>(\n `/api/scheduler/jobs?${params.toString()}`\n )\n\n const items = result?.items ?? []\n const mapped = items.map(mapApiItem).filter((x): x is ScheduleRow => x !== null)\n setRows(mapped)\n setTotal(result?.total ?? 0)\n setTotalPages(result?.totalPages ?? 1)\n } catch (error) {\n flash(t('scheduler.error.fetch_failed', 'Failed to load schedules'), 'error')\n setRows([])\n setTotal(0)\n setTotalPages(1)\n } finally {\n setIsLoading(false)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [page, pageSize, search, scopeVersion, t])\n\n React.useEffect(() => {\n fetchSchedules()\n }, [fetchSchedules])\n\n const handleDelete = React.useCallback(\n async (row: ScheduleRow) => {\n const confirmed = await confirm({\n title: t('scheduler.confirm.delete', 'Are you sure you want to delete this schedule?'),\n variant: 'destructive',\n })\n if (!confirmed) {\n return\n }\n\n try {\n await apiCallOrThrow(`/api/scheduler/jobs`, {\n method: 'DELETE',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.deleted', 'Schedule deleted successfully'), 'success')\n fetchSchedules()\n } catch (error) {\n flash(t('scheduler.error.delete_failed', 'Failed to delete schedule'), 'error')\n }\n },\n [confirm, t, fetchSchedules]\n )\n\n const handleTrigger = React.useCallback(\n async (row: ScheduleRow) => {\n try {\n await apiCallOrThrow(`/api/scheduler/trigger`, {\n method: 'POST',\n body: JSON.stringify({ id: row.id }),\n })\n flash(t('scheduler.success.triggered', 'Schedule triggered successfully'), 'success')\n } catch (error) {\n const message = error instanceof Error ? error.message : t('scheduler.error.trigger_failed', 'Failed to trigger schedule')\n flash(message, 'error')\n }\n },\n [t]\n )\n\n const columns = React.useMemo<ColumnDef<ScheduleRow>[]>(\n () => [\n {\n id: 'name',\n accessorKey: 'name',\n header: t('scheduler.field.name', 'Name'),\n cell: ({ row }) => (\n <span className=\"font-medium\">{row.original.name}</span>\n ),\n },\n {\n id: 'scheduleType',\n accessorKey: 'scheduleType',\n header: t('scheduler.field.schedule_type', 'Type'),\n cell: ({ row }) => (\n <span className=\"capitalize\">{row.original.scheduleType}</span>\n ),\n },\n {\n id: 'scheduleValue',\n accessorKey: 'scheduleValue',\n header: t('scheduler.field.schedule', 'Schedule'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {row.original.scheduleValue}\n </span>\n ),\n },\n {\n id: 'targetType',\n accessorKey: 'targetType',\n header: t('scheduler.field.target', 'Target'),\n cell: ({ row }) => {\n const target = row.original.targetType === 'queue'\n ? row.original.targetQueue\n : row.original.targetCommand\n return (\n <div className=\"flex flex-col\">\n <span className=\"text-xs text-muted-foreground capitalize\">{row.original.targetType}</span>\n <span className=\"text-sm\">{target || '-'}</span>\n </div>\n )\n },\n },\n {\n id: 'nextRunAt',\n accessorKey: 'nextRunAt',\n header: t('scheduler.field.next_run', 'Next Run'),\n cell: ({ row }) => (\n <span className=\"text-sm\">\n {formatDateTime(row.original.nextRunAt) || '-'}\n </span>\n ),\n },\n {\n id: 'isEnabled',\n accessorKey: 'isEnabled',\n header: t('scheduler.field.active', 'Active'),\n cell: ({ row }) => <BooleanIcon value={row.original.isEnabled} />,\n },\n {\n id: 'sourceType',\n accessorKey: 'sourceType',\n header: t('scheduler.field.source', 'Source'),\n cell: ({ row }) => (\n <span className=\"text-xs capitalize\">\n {row.original.sourceType}\n {row.original.sourceModule && ` (${row.original.sourceModule})`}\n </span>\n ),\n },\n ],\n [t]\n )\n\n const rowActions = React.useCallback(\n (row: ScheduleRow): RowActionItem[] => [\n {\n id: 'view',\n label: t('scheduler.action.view', 'View Details'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}`),\n },\n {\n id: 'edit',\n label: t('scheduler.action.edit', 'Edit'),\n onSelect: () => router.push(`/backend/config/scheduled-jobs/${row.id}/edit`),\n },\n {\n id: 'trigger',\n label: t('scheduler.action.trigger', 'Run Now'),\n onSelect: () => handleTrigger(row),\n },\n {\n id: 'delete',\n label: t('scheduler.action.delete', 'Delete'),\n onSelect: () => handleDelete(row),\n destructive: true,\n },\n ],\n [t, router, handleDelete, handleTrigger]\n )\n\n return (\n <Page>\n <PageBody>\n <DataTable<ScheduleRow>\n title={t('scheduler.title', 'Scheduled Jobs')}\n actions={\n <Button onClick={() => router.push('/backend/config/scheduled-jobs/new')}>\n {t('scheduler.action.create', 'New Schedule')}\n </Button>\n }\n columns={columns}\n data={rows}\n onRowClick={(row) => router.push(`/backend/config/scheduled-jobs/${row.id}`)}\n rowActions={(row) => <RowActions items={rowActions(row)} />}\n emptyState={(\n <ListEmptyState\n entityName={t('scheduler.title', 'Scheduled Jobs')}\n createHref=\"/backend/config/scheduled-jobs/new\"\n createLabel={t('scheduler.action.create', 'New Schedule')}\n />\n )}\n pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}\n isLoading={isLoading}\n searchValue={search}\n onSearchChange={(value) => { setSearch(value); setPage(1) }}\n searchPlaceholder={t('scheduler.search.placeholder', 'Search schedules...')}\n refreshButton={{\n label: t('scheduler.action.refresh', 'Refresh'),\n onRefresh: fetchSchedules,\n }}\n />\n {ConfirmDialogElement}\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/scheduler",
3
- "version": "0.6.5-develop.4788.1.a3e2520ec0",
3
+ "version": "0.6.5-develop.4838.1.115785d8d7",
4
4
  "description": "Database-managed scheduled jobs with admin UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,13 +58,13 @@
58
58
  "./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json"
59
59
  },
60
60
  "dependencies": {
61
- "@open-mercato/events": "0.6.5-develop.4788.1.a3e2520ec0",
62
- "@open-mercato/queue": "0.6.5-develop.4788.1.a3e2520ec0",
61
+ "@open-mercato/events": "0.6.5-develop.4838.1.115785d8d7",
62
+ "@open-mercato/queue": "0.6.5-develop.4838.1.115785d8d7",
63
63
  "cron-parser": "^5.5.0"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@mikro-orm/core": "^7.0.14",
67
- "@open-mercato/shared": "0.6.5-develop.4788.1.a3e2520ec0",
67
+ "@open-mercato/shared": "0.6.5-develop.4838.1.115785d8d7",
68
68
  "bullmq": "^5.0.0",
69
69
  "ioredis": "^5.0.0",
70
70
  "zod": ">=3.23.0"
@@ -78,7 +78,7 @@
78
78
  }
79
79
  },
80
80
  "devDependencies": {
81
- "@open-mercato/shared": "0.6.5-develop.4788.1.a3e2520ec0",
81
+ "@open-mercato/shared": "0.6.5-develop.4838.1.115785d8d7",
82
82
  "@types/jest": "^30.0.0",
83
83
  "@types/node": "^25.9.1",
84
84
  "jest": "^30.4.2",
@@ -0,0 +1,144 @@
1
+ import { expect, test, type APIRequestContext } from '@playwright/test';
2
+ import { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';
3
+ import { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';
4
+ import {
5
+ runCrudFormRoundTrip,
6
+ skipIfCrudFormExtensionTestsDisabled,
7
+ type CrudRecord,
8
+ } from '@open-mercato/core/helpers/integration/crudFormPersistence';
9
+
10
+ /**
11
+ * TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON (#2466).
12
+ *
13
+ * The scheduler's scheduled-job is the Tier-B "JSON-bearing scalar" surface: scope
14
+ * (scopeType, server-derived org/tenant), schedule (scheduleType/scheduleValue/timezone),
15
+ * target (targetType/targetQueue), and a nested `targetPayload` JSON object. Proves create +
16
+ * update round-trip every value.
17
+ *
18
+ * Verified contract (differs from the makeCrud-default reference specs):
19
+ * - The list GET filters by `?id=` (single uuid), not the `?ids=` other sweep modules use; the
20
+ * explicit `readById` documents that contract and mirrors the sibling specs (the shared
21
+ * harness default reads back via `?id=` too).
22
+ * - Read-back is camelCase: the list `transformItem` maps every column to camelCase
23
+ * (`scopeType`, `scheduleType`, `targetPayload`, `isEnabled`, ...), so the scalar
24
+ * expectations use camelCase keys (not the snake_case of resources/staff).
25
+ * - The scheduled-job declares no custom entity (no `ce.ts`), so there are no `cf_*` fields —
26
+ * the surface is scalars + nested JSON only.
27
+ * - Scope is derived server-side from the caller's auth context and is immutable on update:
28
+ * `scopeType: 'organization'` fills org+tenant from the admin token and survives the edit.
29
+ * - Update is a partial PUT: omitted target fields are retained, so the update changes the
30
+ * payload JSON while proving `targetType`/`targetQueue` survive untouched.
31
+ * - Harness cleanup deletes via `?id=`; the scheduler delete resolves the id from the query
32
+ * string, so the default `finally` cleanup removes the fixture.
33
+ *
34
+ * Self-contained: the job needs no pre-created fixtures (queue target + inline JSON), and the
35
+ * harness deletes it in `finally`.
36
+ *
37
+ * Gated by `OM_INTEGRATION_CRUDFORM_EXTENSION_TESTS_DISABLED` (default off → runs).
38
+ */
39
+ const SCHEDULER_JOBS_PATH = '/api/scheduler/jobs';
40
+
41
+ async function readScheduledJobById(
42
+ request: APIRequestContext,
43
+ token: string,
44
+ id: string,
45
+ ): Promise<CrudRecord | null> {
46
+ const response = await apiRequest(
47
+ request,
48
+ 'GET',
49
+ `${SCHEDULER_JOBS_PATH}?id=${encodeURIComponent(id)}&page=1&pageSize=100`,
50
+ { token },
51
+ );
52
+ expect(response.status(), `read-back scheduler jobs failed: ${response.status()}`).toBe(200);
53
+ const body = await readJsonSafe<{ items?: CrudRecord[] }>(response);
54
+ return (body?.items ?? []).find((item) => item.id === id) ?? null;
55
+ }
56
+
57
+ test.describe('TC-SCHED-CRUDFORM-001: Scheduled Job CrudForm persists scope, target + payload JSON', () => {
58
+ test.beforeAll(() => {
59
+ skipIfCrudFormExtensionTestsDisabled();
60
+ });
61
+
62
+ test('round-trips scalars, scope, target + nested payload JSON on create and update', async ({ request }) => {
63
+ const token = await getAuthToken(request, 'admin');
64
+ const stamp = Date.now();
65
+
66
+ const createPayload = {
67
+ source: 'integration-test',
68
+ attempts: 3,
69
+ retries: { max: 5, backoff: 'exponential' },
70
+ tags: ['alpha', 'beta'],
71
+ };
72
+ const updatePayload = {
73
+ source: 'integration-test-edited',
74
+ attempts: 1,
75
+ retries: { max: 0, backoff: 'fixed' },
76
+ tags: ['gamma'],
77
+ };
78
+
79
+ await runCrudFormRoundTrip({
80
+ request,
81
+ token,
82
+ collectionPath: SCHEDULER_JOBS_PATH,
83
+ readById: (id) => readScheduledJobById(request, token, id),
84
+ create: {
85
+ payload: {
86
+ name: `QA CRUDFORM Scheduled Job ${stamp}`,
87
+ description: 'Original scheduled job description',
88
+ scopeType: 'organization',
89
+ scheduleType: 'interval',
90
+ scheduleValue: '15m',
91
+ timezone: 'UTC',
92
+ targetType: 'queue',
93
+ targetQueue: 'scheduler-execution',
94
+ targetPayload: createPayload,
95
+ isEnabled: true,
96
+ sourceType: 'user',
97
+ },
98
+ },
99
+ expectAfterCreate: {
100
+ scalars: {
101
+ name: `QA CRUDFORM Scheduled Job ${stamp}`,
102
+ description: 'Original scheduled job description',
103
+ scopeType: 'organization',
104
+ scheduleType: 'interval',
105
+ scheduleValue: '15m',
106
+ timezone: 'UTC',
107
+ targetType: 'queue',
108
+ targetQueue: 'scheduler-execution',
109
+ targetCommand: null,
110
+ targetPayload: createPayload,
111
+ isEnabled: true,
112
+ sourceType: 'user',
113
+ },
114
+ },
115
+ update: {
116
+ payload: (id) => ({
117
+ id,
118
+ name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,
119
+ description: 'Updated scheduled job description',
120
+ scheduleType: 'cron',
121
+ scheduleValue: '0 0 * * *',
122
+ timezone: 'Europe/Warsaw',
123
+ targetPayload: updatePayload,
124
+ isEnabled: false,
125
+ }),
126
+ },
127
+ expectAfterUpdate: {
128
+ scalars: {
129
+ name: `QA CRUDFORM Scheduled Job ${stamp} EDITED`,
130
+ description: 'Updated scheduled job description',
131
+ scopeType: 'organization',
132
+ scheduleType: 'cron',
133
+ scheduleValue: '0 0 * * *',
134
+ timezone: 'Europe/Warsaw',
135
+ targetType: 'queue',
136
+ targetQueue: 'scheduler-execution',
137
+ targetCommand: null,
138
+ targetPayload: updatePayload,
139
+ isEnabled: false,
140
+ },
141
+ },
142
+ });
143
+ });
144
+ });
@@ -14,6 +14,7 @@ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
15
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
16
16
  import { formatDateTime } from '@open-mercato/shared/lib/time'
17
+ import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
17
18
 
18
19
  type ScheduleRow = {
19
20
  id: string
@@ -196,7 +197,7 @@ export default function SchedulerPage() {
196
197
  : row.original.targetCommand
197
198
  return (
198
199
  <div className="flex flex-col">
199
- <span className="text-xs text-gray-500 capitalize">{row.original.targetType}</span>
200
+ <span className="text-xs text-muted-foreground capitalize">{row.original.targetType}</span>
200
201
  <span className="text-sm">{target || '-'}</span>
201
202
  </div>
202
203
  )
@@ -274,6 +275,13 @@ export default function SchedulerPage() {
274
275
  data={rows}
275
276
  onRowClick={(row) => router.push(`/backend/config/scheduled-jobs/${row.id}`)}
276
277
  rowActions={(row) => <RowActions items={rowActions(row)} />}
278
+ emptyState={(
279
+ <ListEmptyState
280
+ entityName={t('scheduler.title', 'Scheduled Jobs')}
281
+ createHref="/backend/config/scheduled-jobs/new"
282
+ createLabel={t('scheduler.action.create', 'New Schedule')}
283
+ />
284
+ )}
277
285
  pagination={{ page, pageSize, total, totalPages, onPageChange: setPage }}
278
286
  isLoading={isLoading}
279
287
  searchValue={search}