@open-mercato/core 0.6.6-develop.6402.1.080aab8ec9 → 0.6.6-develop.6403.1.b57089b6fe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/planner/api/availability-weekly.js +11 -1
- package/dist/modules/planner/api/availability-weekly.js.map +2 -2
- package/dist/modules/planner/commands/availability-weekly.js +34 -3
- package/dist/modules/planner/commands/availability-weekly.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +14 -6
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/planner/api/availability-weekly.ts +10 -0
- package/src/modules/planner/commands/availability-weekly.ts +46 -8
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +15 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6403.1.b57089b6fe",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -248,16 +248,16 @@
|
|
|
248
248
|
"zod": "^4.4.3"
|
|
249
249
|
},
|
|
250
250
|
"peerDependencies": {
|
|
251
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
252
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
253
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
251
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6403.1.b57089b6fe",
|
|
252
|
+
"@open-mercato/shared": "0.6.6-develop.6403.1.b57089b6fe",
|
|
253
|
+
"@open-mercato/ui": "0.6.6-develop.6403.1.b57089b6fe",
|
|
254
254
|
"react": "^19.0.0",
|
|
255
255
|
"react-dom": "^19.0.0"
|
|
256
256
|
},
|
|
257
257
|
"devDependencies": {
|
|
258
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
259
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
260
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
258
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6403.1.b57089b6fe",
|
|
259
|
+
"@open-mercato/shared": "0.6.6-develop.6403.1.b57089b6fe",
|
|
260
|
+
"@open-mercato/ui": "0.6.6-develop.6403.1.b57089b6fe",
|
|
261
261
|
"@testing-library/dom": "^10.4.1",
|
|
262
262
|
"@testing-library/jest-dom": "^6.9.1",
|
|
263
263
|
"@testing-library/react": "^16.3.1",
|
|
@@ -123,6 +123,16 @@ export const openApi = {
|
|
|
123
123
|
{ status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
|
|
124
124
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
125
125
|
{ status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
|
|
126
|
+
{
|
|
127
|
+
status: 409,
|
|
128
|
+
description: 'Optimistic lock conflict',
|
|
129
|
+
schema: z.object({
|
|
130
|
+
error: z.string(),
|
|
131
|
+
code: z.literal('optimistic_lock_conflict'),
|
|
132
|
+
currentUpdatedAt: z.string(),
|
|
133
|
+
expectedUpdatedAt: z.string(),
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
126
136
|
],
|
|
127
137
|
},
|
|
128
138
|
},
|
|
@@ -8,14 +8,24 @@ import {
|
|
|
8
8
|
type PlannerAvailabilityWeeklyReplaceInput,
|
|
9
9
|
} from '../data/validators'
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
|
-
import
|
|
11
|
+
import { emitCrudSideEffects } from '@open-mercato/shared/lib/commands/helpers'
|
|
12
12
|
import {
|
|
13
13
|
enforceCommandOptimisticLock,
|
|
14
14
|
enforceRecordGoneIsConflict,
|
|
15
15
|
} from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
16
|
+
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
17
|
+
import type { CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
|
|
18
|
+
import type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'
|
|
16
19
|
import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
|
|
20
|
+
import { plannerAvailabilityRuleSetCrudEvents } from '../lib/crud'
|
|
21
|
+
import { E } from '#generated/entities.ids.generated'
|
|
17
22
|
|
|
18
23
|
const AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'
|
|
24
|
+
const AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND = 'planner.availability-rule-set'
|
|
25
|
+
|
|
26
|
+
const availabilityRuleSetCrudIndexer: CrudIndexerConfig<PlannerAvailabilityRuleSet> = {
|
|
27
|
+
entityType: E.planner.planner_availability_rule_set,
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
// Canonical resource kind for the parent rule set, matching the tag the CRUD
|
|
21
31
|
// factory derives for `planner.availability-rule-sets.*` commands. Weekly
|
|
@@ -95,6 +105,14 @@ function toAvailabilityRuleSnapshot(record: PlannerAvailabilityRule): Availabili
|
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
function nextRuleSetUpdatedAt(current: Date | null | undefined, fallback: Date): Date {
|
|
109
|
+
const currentMs = current instanceof Date ? current.getTime() : Number.NaN
|
|
110
|
+
if (Number.isFinite(currentMs) && fallback.getTime() <= currentMs) {
|
|
111
|
+
return new Date(currentMs + 1)
|
|
112
|
+
}
|
|
113
|
+
return fallback
|
|
114
|
+
}
|
|
115
|
+
|
|
98
116
|
async function loadWeeklySnapshots(
|
|
99
117
|
em: EntityManager,
|
|
100
118
|
params: {
|
|
@@ -173,12 +191,12 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
|
|
|
173
191
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
174
192
|
const now = new Date()
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
// The weekly rules of a rule set are a sub-resource of that rule set: the
|
|
195
|
+
// parent is the optimistic-lock consistency boundary. Guard the parent's
|
|
196
|
+
// version (so a stale weekly save loses to a concurrent rule-set
|
|
197
|
+
// change/delete) and bump its `updated_at` after the replace (so a
|
|
198
|
+
// concurrent rule-set delete/update with a stale token conflicts). See #2927.
|
|
199
|
+
const touchedRuleSet = await em.transactional(async (trx): Promise<PlannerAvailabilityRuleSet | null> => {
|
|
182
200
|
let ruleSet: PlannerAvailabilityRuleSet | null = null
|
|
183
201
|
if (parsed.subjectType === 'ruleset') {
|
|
184
202
|
ruleSet = await trx.findOne(PlannerAvailabilityRuleSet, {
|
|
@@ -250,13 +268,30 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
|
|
|
250
268
|
})
|
|
251
269
|
|
|
252
270
|
if (ruleSet) {
|
|
253
|
-
ruleSet.updatedAt = now
|
|
271
|
+
ruleSet.updatedAt = nextRuleSetUpdatedAt(ruleSet.updatedAt, now)
|
|
254
272
|
trx.persist(ruleSet)
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
await trx.flush()
|
|
276
|
+
return ruleSet
|
|
258
277
|
})
|
|
259
278
|
|
|
279
|
+
if (touchedRuleSet) {
|
|
280
|
+
const dataEngine = ctx.container.resolve('dataEngine') as DataEngine
|
|
281
|
+
await emitCrudSideEffects({
|
|
282
|
+
dataEngine,
|
|
283
|
+
action: 'updated',
|
|
284
|
+
entity: touchedRuleSet,
|
|
285
|
+
identifiers: {
|
|
286
|
+
id: touchedRuleSet.id,
|
|
287
|
+
organizationId: touchedRuleSet.organizationId,
|
|
288
|
+
tenantId: touchedRuleSet.tenantId,
|
|
289
|
+
},
|
|
290
|
+
events: plannerAvailabilityRuleSetCrudEvents,
|
|
291
|
+
indexer: availabilityRuleSetCrudIndexer,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
260
295
|
return { ok: true }
|
|
261
296
|
},
|
|
262
297
|
buildLog: async ({ input, snapshots, ctx }) => {
|
|
@@ -284,6 +319,9 @@ const replaceWeeklyAvailabilityCommand: CommandHandler<PlannerAvailabilityWeekly
|
|
|
284
319
|
after,
|
|
285
320
|
} satisfies WeeklyUndoPayload,
|
|
286
321
|
},
|
|
322
|
+
context: parsed.subjectType === 'ruleset'
|
|
323
|
+
? { cacheAliases: [AVAILABILITY_RULE_SET_CACHE_RESOURCE_KIND] }
|
|
324
|
+
: null,
|
|
287
325
|
}
|
|
288
326
|
},
|
|
289
327
|
undo: async ({ logEntry, ctx }) => {
|
|
@@ -19,6 +19,7 @@ import { createCrud, deleteCrud, updateCrud } from '@open-mercato/ui/backend/uti
|
|
|
19
19
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
20
20
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
21
21
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
22
|
+
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
22
23
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
23
24
|
import { ComboboxInput, TimePicker } from '@open-mercato/ui/backend/inputs'
|
|
24
25
|
import { DictionaryEntrySelect, type DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
31
32
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
32
33
|
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
33
|
-
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
34
34
|
import { parseAvailabilityRuleWindow } from '@open-mercato/core/modules/planner/lib/availabilitySchedule'
|
|
35
35
|
import { deleteAvailabilityRuleSet } from '@open-mercato/core/modules/planner/lib/deleteAvailabilityRuleSet'
|
|
36
36
|
import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
@@ -778,6 +778,12 @@ export function AvailabilityRulesEditor({
|
|
|
778
778
|
void refreshBookedEvents()
|
|
779
779
|
}, [refreshBookedEvents])
|
|
780
780
|
|
|
781
|
+
const refreshAfterRuleSetConflict = React.useCallback(() => {
|
|
782
|
+
void refreshRuleSets()
|
|
783
|
+
void refreshRuleSetRules()
|
|
784
|
+
void refreshAvailability()
|
|
785
|
+
}, [refreshAvailability, refreshRuleSetRules, refreshRuleSets])
|
|
786
|
+
|
|
781
787
|
const weeklyDraft = React.useMemo(() => buildWeeklyDraft(activeRules), [activeRules])
|
|
782
788
|
const [weeklyWindows, setWeeklyWindows] = React.useState<WeeklyWindows>(() => cloneWeeklyWindows(weeklyDraft))
|
|
783
789
|
const weeklyWindowsRef = React.useRef<WeeklyWindows>(cloneWeeklyWindows(weeklyDraft))
|
|
@@ -893,14 +899,15 @@ export function AvailabilityRulesEditor({
|
|
|
893
899
|
if (!shouldSkipRefresh) {
|
|
894
900
|
await refreshAvailability()
|
|
895
901
|
await refreshRuleSetRules()
|
|
902
|
+
if (usingRuleSet) {
|
|
903
|
+
await refreshRuleSets()
|
|
904
|
+
}
|
|
896
905
|
}
|
|
897
906
|
if (parentRuleSet) {
|
|
898
907
|
await refreshRuleSets()
|
|
899
908
|
}
|
|
900
909
|
} catch (error) {
|
|
901
|
-
if (surfaceRecordConflict(error, t))
|
|
902
|
-
return
|
|
903
|
-
}
|
|
910
|
+
if (surfaceRecordConflict(error, t, { onRefresh: refreshAfterRuleSetConflict })) return
|
|
904
911
|
const message = error instanceof Error ? error.message : listLabels.saveWeeklyError
|
|
905
912
|
flash(message, 'error')
|
|
906
913
|
} finally {
|
|
@@ -912,8 +919,9 @@ export function AvailabilityRulesEditor({
|
|
|
912
919
|
refreshAvailability,
|
|
913
920
|
refreshRuleSetRules,
|
|
914
921
|
refreshRuleSets,
|
|
915
|
-
|
|
922
|
+
refreshAfterRuleSetConflict,
|
|
916
923
|
effectiveRulesetId,
|
|
924
|
+
ruleSets,
|
|
917
925
|
subjectId,
|
|
918
926
|
subjectType,
|
|
919
927
|
t,
|
|
@@ -1180,7 +1188,7 @@ export function AvailabilityRulesEditor({
|
|
|
1180
1188
|
refreshRuleSets,
|
|
1181
1189
|
onSuccess: () => flash(listLabels.ruleSetDeleteSuccess, 'success'),
|
|
1182
1190
|
onError: (error) => {
|
|
1183
|
-
if (surfaceRecordConflict(error, t)) return
|
|
1191
|
+
if (surfaceRecordConflict(error, t, { onRefresh: refreshAfterRuleSetConflict })) return
|
|
1184
1192
|
console.error('planner.availability-rule-sets.delete', error)
|
|
1185
1193
|
const normalized = normalizeCrudServerError(error)
|
|
1186
1194
|
flash(normalized.message ?? listLabels.ruleSetDeleteError, 'error')
|
|
@@ -1195,6 +1203,7 @@ export function AvailabilityRulesEditor({
|
|
|
1195
1203
|
listLabels.ruleSetDeleteSuccess,
|
|
1196
1204
|
onRulesetChange,
|
|
1197
1205
|
refreshAvailability,
|
|
1206
|
+
refreshAfterRuleSetConflict,
|
|
1198
1207
|
refreshRuleSets,
|
|
1199
1208
|
ruleSets,
|
|
1200
1209
|
rulesetId,
|