@open-mercato/core 0.5.1-develop.2949.009dcdd2d5 → 0.5.1-develop.2954.610bab2d08
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/helpers/integration/salesUi.js +25 -23
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/api_docs/frontend/docs/api/Explorer.js +24 -24
- package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js +15 -7
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/attachments/fields/attachment.js +4 -6
- package/dist/modules/attachments/fields/attachment.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +63 -23
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +26 -26
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/business_rules/components/ActionRow.js +36 -25
- package/dist/modules/business_rules/components/ActionRow.js.map +2 -2
- package/dist/modules/business_rules/components/ConditionGroup.js +14 -5
- package/dist/modules/business_rules/components/ConditionGroup.js.map +2 -2
- package/dist/modules/business_rules/components/ConditionRow.js +19 -10
- package/dist/modules/business_rules/components/ConditionRow.js.map +2 -2
- package/dist/modules/business_rules/components/RuleSetMembers.js +16 -10
- package/dist/modules/business_rules/components/RuleSetMembers.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +30 -34
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +220 -223
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/PriceKindSettings.js +20 -19
- package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
- package/dist/modules/catalog/components/products/ProductUomSection.js +42 -37
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +2 -2
- package/dist/modules/catalog/components/products/VariantBuilder.js +22 -18
- package/dist/modules/catalog/components/products/VariantBuilder.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +18 -26
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +4 -6
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +5 -4
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +19 -7
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +24 -21
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/components/AddressEditor.js +24 -7
- package/dist/modules/customers/components/AddressEditor.js.map +2 -2
- package/dist/modules/customers/components/AddressFormatSettings.js +35 -25
- package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityForm.js +20 -12
- package/dist/modules/customers/components/detail/ActivityForm.js.map +2 -2
- package/dist/modules/customers/components/detail/AnnualRevenueField.js +2 -2
- package/dist/modules/customers/components/detail/AnnualRevenueField.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +19 -14
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +16 -12
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js +3 -2
- package/dist/modules/customers/widgets/dashboard/customer-todos/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/new-customers/widget.client.js +18 -10
- package/dist/modules/customers/widgets/dashboard/new-customers/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/new-deals/widget.client.js +3 -2
- package/dist/modules/customers/widgets/dashboard/new-deals/widget.client.js.map +2 -2
- package/dist/modules/customers/widgets/dashboard/next-interactions/widget.client.js +3 -2
- package/dist/modules/customers/widgets/dashboard/next-interactions/widget.client.js.map +2 -2
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +27 -28
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js +14 -6
- package/dist/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js +14 -6
- package/dist/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js +3 -2
- package/dist/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js +3 -2
- package/dist/modules/dashboards/widgets/dashboard/top-customers/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js +17 -8
- package/dist/modules/dashboards/widgets/dashboard/top-products/widget.client.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +40 -23
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +15 -6
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/dictionaries/components/AppearanceSelector.js +4 -4
- package/dist/modules/dictionaries/components/AppearanceSelector.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js +4 -5
- package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js +22 -14
- package/dist/modules/dictionaries/components/DictionaryEntrySelect.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +18 -13
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +23 -19
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/feature_toggles/components/formConfig.js +17 -9
- package/dist/modules/feature_toggles/components/formConfig.js.map +2 -2
- package/dist/modules/feature_toggles/components/overrideFormConfig.js +17 -9
- package/dist/modules/feature_toggles/components/overrideFormConfig.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +15 -8
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js +37 -22
- package/dist/modules/inbox_ops/components/proposals/EditActionDialog.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +22 -17
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +12 -6
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +19 -12
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/resources/components/ResourceCrudForm.js +15 -10
- package/dist/modules/resources/components/ResourceCrudForm.js.map +3 -3
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +15 -18
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/components/ProviderFieldInput.js +23 -20
- package/dist/modules/sales/components/ProviderFieldInput.js.map +2 -2
- package/dist/modules/sales/components/ShippingMethodsSettings.js +25 -17
- package/dist/modules/sales/components/ShippingMethodsSettings.js.map +3 -3
- package/dist/modules/sales/components/channels/ChannelOfferForm.js +35 -42
- package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +87 -90
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentDialog.js +17 -6
- package/dist/modules/sales/components/documents/AdjustmentDialog.js.map +3 -3
- package/dist/modules/sales/components/documents/LineItemDialog.js +42 -25
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +96 -87
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js +20 -11
- package/dist/modules/sales/widgets/dashboard/new-orders/widget.client.js.map +2 -2
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js +20 -11
- package/dist/modules/sales/widgets/dashboard/new-quotes/widget.client.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js +36 -22
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js.map +2 -2
- package/dist/modules/staff/components/TeamMemberForm.js +14 -9
- package/dist/modules/staff/components/TeamMemberForm.js.map +3 -3
- package/dist/modules/workflows/backend/tasks/[id]/page.js +42 -21
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/components/ActivitiesEditor.js +14 -6
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +3 -3
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +25 -17
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +3 -3
- package/dist/modules/workflows/components/EdgeEditDialog.js +48 -45
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +90 -90
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/StepsEditor.js +14 -6
- package/dist/modules/workflows/components/StepsEditor.js.map +3 -3
- package/dist/modules/workflows/components/TransitionsEditor.js +31 -26
- package/dist/modules/workflows/components/TransitionsEditor.js.map +3 -3
- package/dist/modules/workflows/components/fields/ActivityArrayEditor.js +19 -11
- package/dist/modules/workflows/components/fields/ActivityArrayEditor.js.map +3 -3
- package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js +12 -14
- package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +24 -16
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +3 -3
- package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js +12 -13
- package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileTaskForm.js +12 -8
- package/dist/modules/workflows/components/mobile/MobileTaskForm.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +43 -46
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/package.json +3 -3
- package/src/helpers/integration/salesUi.ts +40 -30
- package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +25 -19
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +21 -11
- package/src/modules/attachments/fields/attachment.tsx +4 -6
- package/src/modules/auth/api/users/route.ts +75 -25
- package/src/modules/auth/backend/users/create/page.tsx +16 -20
- package/src/modules/business_rules/components/ActionRow.tsx +51 -32
- package/src/modules/business_rules/components/ConditionGroup.tsx +20 -9
- package/src/modules/business_rules/components/ConditionRow.tsx +24 -15
- package/src/modules/business_rules/components/RuleSetMembers.tsx +23 -13
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +47 -53
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +84 -87
- package/src/modules/catalog/components/PriceKindSettings.tsx +9 -9
- package/src/modules/catalog/components/products/ProductUomSection.tsx +85 -83
- package/src/modules/catalog/components/products/VariantBuilder.tsx +49 -33
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +12 -27
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +4 -6
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +5 -4
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +28 -15
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +37 -26
- package/src/modules/customers/components/AddressEditor.tsx +30 -16
- package/src/modules/customers/components/AddressFormatSettings.tsx +25 -19
- package/src/modules/customers/components/detail/ActivityForm.tsx +35 -23
- package/src/modules/customers/components/detail/AnnualRevenueField.tsx +2 -2
- package/src/modules/customers/components/detail/DealForm.tsx +33 -20
- package/src/modules/customers/components/formConfig.tsx +25 -17
- package/src/modules/customers/widgets/dashboard/customer-todos/widget.client.tsx +3 -2
- package/src/modules/customers/widgets/dashboard/new-customers/widget.client.tsx +21 -11
- package/src/modules/customers/widgets/dashboard/new-deals/widget.client.tsx +3 -2
- package/src/modules/customers/widgets/dashboard/next-interactions/widget.client.tsx +3 -2
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +17 -22
- package/src/modules/dashboards/widgets/dashboard/orders-by-status/widget.client.tsx +17 -7
- package/src/modules/dashboards/widgets/dashboard/revenue-trend/widget.client.tsx +20 -10
- package/src/modules/dashboards/widgets/dashboard/sales-by-region/widget.client.tsx +3 -2
- package/src/modules/dashboards/widgets/dashboard/top-customers/widget.client.tsx +3 -2
- package/src/modules/dashboards/widgets/dashboard/top-products/widget.client.tsx +20 -9
- package/src/modules/data_sync/backend/data-sync/page.tsx +64 -38
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +18 -7
- package/src/modules/dictionaries/components/AppearanceSelector.tsx +4 -4
- package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +3 -4
- package/src/modules/dictionaries/components/DictionaryEntrySelect.tsx +27 -21
- package/src/modules/dictionaries/fields/dictionary.tsx +36 -23
- package/src/modules/entities/components/EncryptionManager.tsx +49 -33
- package/src/modules/feature_toggles/components/formConfig.tsx +20 -10
- package/src/modules/feature_toggles/components/overrideFormConfig.tsx +20 -10
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +19 -10
- package/src/modules/inbox_ops/components/proposals/EditActionDialog.tsx +49 -26
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +20 -11
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -9
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +34 -21
- package/src/modules/resources/components/ResourceCrudForm.tsx +24 -15
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +12 -15
- package/src/modules/sales/components/ProviderFieldInput.tsx +26 -17
- package/src/modules/sales/components/ShippingMethodsSettings.tsx +28 -20
- package/src/modules/sales/components/channels/ChannelOfferForm.tsx +51 -46
- package/src/modules/sales/components/documents/AddressesSection.tsx +78 -76
- package/src/modules/sales/components/documents/AdjustmentDialog.tsx +27 -15
- package/src/modules/sales/components/documents/LineItemDialog.tsx +69 -51
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +98 -87
- package/src/modules/sales/widgets/dashboard/new-orders/widget.client.tsx +23 -12
- package/src/modules/sales/widgets/dashboard/new-quotes/widget.client.tsx +23 -12
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.tsx +35 -19
- package/src/modules/staff/components/TeamMemberForm.tsx +23 -14
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +51 -23
- package/src/modules/workflows/components/ActivitiesEditor.tsx +20 -10
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +28 -18
- package/src/modules/workflows/components/EdgeEditDialog.tsx +51 -40
- package/src/modules/workflows/components/NodeEditDialog.tsx +81 -77
- package/src/modules/workflows/components/StepsEditor.tsx +20 -10
- package/src/modules/workflows/components/TransitionsEditor.tsx +61 -44
- package/src/modules/workflows/components/fields/ActivityArrayEditor.tsx +22 -12
- package/src/modules/workflows/components/fields/BusinessRuleConditionsEditor.tsx +9 -13
- package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +27 -17
- package/src/modules/workflows/components/fields/StartPreConditionsEditor.tsx +9 -12
- package/src/modules/workflows/components/mobile/MobileTaskForm.tsx +19 -11
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +71 -60
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2954.610bab2d08",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -237,10 +237,10 @@
|
|
|
237
237
|
"ts-pattern": "^5.0.0"
|
|
238
238
|
},
|
|
239
239
|
"peerDependencies": {
|
|
240
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
240
|
+
"@open-mercato/shared": "0.5.1-develop.2954.610bab2d08"
|
|
241
241
|
},
|
|
242
242
|
"devDependencies": {
|
|
243
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
243
|
+
"@open-mercato/shared": "0.5.1-develop.2954.610bab2d08",
|
|
244
244
|
"@testing-library/dom": "^10.4.1",
|
|
245
245
|
"@testing-library/jest-dom": "^6.9.1",
|
|
246
246
|
"@testing-library/react": "^16.3.1",
|
|
@@ -258,20 +258,23 @@ async function ensureSalesDocumentFixtures(
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
async function selectFirstAddressIfAvailable(page: Page): Promise<void> {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.
|
|
261
|
+
// Radix Select: trigger has placeholder text "Select address" rendered inside
|
|
262
|
+
const addressTrigger = page
|
|
263
|
+
.locator('[role="combobox"]')
|
|
264
|
+
.filter({ hasText: /Select address/i })
|
|
264
265
|
.first();
|
|
265
|
-
if ((await
|
|
266
|
-
if (!(await
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
await
|
|
266
|
+
if ((await addressTrigger.count()) === 0) return;
|
|
267
|
+
if (!(await addressTrigger.isEnabled().catch(() => false))) return;
|
|
268
|
+
|
|
269
|
+
await addressTrigger.click();
|
|
270
|
+
// Pick the first available option from the portal-rendered listbox
|
|
271
|
+
const firstOption = page.getByRole('option').first();
|
|
272
|
+
if ((await firstOption.count()) === 0) {
|
|
273
|
+
// Close listbox if no options
|
|
274
|
+
await page.keyboard.press('Escape');
|
|
275
|
+
return;
|
|
274
276
|
}
|
|
277
|
+
await firstOption.click();
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
async function ensureShippingMethodFixture(page: Page): Promise<void> {
|
|
@@ -997,12 +1000,16 @@ export async function addCustomLine(page: Page, options: AddLineOptions): Promis
|
|
|
997
1000
|
await dialog.getByRole('textbox', { name: '1' }).fill(String(options.quantity));
|
|
998
1001
|
|
|
999
1002
|
if (options.taxClassName) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
.
|
|
1003
|
+
// Radix Select inside Dialog — force click on option to bypass overlay
|
|
1004
|
+
const taxClassTrigger = dialog
|
|
1005
|
+
.locator('[role="combobox"]')
|
|
1006
|
+
.filter({ hasText: /No tax class selected/i })
|
|
1003
1007
|
.first();
|
|
1004
|
-
if ((await
|
|
1005
|
-
await
|
|
1008
|
+
if ((await taxClassTrigger.count()) > 0) {
|
|
1009
|
+
await taxClassTrigger.click();
|
|
1010
|
+
const opt = page.getByRole('option', { name: options.taxClassName, exact: true });
|
|
1011
|
+
await opt.first().waitFor({ state: 'visible', timeout: 3_000 });
|
|
1012
|
+
await opt.first().click({ force: true });
|
|
1006
1013
|
}
|
|
1007
1014
|
}
|
|
1008
1015
|
|
|
@@ -1068,26 +1075,29 @@ export async function addAdjustment(page: Page, options: AddAdjustmentOptions):
|
|
|
1068
1075
|
const adjustmentRow = page.getByRole('row', { name: new RegExp(escapeRegExp(options.label), 'i') });
|
|
1069
1076
|
const fillAdjustmentForm = async (): Promise<void> => {
|
|
1070
1077
|
await dialog.getByText(/Loading adjustments/i).waitFor({ state: 'hidden', timeout: 3_000 }).catch(() => {});
|
|
1071
|
-
|
|
1072
|
-
|
|
1078
|
+
// Radix Select trigger — target by CrudForm field id (kind picker)
|
|
1079
|
+
const kindTrigger = dialog.locator('[data-crud-field-id="kind"] [role="combobox"]').first();
|
|
1080
|
+
await expect(kindTrigger).toBeVisible({ timeout: TEST_WAIT_TIMEOUT_MS });
|
|
1073
1081
|
|
|
1074
1082
|
const labelInput = dialog.getByPlaceholder(/e\.g\. Shipping fee/i).first();
|
|
1075
1083
|
await expect(labelInput).toBeVisible({ timeout: TEST_WAIT_TIMEOUT_MS });
|
|
1076
1084
|
await labelInput.fill(options.label);
|
|
1077
1085
|
await expect(labelInput).toHaveValue(options.label, { timeout: 2_000 });
|
|
1078
1086
|
|
|
1079
|
-
if ((await
|
|
1087
|
+
if ((await kindTrigger.count()) > 0) {
|
|
1080
1088
|
const expectedKindValue = normalizeAdjustmentKindValue(options.kindLabel ?? 'Surcharge');
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1089
|
+
const kindLabel = options.kindLabel ?? 'Surcharge';
|
|
1090
|
+
// Open Radix Select via click; then drive selection via keyboard so the
|
|
1091
|
+
// listbox keystroke matching jumps to the desired option (Discount /
|
|
1092
|
+
// Surcharge / etc.). This sidesteps modal-backdrop click interception
|
|
1093
|
+
// that occurs when a Radix Select is rendered inside a Radix Dialog.
|
|
1094
|
+
await kindTrigger.click();
|
|
1095
|
+
await page.waitForTimeout(150);
|
|
1096
|
+
// Type the first letter to jump to the matching option
|
|
1097
|
+
await page.keyboard.type(kindLabel.charAt(0));
|
|
1098
|
+
await page.waitForTimeout(150);
|
|
1099
|
+
await page.keyboard.press('Enter');
|
|
1100
|
+
void expectedKindValue;
|
|
1091
1101
|
}
|
|
1092
1102
|
|
|
1093
1103
|
const fixedAmountButton = dialog.getByRole('button', { name: /^Fixed amount$/i }).first();
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
4
4
|
import Image from 'next/image'
|
|
5
5
|
import Link from 'next/link'
|
|
6
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from '@open-mercato/ui/primitives/select'
|
|
6
14
|
|
|
7
15
|
const METHOD_STYLES: Record<string, string> = {
|
|
8
16
|
GET: 'bg-emerald-100 text-emerald-800 border border-emerald-200',
|
|
@@ -503,12 +511,12 @@ export default function ApiDocsExplorer(props: ApiDocsExplorerProps) {
|
|
|
503
511
|
</div>
|
|
504
512
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
505
513
|
<div className="flex flex-1 items-center gap-3">
|
|
506
|
-
<
|
|
514
|
+
<Input
|
|
507
515
|
type="search"
|
|
508
516
|
value={searchTerm}
|
|
509
517
|
onChange={(event) => setSearchTerm(event.target.value)}
|
|
510
518
|
placeholder="Search endpoints by path or summary"
|
|
511
|
-
className="flex-1
|
|
519
|
+
className="flex-1"
|
|
512
520
|
/>
|
|
513
521
|
</div>
|
|
514
522
|
<div className="flex flex-wrap gap-2 text-xs">
|
|
@@ -1310,17 +1318,18 @@ function TesterPanel(props: TesterPanelProps) {
|
|
|
1310
1318
|
|
|
1311
1319
|
<label className="space-y-2 text-sm">
|
|
1312
1320
|
<span className="font-medium text-foreground">Base URL</span>
|
|
1313
|
-
<
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1321
|
+
<Select value={baseUrl} onValueChange={(value) => setBaseUrl(value)}>
|
|
1322
|
+
<SelectTrigger>
|
|
1323
|
+
<SelectValue />
|
|
1324
|
+
</SelectTrigger>
|
|
1325
|
+
<SelectContent>
|
|
1326
|
+
{mergedBaseUrls.map((server) => (
|
|
1327
|
+
<SelectItem key={server.url} value={server.url}>
|
|
1328
|
+
{server.url} {server.description ? `— ${server.description}` : ''}
|
|
1329
|
+
</SelectItem>
|
|
1330
|
+
))}
|
|
1331
|
+
</SelectContent>
|
|
1332
|
+
</Select>
|
|
1324
1333
|
{!baseUrl.trim() && requestPreview?.usesPlaceholderBase ? (
|
|
1325
1334
|
<p className="text-xs text-muted-foreground">
|
|
1326
1335
|
Examples default to {PLACEHOLDER_BASE_URL}. Update the base URL to match your environment.
|
|
@@ -1333,12 +1342,11 @@ function TesterPanel(props: TesterPanelProps) {
|
|
|
1333
1342
|
|
|
1334
1343
|
<label className="space-y-2 text-sm">
|
|
1335
1344
|
<span className="font-medium text-foreground">API key</span>
|
|
1336
|
-
<
|
|
1345
|
+
<Input
|
|
1337
1346
|
type="text"
|
|
1338
1347
|
value={apiKey}
|
|
1339
1348
|
onChange={(event) => setApiKey(event.target.value)}
|
|
1340
1349
|
placeholder="Paste your API key secret (omk_…)"
|
|
1341
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20"
|
|
1342
1350
|
/>
|
|
1343
1351
|
</label>
|
|
1344
1352
|
|
|
@@ -1352,7 +1360,7 @@ function TesterPanel(props: TesterPanelProps) {
|
|
|
1352
1360
|
<span>{parameter.name}</span>
|
|
1353
1361
|
{parameter.required ? <span className="text-amber-600">required</span> : null}
|
|
1354
1362
|
</div>
|
|
1355
|
-
<
|
|
1363
|
+
<Input
|
|
1356
1364
|
type="text"
|
|
1357
1365
|
value={pathValues[parameter.name] ?? ''}
|
|
1358
1366
|
onChange={(event) =>
|
|
@@ -1362,7 +1370,6 @@ function TesterPanel(props: TesterPanelProps) {
|
|
|
1362
1370
|
}))
|
|
1363
1371
|
}
|
|
1364
1372
|
placeholder={parameter.description ?? ''}
|
|
1365
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20"
|
|
1366
1373
|
/>
|
|
1367
1374
|
</label>
|
|
1368
1375
|
))}
|
|
@@ -1380,7 +1387,7 @@ function TesterPanel(props: TesterPanelProps) {
|
|
|
1380
1387
|
<span>{parameter.name}</span>
|
|
1381
1388
|
{parameter.required ? <span className="text-amber-600">required</span> : null}
|
|
1382
1389
|
</div>
|
|
1383
|
-
<
|
|
1390
|
+
<Input
|
|
1384
1391
|
type="text"
|
|
1385
1392
|
value={queryValues[parameter.name] ?? ''}
|
|
1386
1393
|
onChange={(event) =>
|
|
@@ -1390,7 +1397,6 @@ function TesterPanel(props: TesterPanelProps) {
|
|
|
1390
1397
|
}))
|
|
1391
1398
|
}
|
|
1392
1399
|
placeholder={parameter.description ?? ''}
|
|
1393
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus-visible:border-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20"
|
|
1394
1400
|
/>
|
|
1395
1401
|
</label>
|
|
1396
1402
|
))}
|
|
@@ -7,6 +7,13 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
8
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
9
9
|
import { Label } from '@open-mercato/ui/primitives/label'
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from '@open-mercato/ui/primitives/select'
|
|
10
17
|
import {
|
|
11
18
|
Dialog,
|
|
12
19
|
DialogContent,
|
|
@@ -400,18 +407,21 @@ export function AttachmentPartitionSettings() {
|
|
|
400
407
|
<Label htmlFor="partition-ocr-model">
|
|
401
408
|
{t('attachments.partitions.form.ocrModelLabel', 'OCR Model')}
|
|
402
409
|
</Label>
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
value={form.ocrModel}
|
|
407
|
-
onChange={(event) => setForm((prev) => ({ ...prev, ocrModel: event.target.value }))}
|
|
410
|
+
<Select
|
|
411
|
+
value={form.ocrModel || undefined}
|
|
412
|
+
onValueChange={(value) => setForm((prev) => ({ ...prev, ocrModel: value ?? '' }))}
|
|
408
413
|
>
|
|
409
|
-
|
|
410
|
-
<
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
414
|
+
<SelectTrigger id="partition-ocr-model">
|
|
415
|
+
<SelectValue />
|
|
416
|
+
</SelectTrigger>
|
|
417
|
+
<SelectContent>
|
|
418
|
+
{OCR_MODEL_OPTIONS.map((option) => (
|
|
419
|
+
<SelectItem key={option.value} value={option.value}>
|
|
420
|
+
{t(`attachments.partitions.form.ocrModelOptions.${option.value || 'default'}`, option.label)}
|
|
421
|
+
</SelectItem>
|
|
422
|
+
))}
|
|
423
|
+
</SelectContent>
|
|
424
|
+
</Select>
|
|
415
425
|
<p className="text-xs text-muted-foreground">
|
|
416
426
|
{t(
|
|
417
427
|
'attachments.partitions.form.ocrModelHelp',
|
|
@@ -4,6 +4,7 @@ import { FieldRegistry } from '@open-mercato/ui/backend/fields/registry'
|
|
|
4
4
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
5
5
|
import type { CustomFieldDefDto } from '@open-mercato/ui/backend/utils/customFieldDefs'
|
|
6
6
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
7
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
9
|
import { Upload } from 'lucide-react'
|
|
9
10
|
|
|
@@ -186,8 +187,7 @@ function AttachmentDefEditor({ def, onChange }: { def: AttachmentFieldDef; onCha
|
|
|
186
187
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
187
188
|
<div className="space-y-2">
|
|
188
189
|
<label className="text-xs font-medium">Max file size (MB)</label>
|
|
189
|
-
<
|
|
190
|
-
className="w-full rounded border px-2 py-1 text-sm"
|
|
190
|
+
<Input
|
|
191
191
|
type="number"
|
|
192
192
|
min={0}
|
|
193
193
|
placeholder="e.g., 10"
|
|
@@ -198,8 +198,7 @@ function AttachmentDefEditor({ def, onChange }: { def: AttachmentFieldDef; onCha
|
|
|
198
198
|
</div>
|
|
199
199
|
<div className="space-y-2">
|
|
200
200
|
<label className="text-xs font-medium">Accepted extensions</label>
|
|
201
|
-
<
|
|
202
|
-
className="w-full rounded border px-2 py-1 text-sm"
|
|
201
|
+
<Input
|
|
203
202
|
placeholder="e.g., pdf, jpg, png"
|
|
204
203
|
value={exts}
|
|
205
204
|
onChange={(e) => setExts(e.target.value)}
|
|
@@ -209,8 +208,7 @@ function AttachmentDefEditor({ def, onChange }: { def: AttachmentFieldDef; onCha
|
|
|
209
208
|
</div>
|
|
210
209
|
<div className="space-y-2">
|
|
211
210
|
<label className="text-xs font-medium">Partition code</label>
|
|
212
|
-
<
|
|
213
|
-
className="w-full rounded border px-2 py-1 text-sm"
|
|
211
|
+
<Input
|
|
214
212
|
placeholder="e.g., productsMedia"
|
|
215
213
|
value={partition}
|
|
216
214
|
onChange={(e) => setPartition(e.target.value)}
|
|
@@ -15,6 +15,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
|
|
|
15
15
|
import { userCrudEvents, userCrudIndexer } from '@open-mercato/core/modules/auth/commands/users'
|
|
16
16
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
17
17
|
import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
18
|
+
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
18
19
|
import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
|
|
19
20
|
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
20
21
|
import { sql } from 'kysely'
|
|
@@ -156,14 +157,15 @@ export async function GET(req: Request) {
|
|
|
156
157
|
console.error('users: failed to resolve rbac', err)
|
|
157
158
|
}
|
|
158
159
|
const { id, page, pageSize, search, organizationId, roleIds } = parsed.data
|
|
159
|
-
const
|
|
160
|
+
const filters: any[] = [{ deletedAt: null }]
|
|
161
|
+
const actorTenantId = auth.tenantId ? String(auth.tenantId) : null
|
|
160
162
|
if (!isSuperAdmin) {
|
|
161
|
-
if (!
|
|
163
|
+
if (!actorTenantId) {
|
|
162
164
|
return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })
|
|
163
165
|
}
|
|
164
|
-
|
|
166
|
+
filters.push({ tenantId: actorTenantId })
|
|
165
167
|
}
|
|
166
|
-
if (organizationId)
|
|
168
|
+
if (organizationId) filters.push({ organizationId })
|
|
167
169
|
let idFilter: Set<string> | null = id ? new Set([id]) : null
|
|
168
170
|
if (Array.isArray(roleIds) && roleIds.length > 0) {
|
|
169
171
|
const uniqueRoleIds = Array.from(new Set(roleIds))
|
|
@@ -183,33 +185,81 @@ export async function GET(req: Request) {
|
|
|
183
185
|
}
|
|
184
186
|
if (!idFilter || idFilter.size === 0) return NextResponse.json({ items: [], total: 0, totalPages: 1 })
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
188
|
+
const trimmedSearch = typeof search === 'string' ? search.trim() : ''
|
|
189
|
+
if (trimmedSearch) {
|
|
190
|
+
// Email is encrypted at rest, so plaintext search must go through search_tokens.
|
|
189
191
|
const tenantScope: string | null | undefined = isSuperAdmin ? undefined : auth.tenantId ?? null
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
192
|
+
const searchFilters: any[] = []
|
|
193
|
+
|
|
194
|
+
const matchedIds = await findUserIdsBySearchTokens(em, E.auth.user, trimmedSearch, tenantScope)
|
|
195
|
+
if (matchedIds && matchedIds.length) {
|
|
196
|
+
searchFilters.push({ id: { $in: matchedIds as any } })
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const searchPattern = `%${escapeLikePattern(trimmedSearch)}%`
|
|
200
|
+
const organizationSearchFilters: any[] = [
|
|
201
|
+
{ deletedAt: null },
|
|
202
|
+
{ name: { $ilike: searchPattern } },
|
|
203
|
+
]
|
|
204
|
+
if (tenantScope) {
|
|
205
|
+
organizationSearchFilters.push({ tenant: tenantScope })
|
|
206
|
+
}
|
|
207
|
+
const matchingOrganizations = await em.find(
|
|
208
|
+
Organization,
|
|
209
|
+
organizationSearchFilters.length > 1 ? { $and: organizationSearchFilters } : organizationSearchFilters[0],
|
|
210
|
+
)
|
|
211
|
+
const matchingOrganizationIds = matchingOrganizations
|
|
212
|
+
.map((org) => (org?.id ? String(org.id) : null))
|
|
213
|
+
.filter((orgId): orgId is string => typeof orgId === 'string' && orgId.length > 0)
|
|
214
|
+
if (matchingOrganizationIds.length) {
|
|
215
|
+
searchFilters.push({ organizationId: { $in: matchingOrganizationIds as any } })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const roleSearchFilters: any[] = [
|
|
219
|
+
{ deletedAt: null },
|
|
220
|
+
{ name: { $ilike: searchPattern } },
|
|
221
|
+
]
|
|
222
|
+
if (tenantScope) {
|
|
223
|
+
roleSearchFilters.push({ $or: [{ tenantId: tenantScope }, { tenantId: null }] })
|
|
224
|
+
}
|
|
225
|
+
const matchingRoles = await em.find(
|
|
226
|
+
Role,
|
|
227
|
+
roleSearchFilters.length > 1 ? { $and: roleSearchFilters } : roleSearchFilters[0],
|
|
228
|
+
)
|
|
229
|
+
const matchingRoleIds = matchingRoles
|
|
230
|
+
.map((role) => (role?.id ? String(role.id) : null))
|
|
231
|
+
.filter((roleId): roleId is string => typeof roleId === 'string' && roleId.length > 0)
|
|
232
|
+
if (matchingRoleIds.length) {
|
|
233
|
+
const roleSearchLinks = await em.find(
|
|
234
|
+
UserRole,
|
|
235
|
+
{ role: { $in: matchingRoleIds as any } } as any,
|
|
236
|
+
)
|
|
237
|
+
const matchingRoleUserIds = Array.from(new Set(
|
|
238
|
+
roleSearchLinks
|
|
239
|
+
.map((link) => {
|
|
240
|
+
const userRef = (link as any).user
|
|
241
|
+
const userId = userRef?.id ?? userRef
|
|
242
|
+
return userId ? String(userId) : null
|
|
243
|
+
})
|
|
244
|
+
.filter((userId): userId is string => typeof userId === 'string' && userId.length > 0),
|
|
245
|
+
))
|
|
246
|
+
if (matchingRoleUserIds.length) {
|
|
247
|
+
searchFilters.push({ id: { $in: matchingRoleUserIds as any } })
|
|
205
248
|
}
|
|
206
249
|
}
|
|
250
|
+
|
|
251
|
+
if (!searchFilters.length) {
|
|
252
|
+
return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
filters.push(searchFilters.length > 1 ? { $or: searchFilters } : searchFilters[0])
|
|
207
256
|
}
|
|
208
257
|
if (idFilter && idFilter.size) {
|
|
209
|
-
|
|
258
|
+
filters.push({ id: { $in: Array.from(idFilter) as any } })
|
|
210
259
|
} else if (id) {
|
|
211
|
-
|
|
260
|
+
filters.push({ id })
|
|
212
261
|
}
|
|
262
|
+
const where = filters.length > 1 ? { $and: filters } : filters[0]
|
|
213
263
|
const [rows, count] = await em.findAndCount(User, where, { limit: pageSize, offset: (page - 1) * pageSize })
|
|
214
264
|
const userIds = rows.map((u: any) => u.id)
|
|
215
265
|
const links = userIds.length
|
|
@@ -402,7 +452,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
402
452
|
GET: {
|
|
403
453
|
summary: 'List users',
|
|
404
454
|
description:
|
|
405
|
-
'Returns users for the current tenant. Super administrators may scope the response via organization or role filters.',
|
|
455
|
+
'Returns users for the current tenant. Search matches email, organization name, and role name. Super administrators may scope the response via organization or role filters.',
|
|
406
456
|
query: querySchema,
|
|
407
457
|
responses: [
|
|
408
458
|
{ status: 200, description: 'User collection', schema: userListResponseSchema },
|
|
@@ -10,6 +10,8 @@ import { OrganizationSelect } from '@open-mercato/core/modules/directory/compone
|
|
|
10
10
|
import { TenantSelect } from '@open-mercato/core/modules/directory/components/TenantSelect'
|
|
11
11
|
import { fetchRoleOptions } from '@open-mercato/core/modules/auth/backend/users/roleOptions'
|
|
12
12
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
13
|
+
import { RadioGroup } from '@open-mercato/ui/primitives/radio'
|
|
14
|
+
import { RadioField } from '@open-mercato/ui/primitives/radio-field'
|
|
13
15
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
14
16
|
import { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
15
17
|
|
|
@@ -395,26 +397,20 @@ function DashboardWidgetSelector({
|
|
|
395
397
|
)}
|
|
396
398
|
{!error && (
|
|
397
399
|
<>
|
|
398
|
-
<
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
checked={mode === 'override'}
|
|
413
|
-
onChange={() => onModeChange('override')}
|
|
414
|
-
/>
|
|
415
|
-
{t('auth.users.widgets.mode.override', 'Override for this user')}
|
|
416
|
-
</label>
|
|
417
|
-
</div>
|
|
400
|
+
<RadioGroup
|
|
401
|
+
className="flex flex-row items-center gap-3 rounded-md border bg-muted/30 px-3 py-2"
|
|
402
|
+
value={mode}
|
|
403
|
+
onValueChange={(next) => onModeChange(next as 'inherit' | 'override')}
|
|
404
|
+
>
|
|
405
|
+
<RadioField
|
|
406
|
+
value="inherit"
|
|
407
|
+
label={t('auth.users.widgets.mode.inherit', 'Inherit from roles')}
|
|
408
|
+
/>
|
|
409
|
+
<RadioField
|
|
410
|
+
value="override"
|
|
411
|
+
label={t('auth.users.widgets.mode.override', 'Override for this user')}
|
|
412
|
+
/>
|
|
413
|
+
</RadioGroup>
|
|
418
414
|
{mode === 'override' && (
|
|
419
415
|
<div className="space-y-2">
|
|
420
416
|
{catalog.map((widget) => (
|