@open-mercato/core 0.6.6-develop.6171.1.17de2dc37a → 0.6.6-develop.6176.1.4507b99c2f
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +5 -3
- package/agentic/standalone-guide.md +4 -2
- package/dist/modules/customers/api/interactions/encryptedSortPage.js +24 -0
- package/dist/modules/customers/api/interactions/encryptedSortPage.js.map +7 -0
- package/dist/modules/customers/api/interactions/route.js +160 -82
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/labels/route.js +24 -2
- package/dist/modules/customers/api/labels/route.js.map +2 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js +25 -2
- package/dist/modules/resources/backend/resources/resource-types/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +88 -37
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/page.js +27 -4
- package/dist/modules/resources/backend/resources/resources/page.js.map +2 -2
- package/dist/modules/resources/components/detail/activitiesAdapter.js +30 -13
- package/dist/modules/resources/components/detail/activitiesAdapter.js.map +2 -2
- package/dist/modules/resources/components/detail/notesAdapter.js +45 -29
- package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/api/interactions/encryptedSortPage.ts +34 -0
- package/src/modules/customers/api/interactions/route.ts +195 -99
- package/src/modules/customers/api/labels/route.ts +27 -3
- package/src/modules/resources/backend/resources/resource-types/page.tsx +38 -4
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +102 -37
- package/src/modules/resources/backend/resources/resources/page.tsx +40 -6
- package/src/modules/resources/components/detail/activitiesAdapter.ts +46 -13
- package/src/modules/resources/components/detail/notesAdapter.ts +61 -29
|
@@ -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
|
|
234
|
-
|
|
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
|
|
112
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
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
|
{
|
|
@@ -6,7 +6,29 @@ import type { ActivitiesDataAdapter, ActivitySummary } from '@open-mercato/ui/ba
|
|
|
6
6
|
|
|
7
7
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export type ResourceActivitiesGuardedMutation = <T>(
|
|
10
|
+
runner: () => Promise<T>,
|
|
11
|
+
payload?: Record<string, unknown>,
|
|
12
|
+
) => Promise<T>
|
|
13
|
+
|
|
14
|
+
export type CreateResourceActivitiesAdapterOptions = {
|
|
15
|
+
runMutation?: ResourceActivitiesGuardedMutation
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createResourceActivitiesAdapter(
|
|
19
|
+
translator: Translator,
|
|
20
|
+
options: CreateResourceActivitiesAdapterOptions = {},
|
|
21
|
+
): ActivitiesDataAdapter {
|
|
22
|
+
const runWrite = async <T,>(
|
|
23
|
+
runner: () => Promise<T>,
|
|
24
|
+
payload: Record<string, unknown>,
|
|
25
|
+
): Promise<T> => {
|
|
26
|
+
if (options.runMutation) {
|
|
27
|
+
return options.runMutation(runner, payload)
|
|
28
|
+
}
|
|
29
|
+
return runner()
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
return {
|
|
11
33
|
list: async ({ entityId }) => {
|
|
12
34
|
const params = new URLSearchParams({
|
|
@@ -23,19 +45,23 @@ export function createResourceActivitiesAdapter(translator: Translator): Activit
|
|
|
23
45
|
return Array.isArray(payload?.items) ? (payload.items as ActivitySummary[]) : []
|
|
24
46
|
},
|
|
25
47
|
create: async ({ entityId, activityType, subject, body, occurredAt, customFields }) => {
|
|
26
|
-
|
|
48
|
+
const payload = {
|
|
27
49
|
entityId,
|
|
28
50
|
activityType,
|
|
29
51
|
subject: subject ?? undefined,
|
|
30
52
|
body: body ?? undefined,
|
|
31
53
|
occurredAt: occurredAt ?? undefined,
|
|
32
54
|
...(customFields ? { customFields } : {}),
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
}
|
|
56
|
+
await runWrite(
|
|
57
|
+
() => createCrud('resources/activities', payload, {
|
|
58
|
+
errorMessage: translator('resources.resources.detail.activities.error', 'Failed to save activity'),
|
|
59
|
+
}),
|
|
60
|
+
{ operation: 'createActivity', entityId, activityType },
|
|
61
|
+
)
|
|
36
62
|
},
|
|
37
63
|
update: async ({ id, patch }) => {
|
|
38
|
-
|
|
64
|
+
const payload = {
|
|
39
65
|
id,
|
|
40
66
|
entityId: patch.entityId,
|
|
41
67
|
activityType: patch.activityType,
|
|
@@ -43,15 +69,22 @@ export function createResourceActivitiesAdapter(translator: Translator): Activit
|
|
|
43
69
|
body: patch.body ?? undefined,
|
|
44
70
|
occurredAt: patch.occurredAt ?? undefined,
|
|
45
71
|
...(patch.customFields ? { customFields: patch.customFields } : {}),
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
}
|
|
73
|
+
await runWrite(
|
|
74
|
+
() => updateCrud('resources/activities', payload, {
|
|
75
|
+
errorMessage: translator('resources.resources.detail.activities.error', 'Failed to save activity'),
|
|
76
|
+
}),
|
|
77
|
+
{ operation: 'updateActivity', id },
|
|
78
|
+
)
|
|
49
79
|
},
|
|
50
80
|
delete: async ({ id }) => {
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
81
|
+
await runWrite(
|
|
82
|
+
() => deleteCrud('resources/activities', {
|
|
83
|
+
id,
|
|
84
|
+
errorMessage: translator('resources.resources.detail.activities.deleteError', 'Failed to delete activity.'),
|
|
85
|
+
}),
|
|
86
|
+
{ operation: 'deleteActivity', id },
|
|
87
|
+
)
|
|
55
88
|
},
|
|
56
89
|
}
|
|
57
90
|
}
|
|
@@ -5,7 +5,29 @@ import { mapCommentSummary, type NotesDataAdapter } from '@open-mercato/ui/backe
|
|
|
5
5
|
|
|
6
6
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export type ResourceNotesGuardedMutation = <T>(
|
|
9
|
+
runner: () => Promise<T>,
|
|
10
|
+
payload?: Record<string, unknown>,
|
|
11
|
+
) => Promise<T>
|
|
12
|
+
|
|
13
|
+
export type CreateResourceNotesAdapterOptions = {
|
|
14
|
+
runMutation?: ResourceNotesGuardedMutation
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createResourceNotesAdapter(
|
|
18
|
+
translator: Translator,
|
|
19
|
+
options: CreateResourceNotesAdapterOptions = {},
|
|
20
|
+
): NotesDataAdapter {
|
|
21
|
+
const runWrite = async <T,>(
|
|
22
|
+
runner: () => Promise<T>,
|
|
23
|
+
payload: Record<string, unknown>,
|
|
24
|
+
): Promise<T> => {
|
|
25
|
+
if (options.runMutation) {
|
|
26
|
+
return options.runMutation(runner, payload)
|
|
27
|
+
}
|
|
28
|
+
return runner()
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
return {
|
|
10
32
|
list: async ({ entityId }) => {
|
|
11
33
|
const params = new URLSearchParams()
|
|
@@ -19,19 +41,23 @@ export function createResourceNotesAdapter(translator: Translator): NotesDataAda
|
|
|
19
41
|
return items.map(mapCommentSummary)
|
|
20
42
|
},
|
|
21
43
|
create: async ({ entityId, body, appearanceIcon, appearanceColor }) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
const requestBody = {
|
|
45
|
+
entityId,
|
|
46
|
+
body,
|
|
47
|
+
appearanceIcon: appearanceIcon ?? undefined,
|
|
48
|
+
appearanceColor: appearanceColor ?? undefined,
|
|
49
|
+
}
|
|
50
|
+
const response = await runWrite(
|
|
51
|
+
() => apiCallOrThrow<Record<string, unknown>>(
|
|
52
|
+
'/api/resources/comments',
|
|
53
|
+
{
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
body: JSON.stringify(requestBody),
|
|
57
|
+
},
|
|
58
|
+
{ errorMessage: translator('resources.resources.detail.notes.error', 'Failed to save note.') },
|
|
59
|
+
),
|
|
60
|
+
{ operation: 'createNote', entityId },
|
|
35
61
|
)
|
|
36
62
|
return response.result ?? {}
|
|
37
63
|
},
|
|
@@ -40,24 +66,30 @@ export function createResourceNotesAdapter(translator: Translator): NotesDataAda
|
|
|
40
66
|
if (patch.body !== undefined) payload.body = patch.body
|
|
41
67
|
if (patch.appearanceIcon !== undefined) payload.appearanceIcon = patch.appearanceIcon
|
|
42
68
|
if (patch.appearanceColor !== undefined) payload.appearanceColor = patch.appearanceColor
|
|
43
|
-
await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
await runWrite(
|
|
70
|
+
() => apiCallOrThrow(
|
|
71
|
+
'/api/resources/comments',
|
|
72
|
+
{
|
|
73
|
+
method: 'PUT',
|
|
74
|
+
headers: { 'content-type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(payload),
|
|
76
|
+
},
|
|
77
|
+
{ errorMessage: translator('resources.resources.detail.notes.updateError', 'Failed to update note.') },
|
|
78
|
+
),
|
|
79
|
+
{ operation: 'updateNote', id },
|
|
51
80
|
)
|
|
52
81
|
},
|
|
53
82
|
delete: async ({ id }) => {
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
83
|
+
await runWrite(
|
|
84
|
+
() => apiCallOrThrow(
|
|
85
|
+
`/api/resources/comments?id=${encodeURIComponent(id)}`,
|
|
86
|
+
{
|
|
87
|
+
method: 'DELETE',
|
|
88
|
+
headers: { 'content-type': 'application/json' },
|
|
89
|
+
},
|
|
90
|
+
{ errorMessage: translator('resources.resources.detail.notes.deleteError', 'Failed to delete note') },
|
|
91
|
+
),
|
|
92
|
+
{ operation: 'deleteNote', id },
|
|
61
93
|
)
|
|
62
94
|
},
|
|
63
95
|
}
|