@open-mercato/core 0.6.6-develop.6159.1.82dfc232ca → 0.6.6-develop.6174.1.8a7040f00d

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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/resources/components/detail/activitiesAdapter.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport type { ActivitiesDataAdapter, ActivitySummary } from '@open-mercato/ui/backend/detail'\n\ntype Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string\n\nexport function createResourceActivitiesAdapter(translator: Translator): ActivitiesDataAdapter {\n return {\n list: async ({ entityId }) => {\n const params = new URLSearchParams({\n pageSize: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n if (entityId) params.set('entityId', entityId)\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/resources/activities?${params.toString()}`,\n undefined,\n { errorMessage: translator('resources.resources.detail.activities.loadError', 'Failed to load activities.') },\n )\n return Array.isArray(payload?.items) ? (payload.items as ActivitySummary[]) : []\n },\n create: async ({ entityId, activityType, subject, body, occurredAt, customFields }) => {\n await createCrud('resources/activities', {\n entityId,\n activityType,\n subject: subject ?? undefined,\n body: body ?? undefined,\n occurredAt: occurredAt ?? undefined,\n ...(customFields ? { customFields } : {}),\n }, {\n errorMessage: translator('resources.resources.detail.activities.error', 'Failed to save activity'),\n })\n },\n update: async ({ id, patch }) => {\n await updateCrud('resources/activities', {\n id,\n entityId: patch.entityId,\n activityType: patch.activityType,\n subject: patch.subject ?? undefined,\n body: patch.body ?? undefined,\n occurredAt: patch.occurredAt ?? undefined,\n ...(patch.customFields ? { customFields: patch.customFields } : {}),\n }, {\n errorMessage: translator('resources.resources.detail.activities.error', 'Failed to save activity'),\n })\n },\n delete: async ({ id }) => {\n await deleteCrud('resources/activities', {\n id,\n errorMessage: translator('resources.resources.detail.activities.deleteError', 'Failed to delete activity.'),\n })\n },\n }\n}\n"],
5
- "mappings": ";AAEA,SAAS,4BAA4B;AACrC,SAAS,YAAY,YAAY,kBAAkB;AAK5C,SAAS,gCAAgC,YAA+C;AAC7F,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,SAAS,MAAM;AAC5B,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,UAAU;AAAA,QACV,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD,UAAI,SAAU,QAAO,IAAI,YAAY,QAAQ;AAC7C,YAAM,UAAU,MAAM;AAAA,QACpB,6BAA6B,OAAO,SAAS,CAAC;AAAA,QAC9C;AAAA,QACA,EAAE,cAAc,WAAW,mDAAmD,4BAA4B,EAAE;AAAA,MAC9G;AACA,aAAO,MAAM,QAAQ,SAAS,KAAK,IAAK,QAAQ,QAA8B,CAAC;AAAA,IACjF;AAAA,IACA,QAAQ,OAAO,EAAE,UAAU,cAAc,SAAS,MAAM,YAAY,aAAa,MAAM;AACrF,YAAM,WAAW,wBAAwB;AAAA,QACvC;AAAA,QACA;AAAA,QACA,SAAS,WAAW;AAAA,QACpB,MAAM,QAAQ;AAAA,QACd,YAAY,cAAc;AAAA,QAC1B,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,MACzC,GAAG;AAAA,QACD,cAAc,WAAW,+CAA+C,yBAAyB;AAAA,MACnG,CAAC;AAAA,IACH;AAAA,IACA,QAAQ,OAAO,EAAE,IAAI,MAAM,MAAM;AAC/B,YAAM,WAAW,wBAAwB;AAAA,QACvC;AAAA,QACA,UAAU,MAAM;AAAA,QAChB,cAAc,MAAM;AAAA,QACpB,SAAS,MAAM,WAAW;AAAA,QAC1B,MAAM,MAAM,QAAQ;AAAA,QACpB,YAAY,MAAM,cAAc;AAAA,QAChC,GAAI,MAAM,eAAe,EAAE,cAAc,MAAM,aAAa,IAAI,CAAC;AAAA,MACnE,GAAG;AAAA,QACD,cAAc,WAAW,+CAA+C,yBAAyB;AAAA,MACnG,CAAC;AAAA,IACH;AAAA,IACA,QAAQ,OAAO,EAAE,GAAG,MAAM;AACxB,YAAM,WAAW,wBAAwB;AAAA,QACvC;AAAA,QACA,cAAc,WAAW,qDAAqD,4BAA4B;AAAA,MAC5G,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["\"use client\"\n\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'\nimport type { ActivitiesDataAdapter, ActivitySummary } from '@open-mercato/ui/backend/detail'\n\ntype Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string\n\nexport type ResourceActivitiesGuardedMutation = <T>(\n runner: () => Promise<T>,\n payload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type CreateResourceActivitiesAdapterOptions = {\n runMutation?: ResourceActivitiesGuardedMutation\n}\n\nexport function createResourceActivitiesAdapter(\n translator: Translator,\n options: CreateResourceActivitiesAdapterOptions = {},\n): ActivitiesDataAdapter {\n const runWrite = async <T,>(\n runner: () => Promise<T>,\n payload: Record<string, unknown>,\n ): Promise<T> => {\n if (options.runMutation) {\n return options.runMutation(runner, payload)\n }\n return runner()\n }\n\n return {\n list: async ({ entityId }) => {\n const params = new URLSearchParams({\n pageSize: '50',\n sortField: 'occurredAt',\n sortDir: 'desc',\n })\n if (entityId) params.set('entityId', entityId)\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/resources/activities?${params.toString()}`,\n undefined,\n { errorMessage: translator('resources.resources.detail.activities.loadError', 'Failed to load activities.') },\n )\n return Array.isArray(payload?.items) ? (payload.items as ActivitySummary[]) : []\n },\n create: async ({ entityId, activityType, subject, body, occurredAt, customFields }) => {\n const payload = {\n entityId,\n activityType,\n subject: subject ?? undefined,\n body: body ?? undefined,\n occurredAt: occurredAt ?? undefined,\n ...(customFields ? { customFields } : {}),\n }\n await runWrite(\n () => createCrud('resources/activities', payload, {\n errorMessage: translator('resources.resources.detail.activities.error', 'Failed to save activity'),\n }),\n { operation: 'createActivity', entityId, activityType },\n )\n },\n update: async ({ id, patch }) => {\n const payload = {\n id,\n entityId: patch.entityId,\n activityType: patch.activityType,\n subject: patch.subject ?? undefined,\n body: patch.body ?? undefined,\n occurredAt: patch.occurredAt ?? undefined,\n ...(patch.customFields ? { customFields: patch.customFields } : {}),\n }\n await runWrite(\n () => updateCrud('resources/activities', payload, {\n errorMessage: translator('resources.resources.detail.activities.error', 'Failed to save activity'),\n }),\n { operation: 'updateActivity', id },\n )\n },\n delete: async ({ id }) => {\n await runWrite(\n () => deleteCrud('resources/activities', {\n id,\n errorMessage: translator('resources.resources.detail.activities.deleteError', 'Failed to delete activity.'),\n }),\n { operation: 'deleteActivity', id },\n )\n },\n }\n}\n"],
5
+ "mappings": ";AAEA,SAAS,4BAA4B;AACrC,SAAS,YAAY,YAAY,kBAAkB;AAc5C,SAAS,gCACd,YACA,UAAkD,CAAC,GAC5B;AACvB,QAAM,WAAW,OACf,QACA,YACe;AACf,QAAI,QAAQ,aAAa;AACvB,aAAO,QAAQ,YAAY,QAAQ,OAAO;AAAA,IAC5C;AACA,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,SAAS,MAAM;AAC5B,YAAM,SAAS,IAAI,gBAAgB;AAAA,QACjC,UAAU;AAAA,QACV,WAAW;AAAA,QACX,SAAS;AAAA,MACX,CAAC;AACD,UAAI,SAAU,QAAO,IAAI,YAAY,QAAQ;AAC7C,YAAM,UAAU,MAAM;AAAA,QACpB,6BAA6B,OAAO,SAAS,CAAC;AAAA,QAC9C;AAAA,QACA,EAAE,cAAc,WAAW,mDAAmD,4BAA4B,EAAE;AAAA,MAC9G;AACA,aAAO,MAAM,QAAQ,SAAS,KAAK,IAAK,QAAQ,QAA8B,CAAC;AAAA,IACjF;AAAA,IACA,QAAQ,OAAO,EAAE,UAAU,cAAc,SAAS,MAAM,YAAY,aAAa,MAAM;AACrF,YAAM,UAAU;AAAA,QACd;AAAA,QACA;AAAA,QACA,SAAS,WAAW;AAAA,QACpB,MAAM,QAAQ;AAAA,QACd,YAAY,cAAc;AAAA,QAC1B,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,MACzC;AACA,YAAM;AAAA,QACJ,MAAM,WAAW,wBAAwB,SAAS;AAAA,UAChD,cAAc,WAAW,+CAA+C,yBAAyB;AAAA,QACnG,CAAC;AAAA,QACD,EAAE,WAAW,kBAAkB,UAAU,aAAa;AAAA,MACxD;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,EAAE,IAAI,MAAM,MAAM;AAC/B,YAAM,UAAU;AAAA,QACd;AAAA,QACA,UAAU,MAAM;AAAA,QAChB,cAAc,MAAM;AAAA,QACpB,SAAS,MAAM,WAAW;AAAA,QAC1B,MAAM,MAAM,QAAQ;AAAA,QACpB,YAAY,MAAM,cAAc;AAAA,QAChC,GAAI,MAAM,eAAe,EAAE,cAAc,MAAM,aAAa,IAAI,CAAC;AAAA,MACnE;AACA,YAAM;AAAA,QACJ,MAAM,WAAW,wBAAwB,SAAS;AAAA,UAChD,cAAc,WAAW,+CAA+C,yBAAyB;AAAA,QACnG,CAAC;AAAA,QACD,EAAE,WAAW,kBAAkB,GAAG;AAAA,MACpC;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,EAAE,GAAG,MAAM;AACxB,YAAM;AAAA,QACJ,MAAM,WAAW,wBAAwB;AAAA,UACvC;AAAA,UACA,cAAc,WAAW,qDAAqD,4BAA4B;AAAA,QAC5G,CAAC;AAAA,QACD,EAAE,WAAW,kBAAkB,GAAG;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,13 @@
1
1
  "use client";
2
2
  import { apiCallOrThrow, readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
3
3
  import { mapCommentSummary } from "@open-mercato/ui/backend/detail/NotesSection";
4
- function createResourceNotesAdapter(translator) {
4
+ function createResourceNotesAdapter(translator, options = {}) {
5
+ const runWrite = async (runner, payload) => {
6
+ if (options.runMutation) {
7
+ return options.runMutation(runner, payload);
8
+ }
9
+ return runner();
10
+ };
5
11
  return {
6
12
  list: async ({ entityId }) => {
7
13
  const params = new URLSearchParams();
@@ -15,19 +21,23 @@ function createResourceNotesAdapter(translator) {
15
21
  return items.map(mapCommentSummary);
16
22
  },
17
23
  create: async ({ entityId, body, appearanceIcon, appearanceColor }) => {
18
- const response = await apiCallOrThrow(
19
- "/api/resources/comments",
20
- {
21
- method: "POST",
22
- headers: { "content-type": "application/json" },
23
- body: JSON.stringify({
24
- entityId,
25
- body,
26
- appearanceIcon: appearanceIcon ?? void 0,
27
- appearanceColor: appearanceColor ?? void 0
28
- })
29
- },
30
- { errorMessage: translator("resources.resources.detail.notes.error", "Failed to save note.") }
24
+ const requestBody = {
25
+ entityId,
26
+ body,
27
+ appearanceIcon: appearanceIcon ?? void 0,
28
+ appearanceColor: appearanceColor ?? void 0
29
+ };
30
+ const response = await runWrite(
31
+ () => apiCallOrThrow(
32
+ "/api/resources/comments",
33
+ {
34
+ method: "POST",
35
+ headers: { "content-type": "application/json" },
36
+ body: JSON.stringify(requestBody)
37
+ },
38
+ { errorMessage: translator("resources.resources.detail.notes.error", "Failed to save note.") }
39
+ ),
40
+ { operation: "createNote", entityId }
31
41
  );
32
42
  return response.result ?? {};
33
43
  },
@@ -36,24 +46,30 @@ function createResourceNotesAdapter(translator) {
36
46
  if (patch.body !== void 0) payload.body = patch.body;
37
47
  if (patch.appearanceIcon !== void 0) payload.appearanceIcon = patch.appearanceIcon;
38
48
  if (patch.appearanceColor !== void 0) payload.appearanceColor = patch.appearanceColor;
39
- await apiCallOrThrow(
40
- "/api/resources/comments",
41
- {
42
- method: "PUT",
43
- headers: { "content-type": "application/json" },
44
- body: JSON.stringify(payload)
45
- },
46
- { errorMessage: translator("resources.resources.detail.notes.updateError", "Failed to update note.") }
49
+ await runWrite(
50
+ () => apiCallOrThrow(
51
+ "/api/resources/comments",
52
+ {
53
+ method: "PUT",
54
+ headers: { "content-type": "application/json" },
55
+ body: JSON.stringify(payload)
56
+ },
57
+ { errorMessage: translator("resources.resources.detail.notes.updateError", "Failed to update note.") }
58
+ ),
59
+ { operation: "updateNote", id }
47
60
  );
48
61
  },
49
62
  delete: async ({ id }) => {
50
- await apiCallOrThrow(
51
- `/api/resources/comments?id=${encodeURIComponent(id)}`,
52
- {
53
- method: "DELETE",
54
- headers: { "content-type": "application/json" }
55
- },
56
- { errorMessage: translator("resources.resources.detail.notes.deleteError", "Failed to delete note") }
63
+ await runWrite(
64
+ () => apiCallOrThrow(
65
+ `/api/resources/comments?id=${encodeURIComponent(id)}`,
66
+ {
67
+ method: "DELETE",
68
+ headers: { "content-type": "application/json" }
69
+ },
70
+ { errorMessage: translator("resources.resources.detail.notes.deleteError", "Failed to delete note") }
71
+ ),
72
+ { operation: "deleteNote", id }
57
73
  );
58
74
  }
59
75
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/resources/components/detail/notesAdapter.ts"],
4
- "sourcesContent": ["\"use client\"\n\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { mapCommentSummary, type NotesDataAdapter } from '@open-mercato/ui/backend/detail/NotesSection'\n\ntype Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string\n\nexport function createResourceNotesAdapter(translator: Translator): NotesDataAdapter {\n return {\n list: async ({ entityId }) => {\n const params = new URLSearchParams()\n if (entityId) params.set('entityId', entityId)\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/resources/comments?${params.toString()}`,\n undefined,\n { errorMessage: translator('resources.resources.detail.notes.loadError', 'Failed to load notes.') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n return items.map(mapCommentSummary)\n },\n create: async ({ entityId, body, appearanceIcon, appearanceColor }) => {\n const response = await apiCallOrThrow<Record<string, unknown>>(\n '/api/resources/comments',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n entityId,\n body,\n appearanceIcon: appearanceIcon ?? undefined,\n appearanceColor: appearanceColor ?? undefined,\n }),\n },\n { errorMessage: translator('resources.resources.detail.notes.error', 'Failed to save note.') },\n )\n return response.result ?? {}\n },\n update: async ({ id, patch }) => {\n const payload: Record<string, unknown> = { id }\n if (patch.body !== undefined) payload.body = patch.body\n if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon\n if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor\n await apiCallOrThrow(\n '/api/resources/comments',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: translator('resources.resources.detail.notes.updateError', 'Failed to update note.') },\n )\n },\n delete: async ({ id }) => {\n await apiCallOrThrow(\n `/api/resources/comments?id=${encodeURIComponent(id)}`,\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n },\n { errorMessage: translator('resources.resources.detail.notes.deleteError', 'Failed to delete note') },\n )\n },\n }\n}\n"],
5
- "mappings": ";AAEA,SAAS,gBAAgB,4BAA4B;AACrD,SAAS,yBAAgD;AAIlD,SAAS,2BAA2B,YAA0C;AACnF,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,SAAS,MAAM;AAC5B,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,SAAU,QAAO,IAAI,YAAY,QAAQ;AAC7C,YAAM,UAAU,MAAM;AAAA,QACpB,2BAA2B,OAAO,SAAS,CAAC;AAAA,QAC5C;AAAA,QACA,EAAE,cAAc,WAAW,8CAA8C,uBAAuB,EAAE;AAAA,MACpG;AACA,YAAM,QAAQ,MAAM,QAAQ,SAAS,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC/D,aAAO,MAAM,IAAI,iBAAiB;AAAA,IACpC;AAAA,IACA,QAAQ,OAAO,EAAE,UAAU,MAAM,gBAAgB,gBAAgB,MAAM;AACrE,YAAM,WAAW,MAAM;AAAA,QACrB;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU;AAAA,YACnB;AAAA,YACA;AAAA,YACA,gBAAgB,kBAAkB;AAAA,YAClC,iBAAiB,mBAAmB;AAAA,UACtC,CAAC;AAAA,QACH;AAAA,QACA,EAAE,cAAc,WAAW,0CAA0C,sBAAsB,EAAE;AAAA,MAC/F;AACA,aAAO,SAAS,UAAU,CAAC;AAAA,IAC7B;AAAA,IACA,QAAQ,OAAO,EAAE,IAAI,MAAM,MAAM;AAC/B,YAAM,UAAmC,EAAE,GAAG;AAC9C,UAAI,MAAM,SAAS,OAAW,SAAQ,OAAO,MAAM;AACnD,UAAI,MAAM,mBAAmB,OAAW,SAAQ,iBAAiB,MAAM;AACvE,UAAI,MAAM,oBAAoB,OAAW,SAAQ,kBAAkB,MAAM;AACzE,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B;AAAA,QACA,EAAE,cAAc,WAAW,gDAAgD,wBAAwB,EAAE;AAAA,MACvG;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,EAAE,GAAG,MAAM;AACxB,YAAM;AAAA,QACJ,8BAA8B,mBAAmB,EAAE,CAAC;AAAA,QACpD;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAChD;AAAA,QACA,EAAE,cAAc,WAAW,gDAAgD,uBAAuB,EAAE;AAAA,MACtG;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["\"use client\"\n\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { mapCommentSummary, type NotesDataAdapter } from '@open-mercato/ui/backend/detail/NotesSection'\n\ntype Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string\n\nexport type ResourceNotesGuardedMutation = <T>(\n runner: () => Promise<T>,\n payload?: Record<string, unknown>,\n) => Promise<T>\n\nexport type CreateResourceNotesAdapterOptions = {\n runMutation?: ResourceNotesGuardedMutation\n}\n\nexport function createResourceNotesAdapter(\n translator: Translator,\n options: CreateResourceNotesAdapterOptions = {},\n): NotesDataAdapter {\n const runWrite = async <T,>(\n runner: () => Promise<T>,\n payload: Record<string, unknown>,\n ): Promise<T> => {\n if (options.runMutation) {\n return options.runMutation(runner, payload)\n }\n return runner()\n }\n\n return {\n list: async ({ entityId }) => {\n const params = new URLSearchParams()\n if (entityId) params.set('entityId', entityId)\n const payload = await readApiResultOrThrow<Record<string, unknown>>(\n `/api/resources/comments?${params.toString()}`,\n undefined,\n { errorMessage: translator('resources.resources.detail.notes.loadError', 'Failed to load notes.') },\n )\n const items = Array.isArray(payload?.items) ? payload.items : []\n return items.map(mapCommentSummary)\n },\n create: async ({ entityId, body, appearanceIcon, appearanceColor }) => {\n const requestBody = {\n entityId,\n body,\n appearanceIcon: appearanceIcon ?? undefined,\n appearanceColor: appearanceColor ?? undefined,\n }\n const response = await runWrite(\n () => apiCallOrThrow<Record<string, unknown>>(\n '/api/resources/comments',\n {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(requestBody),\n },\n { errorMessage: translator('resources.resources.detail.notes.error', 'Failed to save note.') },\n ),\n { operation: 'createNote', entityId },\n )\n return response.result ?? {}\n },\n update: async ({ id, patch }) => {\n const payload: Record<string, unknown> = { id }\n if (patch.body !== undefined) payload.body = patch.body\n if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon\n if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor\n await runWrite(\n () => apiCallOrThrow(\n '/api/resources/comments',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: translator('resources.resources.detail.notes.updateError', 'Failed to update note.') },\n ),\n { operation: 'updateNote', id },\n )\n },\n delete: async ({ id }) => {\n await runWrite(\n () => apiCallOrThrow(\n `/api/resources/comments?id=${encodeURIComponent(id)}`,\n {\n method: 'DELETE',\n headers: { 'content-type': 'application/json' },\n },\n { errorMessage: translator('resources.resources.detail.notes.deleteError', 'Failed to delete note') },\n ),\n { operation: 'deleteNote', id },\n )\n },\n }\n}\n"],
5
+ "mappings": ";AAEA,SAAS,gBAAgB,4BAA4B;AACrD,SAAS,yBAAgD;AAalD,SAAS,2BACd,YACA,UAA6C,CAAC,GAC5B;AAClB,QAAM,WAAW,OACf,QACA,YACe;AACf,QAAI,QAAQ,aAAa;AACvB,aAAO,QAAQ,YAAY,QAAQ,OAAO;AAAA,IAC5C;AACA,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,EAAE,SAAS,MAAM;AAC5B,YAAM,SAAS,IAAI,gBAAgB;AACnC,UAAI,SAAU,QAAO,IAAI,YAAY,QAAQ;AAC7C,YAAM,UAAU,MAAM;AAAA,QACpB,2BAA2B,OAAO,SAAS,CAAC;AAAA,QAC5C;AAAA,QACA,EAAE,cAAc,WAAW,8CAA8C,uBAAuB,EAAE;AAAA,MACpG;AACA,YAAM,QAAQ,MAAM,QAAQ,SAAS,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAC/D,aAAO,MAAM,IAAI,iBAAiB;AAAA,IACpC;AAAA,IACA,QAAQ,OAAO,EAAE,UAAU,MAAM,gBAAgB,gBAAgB,MAAM;AACrE,YAAM,cAAc;AAAA,QAClB;AAAA,QACA;AAAA,QACA,gBAAgB,kBAAkB;AAAA,QAClC,iBAAiB,mBAAmB;AAAA,MACtC;AACA,YAAM,WAAW,MAAM;AAAA,QACrB,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU,WAAW;AAAA,UAClC;AAAA,UACA,EAAE,cAAc,WAAW,0CAA0C,sBAAsB,EAAE;AAAA,QAC/F;AAAA,QACA,EAAE,WAAW,cAAc,SAAS;AAAA,MACtC;AACA,aAAO,SAAS,UAAU,CAAC;AAAA,IAC7B;AAAA,IACA,QAAQ,OAAO,EAAE,IAAI,MAAM,MAAM;AAC/B,YAAM,UAAmC,EAAE,GAAG;AAC9C,UAAI,MAAM,SAAS,OAAW,SAAQ,OAAO,MAAM;AACnD,UAAI,MAAM,mBAAmB,OAAW,SAAQ,iBAAiB,MAAM;AACvE,UAAI,MAAM,oBAAoB,OAAW,SAAQ,kBAAkB,MAAM;AACzE,YAAM;AAAA,QACJ,MAAM;AAAA,UACJ;AAAA,UACA;AAAA,YACE,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,UAC9B;AAAA,UACA,EAAE,cAAc,WAAW,gDAAgD,wBAAwB,EAAE;AAAA,QACvG;AAAA,QACA,EAAE,WAAW,cAAc,GAAG;AAAA,MAChC;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,EAAE,GAAG,MAAM;AACxB,YAAM;AAAA,QACJ,MAAM;AAAA,UACJ,8BAA8B,mBAAmB,EAAE,CAAC;AAAA,UACpD;AAAA,YACE,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAChD;AAAA,UACA,EAAE,cAAc,WAAW,gDAAgD,uBAAuB,EAAE;AAAA,QACtG;AAAA,QACA,EAAE,WAAW,cAAc,GAAG;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.6-develop.6159.1.82dfc232ca",
3
+ "version": "0.6.6-develop.6174.1.8a7040f00d",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -232,30 +232,30 @@
232
232
  "@mikro-orm/decorators": "^7.1.4",
233
233
  "@mikro-orm/postgresql": "^7.1.4",
234
234
  "@xyflow/react": "^12.11.0",
235
- "ai": "^6.0.205",
235
+ "ai": "^6.0.208",
236
236
  "date-fns": "4.4.0",
237
237
  "date-fns-tz": "^3.2.0",
238
238
  "html-to-text": "^10.0.0",
239
239
  "mammoth": "^1.9.0",
240
240
  "pdfjs-dist": "^6.0.227",
241
- "resend": "^6.12.4",
241
+ "resend": "^6.14.0",
242
242
  "sanitize-html": "^2.17.5",
243
- "semver": "^7.8.4",
244
- "svix": "^1.95.2",
243
+ "semver": "^7.8.5",
244
+ "svix": "^1.96.0",
245
245
  "ts-pattern": "^5.0.0",
246
246
  "zod": "^4.4.3"
247
247
  },
248
248
  "peerDependencies": {
249
- "@open-mercato/ai-assistant": "0.6.6-develop.6159.1.82dfc232ca",
250
- "@open-mercato/shared": "0.6.6-develop.6159.1.82dfc232ca",
251
- "@open-mercato/ui": "0.6.6-develop.6159.1.82dfc232ca",
249
+ "@open-mercato/ai-assistant": "0.6.6-develop.6174.1.8a7040f00d",
250
+ "@open-mercato/shared": "0.6.6-develop.6174.1.8a7040f00d",
251
+ "@open-mercato/ui": "0.6.6-develop.6174.1.8a7040f00d",
252
252
  "react": "^19.0.0",
253
253
  "react-dom": "^19.0.0"
254
254
  },
255
255
  "devDependencies": {
256
- "@open-mercato/ai-assistant": "0.6.6-develop.6159.1.82dfc232ca",
257
- "@open-mercato/shared": "0.6.6-develop.6159.1.82dfc232ca",
258
- "@open-mercato/ui": "0.6.6-develop.6159.1.82dfc232ca",
256
+ "@open-mercato/ai-assistant": "0.6.6-develop.6174.1.8a7040f00d",
257
+ "@open-mercato/shared": "0.6.6-develop.6174.1.8a7040f00d",
258
+ "@open-mercato/ui": "0.6.6-develop.6174.1.8a7040f00d",
259
259
  "@testing-library/dom": "^10.4.1",
260
260
  "@testing-library/jest-dom": "^6.9.1",
261
261
  "@testing-library/react": "^16.3.1",
@@ -13,6 +13,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
13
13
  import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
14
14
  import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
15
15
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
16
17
  import { renderDictionaryColor, renderDictionaryIcon } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
17
18
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
18
19
  import { Package } from 'lucide-react'
@@ -23,6 +24,7 @@ import { formatDateTime } from '@open-mercato/shared/lib/time'
23
24
  const PAGE_SIZE = 50
24
25
  const DESCRIPTION_CLASSNAME = 'line-clamp-3 whitespace-pre-line text-sm text-foreground'
25
26
  const SUBTEXT_CLASSNAME = 'line-clamp-2 text-xs text-muted-foreground'
27
+ const RESOURCE_TYPES_MUTATION_CONTEXT_ID = 'resources.resource-types.list'
26
28
 
27
29
  type ResourceTypeRow = {
28
30
  id: string
@@ -40,6 +42,13 @@ type ResourceTypesResponse = {
40
42
  totalPages?: number
41
43
  }
42
44
 
45
+ type ResourceTypesMutationContext = {
46
+ formId: string
47
+ resourceKind: string
48
+ resourceId?: string
49
+ retryLastMutation: () => Promise<boolean>
50
+ }
51
+
43
52
  export default function ResourcesResourceTypesPage() {
44
53
  const translate = useT()
45
54
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
@@ -53,6 +62,27 @@ export default function ResourcesResourceTypesPage() {
53
62
  const [search, setSearch] = React.useState('')
54
63
  const [isLoading, setIsLoading] = React.useState(true)
55
64
  const [reloadToken, setReloadToken] = React.useState(0)
65
+ const { runMutation, retryLastMutation } = useGuardedMutation<ResourceTypesMutationContext>({
66
+ contextId: RESOURCE_TYPES_MUTATION_CONTEXT_ID,
67
+ blockedMessage: translate('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
68
+ })
69
+ const runResourceTypeMutation = React.useCallback(
70
+ async <T,>(
71
+ operation: () => Promise<T>,
72
+ mutationPayload: Record<string, unknown>,
73
+ resourceId?: string,
74
+ ): Promise<T> => runMutation({
75
+ operation,
76
+ mutationPayload,
77
+ context: {
78
+ formId: RESOURCE_TYPES_MUTATION_CONTEXT_ID,
79
+ resourceKind: 'resources.resourceType',
80
+ resourceId,
81
+ retryLastMutation,
82
+ },
83
+ }),
84
+ [retryLastMutation, runMutation],
85
+ )
56
86
 
57
87
  const translations = React.useMemo(() => ({
58
88
  title: translate('resources.resourceTypes.page.title', 'Resource types'),
@@ -230,16 +260,20 @@ export default function ResourcesResourceTypesPage() {
230
260
  if (!confirmed) return
231
261
  try {
232
262
  const headers = buildOptimisticLockHeader(entry.updatedAt)
233
- await withScopedApiRequestHeaders(headers, () => (
234
- deleteCrud('resources/resource-types', entry.id, { errorMessage: translations.errors.delete })
235
- ))
263
+ await runResourceTypeMutation(
264
+ () => withScopedApiRequestHeaders(headers, () => (
265
+ deleteCrud('resources/resource-types', entry.id, { errorMessage: translations.errors.delete })
266
+ )),
267
+ { operation: 'deleteResourceType', id: entry.id, updatedAt: entry.updatedAt ?? null },
268
+ entry.id,
269
+ )
236
270
  flash(translations.messages.deleted, 'success')
237
271
  handleRefresh()
238
272
  } catch (error) {
239
273
  console.error('resources.resource-types.delete', error)
240
274
  flash(translations.errors.delete, 'error')
241
275
  }
242
- }, [confirm, handleRefresh, translations.actions.deleteConfirm, translations.errors.delete, translations.errors.deleteAssigned, translations.messages.deleted])
276
+ }, [confirm, handleRefresh, runResourceTypeMutation, translations.actions.deleteConfirm, translations.errors.delete, translations.errors.deleteAssigned, translations.messages.deleted])
243
277
 
244
278
  return (
245
279
  <Page>
@@ -12,6 +12,7 @@ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors
12
12
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
13
13
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
14
14
  import { ActivitiesSection, NotesSection, RecordNotFoundState, type SectionAction, type TagOption } from '@open-mercato/ui/backend/detail'
15
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
15
16
  import { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'
16
17
  import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
17
18
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -62,6 +63,14 @@ type ResourceResponse = {
62
63
  items: ResourceRecord[]
63
64
  }
64
65
 
66
+ type ResourceDetailMutationContext = {
67
+ formId: string
68
+ resourceKind: string
69
+ resourceId?: string
70
+ data: Record<string, unknown> | null
71
+ retryLastMutation: () => Promise<boolean>
72
+ }
73
+
65
74
  function normalizeResourceRecord(record: ResourceRecord): ResourceRecord {
66
75
  return {
67
76
  ...record,
@@ -108,8 +117,42 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
108
117
  const flashShownRef = React.useRef(false)
109
118
 
110
119
  const availabilityMode = 'availability'
111
- const notesAdapter = React.useMemo(() => createResourceNotesAdapter(detailTranslator), [detailTranslator])
112
- const activitiesAdapter = React.useMemo(() => createResourceActivitiesAdapter(detailTranslator), [detailTranslator])
120
+ const mutationContextId = React.useMemo(
121
+ () => (resourceId ? `resources.resource:${resourceId}` : 'resources.resource:pending'),
122
+ [resourceId],
123
+ )
124
+ const { runMutation, retryLastMutation } = useGuardedMutation<ResourceDetailMutationContext>({
125
+ contextId: mutationContextId,
126
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
127
+ })
128
+ const mutationContext = React.useMemo<ResourceDetailMutationContext>(
129
+ () => ({
130
+ formId: mutationContextId,
131
+ resourceKind: 'resources.resource',
132
+ resourceId,
133
+ data: initialValues,
134
+ retryLastMutation,
135
+ }),
136
+ [initialValues, mutationContextId, resourceId, retryLastMutation],
137
+ )
138
+ const runMutationWithContext = React.useCallback(
139
+ async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => (
140
+ runMutation({
141
+ operation,
142
+ mutationPayload,
143
+ context: mutationContext,
144
+ })
145
+ ),
146
+ [mutationContext, runMutation],
147
+ )
148
+ const notesAdapter = React.useMemo(
149
+ () => createResourceNotesAdapter(detailTranslator, { runMutation: runMutationWithContext }),
150
+ [detailTranslator, runMutationWithContext],
151
+ )
152
+ const activitiesAdapter = React.useMemo(
153
+ () => createResourceActivitiesAdapter(detailTranslator, { runMutation: runMutationWithContext }),
154
+ [detailTranslator, runMutationWithContext],
155
+ )
113
156
 
114
157
  const activityTypeLabels = React.useMemo<DictionarySelectLabels>(() => ({
115
158
  placeholder: t('resources.resources.detail.activities.dictionary.placeholder', 'Select an activity type'),
@@ -342,14 +385,18 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
342
385
  if (!trimmed.length) {
343
386
  throw new Error(t('resources.resources.tags.labelRequired', 'Tag name is required.'))
344
387
  }
345
- const response = await apiCallOrThrow<Record<string, unknown>>(
346
- '/api/resources/tags',
347
- {
348
- method: 'POST',
349
- headers: { 'content-type': 'application/json' },
350
- body: JSON.stringify({ label: trimmed }),
351
- },
352
- { errorMessage: t('resources.resources.tags.createError', 'Failed to create tag.') },
388
+ const requestBody = { label: trimmed }
389
+ const response = await runMutationWithContext(
390
+ () => apiCallOrThrow<Record<string, unknown>>(
391
+ '/api/resources/tags',
392
+ {
393
+ method: 'POST',
394
+ headers: { 'content-type': 'application/json' },
395
+ body: JSON.stringify(requestBody),
396
+ },
397
+ { errorMessage: t('resources.resources.tags.createError', 'Failed to create tag.') },
398
+ ),
399
+ { operation: 'createTag', label: trimmed },
353
400
  )
354
401
  const payload = response.result ?? {}
355
402
  const id =
@@ -364,34 +411,42 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
364
411
  : null
365
412
  return { id, label: trimmed, color }
366
413
  },
367
- [t],
414
+ [runMutationWithContext, t],
368
415
  )
369
416
 
370
417
  const assignTag = React.useCallback(async (tagId: string) => {
371
418
  if (!resourceId) return
372
- await apiCallOrThrow(
373
- '/api/resources/resources/tags/assign',
374
- {
375
- method: 'POST',
376
- headers: { 'content-type': 'application/json' },
377
- body: JSON.stringify({ tagId, resourceId }),
378
- },
379
- { errorMessage: t('resources.resources.tags.updateError', 'Failed to update tags.') },
419
+ const requestBody = { tagId, resourceId }
420
+ await runMutationWithContext(
421
+ () => apiCallOrThrow(
422
+ '/api/resources/resources/tags/assign',
423
+ {
424
+ method: 'POST',
425
+ headers: { 'content-type': 'application/json' },
426
+ body: JSON.stringify(requestBody),
427
+ },
428
+ { errorMessage: t('resources.resources.tags.updateError', 'Failed to update tags.') },
429
+ ),
430
+ { operation: 'assignTag', resourceId, tagId },
380
431
  )
381
- }, [resourceId, t])
432
+ }, [resourceId, runMutationWithContext, t])
382
433
 
383
434
  const unassignTag = React.useCallback(async (tagId: string) => {
384
435
  if (!resourceId) return
385
- await apiCallOrThrow(
386
- '/api/resources/resources/tags/unassign',
387
- {
388
- method: 'POST',
389
- headers: { 'content-type': 'application/json' },
390
- body: JSON.stringify({ tagId, resourceId }),
391
- },
392
- { errorMessage: t('resources.resources.tags.updateError', 'Failed to update tags.') },
436
+ const requestBody = { tagId, resourceId }
437
+ await runMutationWithContext(
438
+ () => apiCallOrThrow(
439
+ '/api/resources/resources/tags/unassign',
440
+ {
441
+ method: 'POST',
442
+ headers: { 'content-type': 'application/json' },
443
+ body: JSON.stringify(requestBody),
444
+ },
445
+ { errorMessage: t('resources.resources.tags.updateError', 'Failed to update tags.') },
446
+ ),
447
+ { operation: 'unassignTag', resourceId, tagId },
393
448
  )
394
- }, [resourceId, t])
449
+ }, [resourceId, runMutationWithContext, t])
395
450
 
396
451
  const handleTagsSave = React.useCallback(
397
452
  async ({ next, added, removed }: { next: TagOption[]; added: TagOption[]; removed: TagOption[] }) => {
@@ -508,12 +563,21 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
508
563
 
509
564
  const handleDelete = React.useCallback(async () => {
510
565
  if (!resourceId) return
511
- await deleteCrud('resources/resources', resourceId, {
566
+ const resourceOptimisticLockHeader = buildOptimisticLockHeader(
567
+ typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,
568
+ )
569
+ const deleteResource = () => deleteCrud('resources/resources', resourceId, {
512
570
  errorMessage: t('resources.resources.form.errors.delete', 'Failed to delete resource.'),
513
571
  })
572
+ await runMutationWithContext(
573
+ () => Object.keys(resourceOptimisticLockHeader).length > 0
574
+ ? withScopedApiRequestHeaders(resourceOptimisticLockHeader, deleteResource)
575
+ : deleteResource(),
576
+ { operation: 'deleteResource', id: resourceId, updatedAt: initialValues?.updatedAt ?? null },
577
+ )
514
578
  flash(t('resources.resources.form.flash.deleted', 'Resource deleted.'), 'success')
515
579
  router.push('/backend/resources/resources')
516
- }, [resourceId, router, t])
580
+ }, [initialValues?.updatedAt, resourceId, router, runMutationWithContext, t])
517
581
 
518
582
  const handleRulesetChange = React.useCallback(async (nextId: string | null) => {
519
583
  if (!resourceId) return
@@ -523,14 +587,15 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
523
587
  const resourceOptimisticLockHeader = buildOptimisticLockHeader(
524
588
  typeof initialValues?.updatedAt === 'string' ? initialValues.updatedAt : null,
525
589
  )
526
- if (Object.keys(resourceOptimisticLockHeader).length > 0) {
527
- await withScopedApiRequestHeaders(resourceOptimisticLockHeader, updateSchedule)
528
- } else {
529
- await updateSchedule()
530
- }
590
+ await runMutationWithContext(
591
+ () => Object.keys(resourceOptimisticLockHeader).length > 0
592
+ ? withScopedApiRequestHeaders(resourceOptimisticLockHeader, updateSchedule)
593
+ : updateSchedule(),
594
+ { operation: 'updateAvailabilityRuleSet', id: resourceId, availabilityRuleSetId: nextId },
595
+ )
531
596
  setAvailabilityRuleSetId(nextId)
532
597
  flash(t('resources.resources.availability.ruleset.updateSuccess', 'Schedule updated.'), 'success')
533
- }, [initialValues?.updatedAt, resourceId, t])
598
+ }, [initialValues?.updatedAt, resourceId, runMutationWithContext, t])
534
599
 
535
600
  const resourceTitle =
536
601
  typeof initialValues?.name === 'string' && initialValues.name.trim().length > 0
@@ -16,6 +16,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
16
16
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
17
17
  import type { FilterDef, FilterOption, FilterValues } from '@open-mercato/ui/backend/FilterOverlay'
18
18
  import type { TagOption } from '@open-mercato/ui/backend/detail'
19
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
19
20
  import { renderDictionaryColor, renderDictionaryIcon } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
20
21
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
21
22
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -23,6 +24,7 @@ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
23
24
  import { Pencil } from 'lucide-react'
24
25
 
25
26
  const PAGE_SIZE = 20
27
+ const RESOURCE_LIST_MUTATION_CONTEXT_ID = 'resources.resources.list'
26
28
 
27
29
  type ResourceRow = {
28
30
  id: string
@@ -66,6 +68,13 @@ type ResourceTypesResponse = {
66
68
  items: Array<Record<string, unknown>>
67
69
  }
68
70
 
71
+ type ResourceListMutationContext = {
72
+ formId: string
73
+ resourceKind: string
74
+ resourceId?: string
75
+ retryLastMutation: () => Promise<boolean>
76
+ }
77
+
69
78
  export default function ResourcesResourcesPage() {
70
79
  const [rows, setRows] = React.useState<ResourceRow[]>([])
71
80
  const [page, setPage] = React.useState(1)
@@ -87,6 +96,27 @@ export default function ResourcesResourcesPage() {
87
96
  const selectedResourceTypeId = typeof filterValues.resourceTypeId === 'string'
88
97
  ? filterValues.resourceTypeId
89
98
  : resourceTypeFilter
99
+ const { runMutation, retryLastMutation } = useGuardedMutation<ResourceListMutationContext>({
100
+ contextId: RESOURCE_LIST_MUTATION_CONTEXT_ID,
101
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
102
+ })
103
+ const runResourceMutation = React.useCallback(
104
+ async <T,>(
105
+ operation: () => Promise<T>,
106
+ mutationPayload: Record<string, unknown>,
107
+ resourceId?: string,
108
+ ): Promise<T> => runMutation({
109
+ operation,
110
+ mutationPayload,
111
+ context: {
112
+ formId: RESOURCE_LIST_MUTATION_CONTEXT_ID,
113
+ resourceKind: 'resources.resource',
114
+ resourceId,
115
+ retryLastMutation,
116
+ },
117
+ }),
118
+ [retryLastMutation, runMutation],
119
+ )
90
120
 
91
121
  React.useEffect(() => {
92
122
  setPage(1)
@@ -353,11 +383,15 @@ export default function ResourcesResourcesPage() {
353
383
  if (!confirmed) return
354
384
  try {
355
385
  const headers = buildOptimisticLockHeader(row.updatedAt)
356
- await withScopedApiRequestHeaders(headers, () => (
357
- deleteCrud('resources/resources', row.id, {
358
- errorMessage: t('resources.resources.list.error.delete', 'Failed to delete resource.'),
359
- })
360
- ))
386
+ await runResourceMutation(
387
+ () => withScopedApiRequestHeaders(headers, () => (
388
+ deleteCrud('resources/resources', row.id, {
389
+ errorMessage: t('resources.resources.list.error.delete', 'Failed to delete resource.'),
390
+ })
391
+ )),
392
+ { operation: 'deleteResource', id: row.id, updatedAt: row.updatedAt ?? null },
393
+ row.id,
394
+ )
361
395
  flash(t('resources.resources.list.flash.deleted', 'Resource deleted.'), 'success')
362
396
  setPage(1)
363
397
  router.refresh()
@@ -365,7 +399,7 @@ export default function ResourcesResourcesPage() {
365
399
  const message = error instanceof Error ? error.message : t('resources.resources.list.error.delete', 'Failed to delete resource.')
366
400
  flash(message, 'error')
367
401
  }
368
- }, [confirm, router, t])
402
+ }, [confirm, router, runResourceMutation, t])
369
403
 
370
404
  const columns = React.useMemo<ColumnDef<ResourceTableRow>[]>(() => [
371
405
  {