create-mercato-app 0.6.5-develop.4734.1.dd8285dd2e → 0.6.5-develop.4772.1.d517a085d1
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/package.json +1 -1
- package/template/package.json.template +4 -4
- package/template/src/modules/example/__integration__/todo-priority-validation.spec.ts +38 -2
- package/template/src/modules/example/__tests__/acl-dependencies.test.ts +71 -0
- package/template/src/modules/example/api/todos/route.ts +8 -0
- package/template/src/modules/example/backend/todos/[id]/edit/page.tsx +7 -1
- package/template/src/modules/example/ce.ts +5 -1
package/package.json
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"@uiw/react-markdown-preview": "^5.2.0",
|
|
81
81
|
"@uiw/react-md-editor": "^4.1.0",
|
|
82
82
|
"@xyflow/react": "^12.6.0",
|
|
83
|
-
"ai": "^6.0.
|
|
83
|
+
"ai": "^6.0.194",
|
|
84
84
|
"awilix": "^12.0.5",
|
|
85
85
|
"bcryptjs": "^3.0.3",
|
|
86
86
|
"class-variance-authority": "^0.7.1",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"lucide-react": "^1.8.0",
|
|
92
92
|
"mammoth": "^1.9.0",
|
|
93
93
|
"newrelic": "^13.19.1",
|
|
94
|
-
"next": "16.2.
|
|
94
|
+
"next": "16.2.7",
|
|
95
95
|
"pg": "8.21.0",
|
|
96
96
|
"pdfjs-dist": "^5.4.149",
|
|
97
97
|
"react": "19.2.5",
|
|
@@ -106,8 +106,8 @@
|
|
|
106
106
|
"svix": "^1.92.2",
|
|
107
107
|
"tailwind-merge": "^3.5.0",
|
|
108
108
|
"zod": "4.3.6",
|
|
109
|
-
"@stripe/react-stripe-js": "^6.
|
|
110
|
-
"@stripe/stripe-js": "^9.
|
|
109
|
+
"@stripe/react-stripe-js": "^6.5.0",
|
|
110
|
+
"@stripe/stripe-js": "^9.7.0",
|
|
111
111
|
"@open-mercato/gateway-stripe": "{{PACKAGE_VERSION}}",
|
|
112
112
|
"@open-mercato/sync-akeneo": "{{PACKAGE_VERSION}}"
|
|
113
113
|
},
|
|
@@ -91,7 +91,8 @@ test.describe('Todo priority validation', () => {
|
|
|
91
91
|
const severityField = page.locator('[data-crud-field-id="cf_severity"]').first()
|
|
92
92
|
const form = page.locator('[data-crud-field-id="title"]').first().locator('xpath=ancestor::form').first()
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
const severitySelect = severityField.getByRole('combobox').first()
|
|
95
|
+
await expect(severitySelect).toBeVisible()
|
|
95
96
|
await expect(titleInput).toBeVisible()
|
|
96
97
|
await titleInput.fill(title)
|
|
97
98
|
await expect(priorityInput).toBeVisible()
|
|
@@ -103,7 +104,8 @@ test.describe('Todo priority validation', () => {
|
|
|
103
104
|
await expect(priorityField.getByText('Priority must be <= 5')).toBeVisible()
|
|
104
105
|
|
|
105
106
|
await priorityInput.fill('5')
|
|
106
|
-
await
|
|
107
|
+
await severitySelect.click()
|
|
108
|
+
await page.getByRole('option', { name: 'Medium' }).click()
|
|
107
109
|
await form.locator('button[type="submit"]').first().click()
|
|
108
110
|
|
|
109
111
|
await expect(page).toHaveURL(/\/backend\/todos(?:\?.*)?$/)
|
|
@@ -142,4 +144,38 @@ test.describe('Todo priority validation', () => {
|
|
|
142
144
|
}
|
|
143
145
|
}
|
|
144
146
|
})
|
|
147
|
+
|
|
148
|
+
test('prefills the saved severity option label on edit', async ({ page, request }) => {
|
|
149
|
+
test.slow()
|
|
150
|
+
const { login } = await import('@open-mercato/core/helpers/integration/auth')
|
|
151
|
+
const title = `QA severity prefill ${Date.now()}`
|
|
152
|
+
let todoId: string | null = null
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const response = await apiRequest(request, 'POST', '/api/example/todos', {
|
|
156
|
+
token: adminToken,
|
|
157
|
+
data: {
|
|
158
|
+
title,
|
|
159
|
+
cf_priority: 3,
|
|
160
|
+
cf_severity: 'medium',
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
expect(response.ok(), `Failed to create todo fixture: ${response.status()}`).toBeTruthy()
|
|
164
|
+
const body = await response.json() as { id?: string }
|
|
165
|
+
todoId = body.id ?? null
|
|
166
|
+
expect(todoId).toBeTruthy()
|
|
167
|
+
|
|
168
|
+
await login(page, 'admin')
|
|
169
|
+
await page.goto(`/backend/todos/${encodeURIComponent(todoId!)}/edit`, { waitUntil: 'commit' })
|
|
170
|
+
|
|
171
|
+
await expect(page.locator('main').getByText('Edit Todo').first()).toBeVisible()
|
|
172
|
+
await expect(page.locator('[data-crud-field-id="title"] input').first()).toHaveValue(title)
|
|
173
|
+
const severityField = page.locator('[data-crud-field-id="cf_severity"]').first()
|
|
174
|
+
const severitySelect = severityField.getByRole('combobox').first()
|
|
175
|
+
await expect(severitySelect).toBeVisible()
|
|
176
|
+
await expect(severitySelect).toContainText(/medium/i)
|
|
177
|
+
} finally {
|
|
178
|
+
await deleteEntityIfExists(request, adminToken, '/api/example/todos', todoId)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
145
181
|
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveAclDependencyDiagnostics,
|
|
3
|
+
type FeatureDescriptor,
|
|
4
|
+
} from '@open-mercato/shared/security/aclDependencies'
|
|
5
|
+
import { features as exampleFeatures } from '../acl'
|
|
6
|
+
import { features as syncFeatures } from '../../example_customers_sync/acl'
|
|
7
|
+
import { features as customersFeatures } from '@open-mercato/core/modules/customers/acl'
|
|
8
|
+
|
|
9
|
+
const exampleDescriptors: FeatureDescriptor[] = exampleFeatures
|
|
10
|
+
const syncDescriptors: FeatureDescriptor[] = syncFeatures
|
|
11
|
+
|
|
12
|
+
// The example template modules exercise the dependency convention end-to-end so
|
|
13
|
+
// create-app templates ship with declared deps. `example_customers_sync` references
|
|
14
|
+
// the cross-module `customers.people.view`, so the resolved catalog must include the
|
|
15
|
+
// customers features for that reference to resolve to a registered id.
|
|
16
|
+
const resolvedCatalog: FeatureDescriptor[] = [
|
|
17
|
+
...exampleDescriptors,
|
|
18
|
+
...syncDescriptors,
|
|
19
|
+
...customersFeatures,
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const exampleOwnIds = exampleDescriptors.map((feature) => feature.id)
|
|
23
|
+
const syncOwnIds = syncDescriptors.map((feature) => feature.id)
|
|
24
|
+
|
|
25
|
+
describe('example template acl dependency declarations', () => {
|
|
26
|
+
it('declares dependsOn only against features registered in the resolved catalog', () => {
|
|
27
|
+
const granted = resolvedCatalog.map((feature) => feature.id)
|
|
28
|
+
const diagnostics = resolveAclDependencyDiagnostics(granted, resolvedCatalog)
|
|
29
|
+
const ownUnknown = diagnostics.unknownReferences.filter(
|
|
30
|
+
(ref) => ref.feature.startsWith('example.') || ref.feature.startsWith('example_customers_sync.'),
|
|
31
|
+
)
|
|
32
|
+
expect(ownUnknown).toEqual([])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('resolves cleanly with no missing deps when every feature and its deps are granted', () => {
|
|
36
|
+
const granted = resolvedCatalog.map((feature) => feature.id)
|
|
37
|
+
const diagnostics = resolveAclDependencyDiagnostics(granted, resolvedCatalog)
|
|
38
|
+
const ownMissing = diagnostics.missingDependencies.filter(
|
|
39
|
+
(dep) => dep.feature.startsWith('example.') || dep.feature.startsWith('example_customers_sync.'),
|
|
40
|
+
)
|
|
41
|
+
expect(ownMissing).toEqual([])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('flags missing example deps (not unknown) when a dependent is granted without its owner', () => {
|
|
45
|
+
const diagnostics = resolveAclDependencyDiagnostics(['example.todos.manage'], resolvedCatalog)
|
|
46
|
+
expect(diagnostics.unknownReferences).toEqual([])
|
|
47
|
+
const manage = diagnostics.missingDependencies.find(
|
|
48
|
+
(dep) => dep.feature === 'example.todos.manage',
|
|
49
|
+
)
|
|
50
|
+
expect(manage?.missing).toEqual(['example.todos.view'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('flags the cross-module sync dep as missing (not unknown) when only sync features are granted', () => {
|
|
54
|
+
const diagnostics = resolveAclDependencyDiagnostics(syncOwnIds, resolvedCatalog)
|
|
55
|
+
expect(diagnostics.unknownReferences).toEqual([])
|
|
56
|
+
const view = diagnostics.missingDependencies.find(
|
|
57
|
+
(dep) => dep.feature === 'example_customers_sync.view',
|
|
58
|
+
)
|
|
59
|
+
expect(view?.missing).toEqual(['customers.people.view'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('keeps every internal example dependency target within the example feature set', () => {
|
|
63
|
+
const exampleIds = new Set(exampleOwnIds)
|
|
64
|
+
const internalDeps = exampleDescriptors.flatMap((feature) =>
|
|
65
|
+
(feature.dependsOn ?? []).filter((dep) => dep.startsWith('example.')),
|
|
66
|
+
)
|
|
67
|
+
for (const dep of internalDeps) {
|
|
68
|
+
expect(exampleIds.has(dep)).toBe(true)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -32,6 +32,7 @@ const querySchema = z
|
|
|
32
32
|
id: z.string().uuid().optional(),
|
|
33
33
|
page: z.coerce.number().min(1).default(1),
|
|
34
34
|
pageSize: z.coerce.number().min(1).max(100).default(50),
|
|
35
|
+
ids: z.string().optional(),
|
|
35
36
|
sortField: z.string().optional().default('id'),
|
|
36
37
|
sortDir: z.enum(['asc', 'desc']).optional().default('asc'),
|
|
37
38
|
title: z.string().optional(),
|
|
@@ -96,6 +97,13 @@ export const { metadata, GET, POST, PUT, DELETE } = makeCrudRoute({
|
|
|
96
97
|
const filters: Where<BaseFields> = {}
|
|
97
98
|
const F = filters as Record<string, WhereValue>
|
|
98
99
|
// Base fields
|
|
100
|
+
if (q.ids) {
|
|
101
|
+
const ids = q.ids
|
|
102
|
+
.split(',')
|
|
103
|
+
.map((value) => value.trim())
|
|
104
|
+
.filter((value) => value.length > 0)
|
|
105
|
+
if (ids.length > 0) F.id = { $in: ids }
|
|
106
|
+
}
|
|
99
107
|
if (q.id) F.id = q.id
|
|
100
108
|
if (q.title) F.title = { $ilike: `%${q.title}%` }
|
|
101
109
|
if (q.isDone !== undefined) F.is_done = q.isDone as any
|
|
@@ -80,7 +80,7 @@ export default function EditTodoPage({ params }: { params?: { id?: string } }) {
|
|
|
80
80
|
setErr(null)
|
|
81
81
|
setIsNotFound(false)
|
|
82
82
|
try {
|
|
83
|
-
const data = await fetchCrudList<TodoItem>('example/todos', {
|
|
83
|
+
const data = await fetchCrudList<TodoItem>('example/todos', { ids: String(id), pageSize: 1 })
|
|
84
84
|
const item = data?.items?.[0]
|
|
85
85
|
if (!item) {
|
|
86
86
|
if (!cancelled) setIsNotFound(true)
|
|
@@ -94,6 +94,12 @@ export default function EditTodoPage({ params }: { params?: { id?: string } }) {
|
|
|
94
94
|
title: item.title,
|
|
95
95
|
is_done: Boolean(item.is_done),
|
|
96
96
|
...(cfInit as TodoCustomFieldValues),
|
|
97
|
+
cf_priority: extended.cf_priority ?? cfInit.cf_priority,
|
|
98
|
+
cf_severity: extended.cf_severity ?? cfInit.cf_severity,
|
|
99
|
+
cf_blocked: extended.cf_blocked ?? cfInit.cf_blocked,
|
|
100
|
+
cf_labels: extended.cf_labels ?? cfInit.cf_labels,
|
|
101
|
+
cf_assignee: extended.cf_assignee ?? cfInit.cf_assignee,
|
|
102
|
+
cf_description: extended.cf_description ?? cfInit.cf_description,
|
|
97
103
|
}
|
|
98
104
|
if (!cancelled) setInitial(init)
|
|
99
105
|
} catch (error: unknown) {
|
|
@@ -28,7 +28,11 @@ const todoFields = [
|
|
|
28
28
|
key: 'severity',
|
|
29
29
|
kind: 'select',
|
|
30
30
|
label: 'Severity',
|
|
31
|
-
options: [
|
|
31
|
+
options: [
|
|
32
|
+
{ value: 'low', label: 'Low' },
|
|
33
|
+
{ value: 'medium', label: 'Medium' },
|
|
34
|
+
{ value: 'high', label: 'High' },
|
|
35
|
+
],
|
|
32
36
|
defaultValue: 'medium',
|
|
33
37
|
filterable: true,
|
|
34
38
|
formEditable: true,
|