@open-mercato/ui 0.6.1-develop.3278.1.a4d26475e4 → 0.6.1-develop.3287.1.450f4ffb56
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 +23 -14
- package/dist/backend/AppShell.js +8 -32
- package/dist/backend/AppShell.js.map +2 -2
- package/dist/backend/CrudForm.js +18 -0
- package/dist/backend/CrudForm.js.map +2 -2
- package/dist/backend/FilterBar.js +11 -14
- package/dist/backend/FilterBar.js.map +2 -2
- package/dist/backend/columns/ColumnChooserPanel.js +4 -6
- package/dist/backend/columns/ColumnChooserPanel.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +11 -7
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/EventPatternInput.js +5 -2
- package/dist/backend/inputs/EventPatternInput.js.map +2 -2
- package/dist/backend/inputs/EventSelect.js +10 -4
- package/dist/backend/inputs/EventSelect.js.map +2 -2
- package/dist/backend/inputs/LookupSelect.js +51 -47
- package/dist/backend/inputs/LookupSelect.js.map +2 -2
- package/dist/backend/inputs/PhoneNumberField.js +196 -45
- package/dist/backend/inputs/PhoneNumberField.js.map +2 -2
- package/dist/backend/inputs/SwitchableMarkdownInput.js.map +2 -2
- package/dist/backend/messages/message-compose-form-groups.js +2 -2
- package/dist/backend/messages/message-compose-form-groups.js.map +2 -2
- package/dist/backend/utils/serverErrors.js +6 -2
- package/dist/backend/utils/serverErrors.js.map +2 -2
- package/dist/portal/utils/nav.js +17 -0
- package/dist/portal/utils/nav.js.map +2 -2
- package/dist/primitives/amount-input.js +128 -0
- package/dist/primitives/amount-input.js.map +7 -0
- package/dist/primitives/button-input.js +47 -0
- package/dist/primitives/button-input.js.map +7 -0
- package/dist/primitives/card-input.js +133 -0
- package/dist/primitives/card-input.js.map +7 -0
- package/dist/primitives/email-input.js +29 -0
- package/dist/primitives/email-input.js.map +7 -0
- package/dist/primitives/password-input.js +64 -0
- package/dist/primitives/password-input.js.map +7 -0
- package/dist/primitives/search-input.js +57 -0
- package/dist/primitives/search-input.js.map +7 -0
- package/dist/primitives/select.js +43 -5
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/website-input.js +56 -0
- package/dist/primitives/website-input.js.map +7 -0
- package/package.json +3 -3
- package/src/backend/AppShell.tsx +7 -30
- package/src/backend/CrudForm.tsx +17 -0
- package/src/backend/FilterBar.tsx +5 -6
- package/src/backend/__tests__/serverErrors.test.ts +79 -1
- package/src/backend/columns/ColumnChooserPanel.tsx +4 -5
- package/src/backend/inputs/ComboboxInput.tsx +32 -26
- package/src/backend/inputs/EventPatternInput.tsx +11 -2
- package/src/backend/inputs/EventSelect.tsx +13 -4
- package/src/backend/inputs/LookupSelect.tsx +75 -53
- package/src/backend/inputs/PhoneNumberField.tsx +237 -47
- package/src/backend/inputs/SwitchableMarkdownInput.tsx +30 -0
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +4 -0
- package/src/backend/inputs/__tests__/PhoneNumberField.test.tsx +4 -0
- package/src/backend/messages/message-compose-form-groups.tsx +2 -2
- package/src/backend/utils/serverErrors.ts +15 -8
- package/src/portal/utils/__tests__/nav.test.ts +80 -0
- package/src/portal/utils/nav.ts +23 -1
- package/src/primitives/__tests__/password-input.test.tsx +93 -0
- package/src/primitives/__tests__/search-input.test.tsx +95 -0
- package/src/primitives/__tests__/zindex-overlay.test.tsx +4 -0
- package/src/primitives/amount-input.tsx +184 -0
- package/src/primitives/button-input.tsx +68 -0
- package/src/primitives/card-input.tsx +193 -0
- package/src/primitives/email-input.tsx +36 -0
- package/src/primitives/password-input.tsx +100 -0
- package/src/primitives/search-input.tsx +87 -0
- package/src/primitives/select.tsx +98 -11
- package/src/primitives/website-input.tsx +72 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { cn } from "@open-mercato/shared/lib/utils";
|
|
6
|
+
import { inputWrapperVariants, inputElementVariants } from "./input.js";
|
|
7
|
+
const WebsiteInput = React.forwardRef(
|
|
8
|
+
({ className, inputClassName, size, prefix = "https://", showPrefix = true, placeholder, disabled, ...props }, ref) => {
|
|
9
|
+
const t = useT();
|
|
10
|
+
const resolvedPlaceholder = placeholder ?? t("ui.inputs.websiteInput.placeholder", "www.example.com");
|
|
11
|
+
return /* @__PURE__ */ jsxs(
|
|
12
|
+
"div",
|
|
13
|
+
{
|
|
14
|
+
className: cn(
|
|
15
|
+
inputWrapperVariants({ size }),
|
|
16
|
+
// Override default px so prefix box can sit flush left.
|
|
17
|
+
"px-0 overflow-hidden",
|
|
18
|
+
className
|
|
19
|
+
),
|
|
20
|
+
"data-slot": "website-input-wrapper",
|
|
21
|
+
children: [
|
|
22
|
+
showPrefix ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(
|
|
23
|
+
"span",
|
|
24
|
+
{
|
|
25
|
+
className: "flex shrink-0 items-center self-stretch border-r border-input px-3 text-sm text-muted-foreground select-none",
|
|
26
|
+
"aria-hidden": "true",
|
|
27
|
+
children: prefix
|
|
28
|
+
}
|
|
29
|
+
) }) : null,
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"input",
|
|
32
|
+
{
|
|
33
|
+
ref,
|
|
34
|
+
type: "url",
|
|
35
|
+
inputMode: "url",
|
|
36
|
+
autoComplete: "url",
|
|
37
|
+
placeholder: resolvedPlaceholder,
|
|
38
|
+
disabled,
|
|
39
|
+
className: cn(
|
|
40
|
+
inputElementVariants({ size }),
|
|
41
|
+
"px-3",
|
|
42
|
+
inputClassName
|
|
43
|
+
),
|
|
44
|
+
...props
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
WebsiteInput.displayName = "WebsiteInput";
|
|
53
|
+
export {
|
|
54
|
+
WebsiteInput
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=website-input.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/primitives/website-input.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport type { VariantProps } from 'class-variance-authority'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { inputWrapperVariants, inputElementVariants } from './input'\n\nexport type WebsiteInputProps = Omit<React.ComponentPropsWithoutRef<'input'>, 'size' | 'type'> &\n VariantProps<typeof inputWrapperVariants> & {\n /** Protocol shown in the left prefix box. Defaults to `'https://'` per Figma. */\n prefix?: string\n /** Hide the prefix box entirely (consumer wants a bare URL input). */\n showPrefix?: boolean\n /** Optional className on the wrapper. */\n className?: string\n /** Optional className on the inner `<input>`. */\n inputClassName?: string\n }\n\n/**\n * Website input matching Figma `Text Input [1.1]` (node `266:5251`) **Website** variant \u2014 a left\n * prefix box with the protocol text (default `'https://'`), a vertical divider, then the URL text\n * input. Built on the shared `inputWrapperVariants` / `inputElementVariants` CVA so the visual\n * contract matches the foundation `Input` primitive.\n *\n * The prefix is **display-only** \u2014 the inner `<input>` value contains ONLY the host/path portion\n * (e.g. `'www.example.com/path'`). Compose the full URL at the consumer boundary if needed.\n */\nexport const WebsiteInput = React.forwardRef<HTMLInputElement, WebsiteInputProps>(\n ({ className, inputClassName, size, prefix = 'https://', showPrefix = true, placeholder, disabled, ...props }, ref) => {\n const t = useT()\n const resolvedPlaceholder = placeholder ?? t('ui.inputs.websiteInput.placeholder', 'www.example.com')\n return (\n <div\n className={cn(\n inputWrapperVariants({ size }),\n // Override default px so prefix box can sit flush left.\n 'px-0 overflow-hidden',\n className,\n )}\n data-slot=\"website-input-wrapper\"\n >\n {showPrefix ? (\n <>\n <span\n className=\"flex shrink-0 items-center self-stretch border-r border-input px-3 text-sm text-muted-foreground select-none\"\n aria-hidden=\"true\"\n >\n {prefix}\n </span>\n </>\n ) : null}\n <input\n ref={ref}\n type=\"url\"\n inputMode=\"url\"\n autoComplete=\"url\"\n placeholder={resolvedPlaceholder}\n disabled={disabled}\n className={cn(\n inputElementVariants({ size }),\n 'px-3',\n inputClassName,\n )}\n {...props}\n />\n </div>\n )\n },\n)\nWebsiteInput.displayName = 'WebsiteInput'\n"],
|
|
5
|
+
"mappings": ";AAkCM,SAUI,UACE,KAXN;AAhCN,YAAY,WAAW;AAEvB,SAAS,YAAY;AACrB,SAAS,UAAU;AACnB,SAAS,sBAAsB,4BAA4B;AAuBpD,MAAM,eAAe,MAAM;AAAA,EAChC,CAAC,EAAE,WAAW,gBAAgB,MAAM,SAAS,YAAY,aAAa,MAAM,aAAa,UAAU,GAAG,MAAM,GAAG,QAAQ;AACrH,UAAM,IAAI,KAAK;AACf,UAAM,sBAAsB,eAAe,EAAE,sCAAsC,iBAAiB;AACpG,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT,qBAAqB,EAAE,KAAK,CAAC;AAAA;AAAA,UAE7B;AAAA,UACA;AAAA,QACF;AAAA,QACA,aAAU;AAAA,QAET;AAAA,uBACC,gCACE;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,eAAY;AAAA,cAEX;AAAA;AAAA,UACH,GACF,IACE;AAAA,UACJ;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAa;AAAA,cACb,aAAa;AAAA,cACb;AAAA,cACA,WAAW;AAAA,gBACT,qBAAqB,EAAE,KAAK,CAAC;AAAA,gBAC7B;AAAA,gBACA;AAAA,cACF;AAAA,cACC,GAAG;AAAA;AAAA,UACN;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;AACA,aAAa,cAAc;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ui",
|
|
3
|
-
"version": "0.6.1-develop.
|
|
3
|
+
"version": "0.6.1-develop.3287.1.450f4ffb56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -141,12 +141,12 @@
|
|
|
141
141
|
"remark-gfm": "^4.0.1"
|
|
142
142
|
},
|
|
143
143
|
"peerDependencies": {
|
|
144
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
144
|
+
"@open-mercato/shared": "0.6.1-develop.3287.1.450f4ffb56",
|
|
145
145
|
"react": ">=18.0.0",
|
|
146
146
|
"react-dom": ">=18.0.0"
|
|
147
147
|
},
|
|
148
148
|
"devDependencies": {
|
|
149
|
-
"@open-mercato/shared": "0.6.1-develop.
|
|
149
|
+
"@open-mercato/shared": "0.6.1-develop.3287.1.450f4ffb56",
|
|
150
150
|
"@testing-library/dom": "^10.4.1",
|
|
151
151
|
"@testing-library/jest-dom": "^6.9.1",
|
|
152
152
|
"@testing-library/react": "^16.3.1",
|
package/src/backend/AppShell.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { ChevronDown, ChevronLeft, Search, X } from 'lucide-react'
|
|
|
7
7
|
import { Button } from '../primitives/button'
|
|
8
8
|
import { IconButton } from '../primitives/icon-button'
|
|
9
9
|
import { Input } from '../primitives/input'
|
|
10
|
+
import { SearchInput } from '../primitives/search-input'
|
|
10
11
|
import { Checkbox } from '../primitives/checkbox'
|
|
11
12
|
import { Separator } from '../primitives/separator'
|
|
12
13
|
import { FlashMessages } from './FlashMessages'
|
|
@@ -674,24 +675,12 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
|
|
|
674
675
|
</div>
|
|
675
676
|
)}
|
|
676
677
|
{!compact && !hideSearch && (
|
|
677
|
-
<
|
|
678
|
-
type="text"
|
|
678
|
+
<SearchInput
|
|
679
679
|
value={navQuery}
|
|
680
|
-
onChange={
|
|
680
|
+
onChange={setNavQuery}
|
|
681
681
|
placeholder={t('appShell.searchNavPlaceholder', 'Search...')}
|
|
682
682
|
aria-label={t('appShell.searchNavAria', 'Search navigation')}
|
|
683
|
-
|
|
684
|
-
rightIcon={navQueryActive ? (
|
|
685
|
-
<IconButton
|
|
686
|
-
type="button"
|
|
687
|
-
variant="ghost"
|
|
688
|
-
size="xs"
|
|
689
|
-
onClick={() => setNavQuery('')}
|
|
690
|
-
aria-label={t('appShell.searchNavClear', 'Clear search')}
|
|
691
|
-
>
|
|
692
|
-
<X className="size-3.5" aria-hidden />
|
|
693
|
-
</IconButton>
|
|
694
|
-
) : undefined}
|
|
683
|
+
clearLabel={t('appShell.searchNavClear', 'Clear search')}
|
|
695
684
|
className="mb-2"
|
|
696
685
|
/>
|
|
697
686
|
)}
|
|
@@ -891,24 +880,12 @@ function AppShellBody({ productName, logo, email, groups, rightHeaderSlot, child
|
|
|
891
880
|
/>
|
|
892
881
|
) : null}
|
|
893
882
|
{!compact && (
|
|
894
|
-
<
|
|
895
|
-
type="text"
|
|
883
|
+
<SearchInput
|
|
896
884
|
value={navQuery}
|
|
897
|
-
onChange={
|
|
885
|
+
onChange={setNavQuery}
|
|
898
886
|
placeholder={t('appShell.searchNavPlaceholder', 'Search...')}
|
|
899
887
|
aria-label={t('appShell.searchNavAria', 'Search navigation')}
|
|
900
|
-
|
|
901
|
-
rightIcon={navQueryActive ? (
|
|
902
|
-
<IconButton
|
|
903
|
-
type="button"
|
|
904
|
-
variant="ghost"
|
|
905
|
-
size="xs"
|
|
906
|
-
onClick={() => setNavQuery('')}
|
|
907
|
-
aria-label={t('appShell.searchNavClear', 'Clear search')}
|
|
908
|
-
>
|
|
909
|
-
<X className="size-3.5" aria-hidden />
|
|
910
|
-
</IconButton>
|
|
911
|
-
) : undefined}
|
|
888
|
+
clearLabel={t('appShell.searchNavClear', 'Clear search')}
|
|
912
889
|
className="mb-2"
|
|
913
890
|
/>
|
|
914
891
|
)}
|
package/src/backend/CrudForm.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { CSS } from '@dnd-kit/utilities'
|
|
|
19
19
|
import { DataLoader } from '../primitives/DataLoader'
|
|
20
20
|
import { Checkbox } from '../primitives/checkbox'
|
|
21
21
|
import { Input } from '../primitives/input'
|
|
22
|
+
import { PasswordInput } from '../primitives/password-input'
|
|
22
23
|
import { Textarea } from '../primitives/textarea'
|
|
23
24
|
import {
|
|
24
25
|
Select,
|
|
@@ -3401,6 +3402,22 @@ function TextInput({
|
|
|
3401
3402
|
commitIfChanged()
|
|
3402
3403
|
}, [commitIfChanged])
|
|
3403
3404
|
|
|
3405
|
+
if (inputType === 'password') {
|
|
3406
|
+
return (
|
|
3407
|
+
<PasswordInput
|
|
3408
|
+
placeholder={placeholder}
|
|
3409
|
+
value={local}
|
|
3410
|
+
onChange={handleChange}
|
|
3411
|
+
onKeyDown={handleKeyDown}
|
|
3412
|
+
onFocus={handleFocus}
|
|
3413
|
+
onBlur={handleBlur}
|
|
3414
|
+
spellCheck={false}
|
|
3415
|
+
autoFocus={autoFocus}
|
|
3416
|
+
data-crud-focus-target=""
|
|
3417
|
+
disabled={disabled}
|
|
3418
|
+
/>
|
|
3419
|
+
)
|
|
3420
|
+
}
|
|
3404
3421
|
return (
|
|
3405
3422
|
<>
|
|
3406
3423
|
<Input
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
-
import { ListFilter
|
|
3
|
+
import { ListFilter } from 'lucide-react'
|
|
4
4
|
import { Button } from '../primitives/button'
|
|
5
|
+
import { SearchInput } from '../primitives/search-input'
|
|
5
6
|
import { FilterDef, FilterOverlay, FilterValues } from './FilterOverlay'
|
|
6
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
8
|
|
|
@@ -82,15 +83,13 @@ export function FilterBar({
|
|
|
82
83
|
const containerClass = `flex flex-col ${layout === 'inline' ? 'gap-1 sm:gap-2' : 'gap-2'} w-full`
|
|
83
84
|
const searchBlock = onSearchChange ? (
|
|
84
85
|
<div className={`flex items-center gap-2 ${searchAlign === 'right' ? 'sm:ml-auto' : ''}`}>
|
|
85
|
-
<div className="
|
|
86
|
-
<
|
|
86
|
+
<div className="w-full sm:w-72 lg:w-80">
|
|
87
|
+
<SearchInput
|
|
87
88
|
value={searchDraft}
|
|
88
|
-
onChange={
|
|
89
|
+
onChange={setSearchDraft}
|
|
89
90
|
placeholder={resolvedSearchPlaceholder}
|
|
90
|
-
className="h-9 w-full rounded-md border border-input bg-background pl-8 pr-2 text-sm shadow-xs outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50"
|
|
91
91
|
suppressHydrationWarning
|
|
92
92
|
/>
|
|
93
|
-
<Search aria-hidden="true" className="absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
94
93
|
</div>
|
|
95
94
|
{searchTrailing ? (
|
|
96
95
|
<div className="flex items-center gap-1">{searchTrailing}</div>
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mapCrudServerErrorToFormErrors,
|
|
3
|
+
normalizeCrudServerError,
|
|
4
|
+
raiseCrudError,
|
|
5
|
+
readJsonSafe,
|
|
6
|
+
} from '../utils/serverErrors'
|
|
2
7
|
|
|
3
8
|
describe('serverErrors helpers', () => {
|
|
4
9
|
it('maps details array into field errors and message', () => {
|
|
@@ -59,6 +64,79 @@ describe('serverErrors helpers', () => {
|
|
|
59
64
|
}
|
|
60
65
|
})
|
|
61
66
|
|
|
67
|
+
it('raiseCrudError extracts message from structured { error: { code, message } } envelope', async () => {
|
|
68
|
+
expect.assertions(4)
|
|
69
|
+
const response = new Response(
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
ok: false,
|
|
72
|
+
error: {
|
|
73
|
+
code: 'invite_cooldown_active',
|
|
74
|
+
message: 'A recent invite for this email is still pending — please wait before re-inviting.',
|
|
75
|
+
details: { retryAfterSeconds: 60 },
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
{ status: 429, headers: { 'content-type': 'application/json' } },
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await raiseCrudError(response, 'Fallback message')
|
|
83
|
+
} catch (err) {
|
|
84
|
+
expect(err).toMatchObject({
|
|
85
|
+
status: 429,
|
|
86
|
+
message:
|
|
87
|
+
'A recent invite for this email is still pending — please wait before re-inviting.',
|
|
88
|
+
})
|
|
89
|
+
const errorField = (err as { error?: unknown }).error as
|
|
90
|
+
| { code?: unknown; details?: unknown }
|
|
91
|
+
| undefined
|
|
92
|
+
expect(errorField?.code).toBe('invite_cooldown_active')
|
|
93
|
+
expect(errorField?.details).toEqual({ retryAfterSeconds: 60 })
|
|
94
|
+
expect((err as { ok?: unknown }).ok).toBe(false)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('normalizeCrudServerError reads message from nested { error: { message } } envelope', () => {
|
|
99
|
+
const normalized = normalizeCrudServerError({
|
|
100
|
+
ok: false,
|
|
101
|
+
error: {
|
|
102
|
+
code: 'invite_cooldown_active',
|
|
103
|
+
message: 'A recent invite for this email is still pending — please wait before re-inviting.',
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
expect(normalized.message).toBe(
|
|
107
|
+
'A recent invite for this email is still pending — please wait before re-inviting.',
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('raiseCrudError prefers top-level string error over nested error.message', async () => {
|
|
112
|
+
expect.assertions(1)
|
|
113
|
+
const response = new Response(
|
|
114
|
+
JSON.stringify({
|
|
115
|
+
error: 'Top-level message',
|
|
116
|
+
message: 'Top-level message',
|
|
117
|
+
}),
|
|
118
|
+
{ status: 400, headers: { 'content-type': 'application/json' } },
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
await expect(raiseCrudError(response, 'Fallback message')).rejects.toMatchObject({
|
|
122
|
+
status: 400,
|
|
123
|
+
message: 'Top-level message',
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('raiseCrudError falls back when structured envelope has empty nested message', async () => {
|
|
128
|
+
expect.assertions(1)
|
|
129
|
+
const response = new Response(
|
|
130
|
+
JSON.stringify({ ok: false, error: { code: 'oops', message: ' ' } }),
|
|
131
|
+
{ status: 500, headers: { 'content-type': 'application/json' } },
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
await expect(raiseCrudError(response, 'Fallback message')).rejects.toMatchObject({
|
|
135
|
+
status: 500,
|
|
136
|
+
message: 'Fallback message',
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
62
140
|
it('raiseCrudError falls back to message when body is plain text', async () => {
|
|
63
141
|
expect.assertions(1)
|
|
64
142
|
const response = new Response('Something went wrong', { status: 500 })
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
import * as React from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import { GripVertical, X, ChevronRight } from 'lucide-react'
|
|
4
4
|
import { Button } from '../../primitives/button'
|
|
5
5
|
import { IconButton } from '../../primitives/icon-button'
|
|
6
6
|
import { Switch } from '../../primitives/switch'
|
|
7
7
|
import { Input } from '../../primitives/input'
|
|
8
|
+
import { SearchInput } from '../../primitives/search-input'
|
|
8
9
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
10
|
import {
|
|
10
11
|
DndContext,
|
|
@@ -168,13 +169,11 @@ export function ColumnChooserSection({
|
|
|
168
169
|
return (
|
|
169
170
|
<div className="flex flex-col">
|
|
170
171
|
<div className="px-4 py-3 border-t">
|
|
171
|
-
<
|
|
172
|
-
type="text"
|
|
172
|
+
<SearchInput
|
|
173
173
|
size="sm"
|
|
174
|
-
leftIcon={<Search />}
|
|
175
174
|
placeholder={t('ui.columnChooser.search', 'Search columns...')}
|
|
176
175
|
value={searchQuery}
|
|
177
|
-
onChange={
|
|
176
|
+
onChange={setSearchQuery}
|
|
178
177
|
/>
|
|
179
178
|
</div>
|
|
180
179
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
5
|
import { Button } from '../../primitives/button'
|
|
5
6
|
|
|
6
7
|
export type ComboboxOption = {
|
|
@@ -62,6 +63,9 @@ export function ComboboxInput({
|
|
|
62
63
|
disabled = false,
|
|
63
64
|
allowCustomValues = true,
|
|
64
65
|
}: ComboboxInputProps) {
|
|
66
|
+
const t = useT()
|
|
67
|
+
const resolvedPlaceholder = placeholder ?? t('ui.inputs.comboboxInput.placeholder', 'Type to search...')
|
|
68
|
+
const loadingLabel = t('ui.inputs.comboboxInput.loading', 'Loading suggestions…')
|
|
65
69
|
const [input, setInput] = React.useState('')
|
|
66
70
|
const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])
|
|
67
71
|
const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])
|
|
@@ -288,9 +292,9 @@ export function ComboboxInput({
|
|
|
288
292
|
<input
|
|
289
293
|
ref={inputRef}
|
|
290
294
|
type="text"
|
|
291
|
-
className="w-full h-9 rounded border px-
|
|
295
|
+
className="w-full h-9 rounded-md border border-input bg-background px-3 text-sm shadow-xs transition-colors outline-none placeholder:text-muted-foreground focus-visible:shadow-focus focus-visible:border-foreground disabled:bg-bg-disabled disabled:border-border-disabled disabled:text-muted-foreground disabled:cursor-not-allowed"
|
|
292
296
|
value={input}
|
|
293
|
-
placeholder={
|
|
297
|
+
placeholder={resolvedPlaceholder}
|
|
294
298
|
autoFocus={autoFocus}
|
|
295
299
|
data-crud-focus-target=""
|
|
296
300
|
disabled={disabled}
|
|
@@ -319,32 +323,34 @@ export function ComboboxInput({
|
|
|
319
323
|
/>
|
|
320
324
|
|
|
321
325
|
{showSuggestions && !disabled && (loading || filteredSuggestions.length > 0) && (
|
|
322
|
-
<div className="absolute z-popover w-full mt-1 rounded border bg-popover shadow-
|
|
326
|
+
<div className="absolute z-popover w-full mt-1 rounded-md border border-input bg-popover p-2 shadow-md max-h-48 sm:max-h-60 overflow-auto">
|
|
323
327
|
{loading && touched ? (
|
|
324
|
-
<div className="px-
|
|
328
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">{loadingLabel}</div>
|
|
325
329
|
) : (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
330
|
+
<div className="flex flex-col gap-1">
|
|
331
|
+
{filteredSuggestions.map((option, index) => (
|
|
332
|
+
<Button
|
|
333
|
+
key={option.value}
|
|
334
|
+
type="button"
|
|
335
|
+
variant="ghost"
|
|
336
|
+
size="sm"
|
|
337
|
+
className={[
|
|
338
|
+
'w-full h-auto justify-start font-normal text-left flex flex-col items-start rounded-lg p-2',
|
|
339
|
+
index === selectedIndex ? 'bg-muted' : '',
|
|
340
|
+
]
|
|
341
|
+
.filter(Boolean)
|
|
342
|
+
.join(' ')}
|
|
343
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
344
|
+
onClick={() => selectValue(option.value)}
|
|
345
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
346
|
+
>
|
|
347
|
+
<span className="font-medium text-foreground">{option.label}</span>
|
|
348
|
+
{option.description ? (
|
|
349
|
+
<span className="text-xs text-muted-foreground">{option.description}</span>
|
|
350
|
+
) : null}
|
|
351
|
+
</Button>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
348
354
|
)}
|
|
349
355
|
</div>
|
|
350
356
|
)}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
5
|
import { ComboboxInput, type ComboboxOption } from './ComboboxInput'
|
|
5
6
|
import { useAvailableEvents } from './EventSelect'
|
|
6
7
|
|
|
7
8
|
export interface EventPatternInputProps {
|
|
8
9
|
value: string
|
|
9
10
|
onChange: (pattern: string) => void
|
|
11
|
+
/**
|
|
12
|
+
* Placeholder hint shown when the field is empty. Defaults to a translated
|
|
13
|
+
* `ui.inputs.eventPatternInput.placeholder` key (English fallback
|
|
14
|
+
* `'sales.orders.created'`) — an example of a valid event id with wildcards.
|
|
15
|
+
* Pass a custom string to override per-surface.
|
|
16
|
+
*/
|
|
10
17
|
placeholder?: string
|
|
11
18
|
disabled?: boolean
|
|
12
19
|
categories?: Array<'crud' | 'lifecycle' | 'system' | 'custom'>
|
|
@@ -16,11 +23,13 @@ export interface EventPatternInputProps {
|
|
|
16
23
|
export function EventPatternInput({
|
|
17
24
|
value,
|
|
18
25
|
onChange,
|
|
19
|
-
placeholder
|
|
26
|
+
placeholder,
|
|
20
27
|
disabled,
|
|
21
28
|
categories,
|
|
22
29
|
modules,
|
|
23
30
|
}: EventPatternInputProps) {
|
|
31
|
+
const t = useT()
|
|
32
|
+
const resolvedPlaceholder = placeholder ?? t('ui.inputs.eventPatternInput.placeholder', 'sales.orders.created')
|
|
24
33
|
const { events } = useAvailableEvents({ categories, modules })
|
|
25
34
|
|
|
26
35
|
const suggestions = React.useMemo<ComboboxOption[]>(
|
|
@@ -37,7 +46,7 @@ export function EventPatternInput({
|
|
|
37
46
|
<ComboboxInput
|
|
38
47
|
value={value}
|
|
39
48
|
onChange={onChange}
|
|
40
|
-
placeholder={
|
|
49
|
+
placeholder={resolvedPlaceholder}
|
|
41
50
|
suggestions={suggestions}
|
|
42
51
|
allowCustomValues={true}
|
|
43
52
|
disabled={disabled}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { useMemo } from 'react'
|
|
5
5
|
import { useQuery } from '@tanstack/react-query'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
7
|
import { apiCall } from '../utils/apiCall'
|
|
7
8
|
import {
|
|
8
9
|
Select,
|
|
@@ -32,7 +33,7 @@ export interface EventSelectProps {
|
|
|
32
33
|
value: string
|
|
33
34
|
/** Called when event is selected */
|
|
34
35
|
onChange: (eventId: string) => void
|
|
35
|
-
/** Placeholder text when no event selected */
|
|
36
|
+
/** Placeholder text when no event selected. Defaults to a translated string. */
|
|
36
37
|
placeholder?: string
|
|
37
38
|
/** Additional CSS classes */
|
|
38
39
|
className?: string
|
|
@@ -44,6 +45,8 @@ export interface EventSelectProps {
|
|
|
44
45
|
modules?: string[]
|
|
45
46
|
/** Whether to exclude events marked as excludeFromTriggers (default: true) */
|
|
46
47
|
excludeTriggerExcluded?: boolean
|
|
48
|
+
/** Trigger size — defaults to `'default'` (DS row-height contract). */
|
|
49
|
+
size?: 'sm' | 'default' | 'lg'
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -54,13 +57,19 @@ export interface EventSelectProps {
|
|
|
54
57
|
export function EventSelect({
|
|
55
58
|
value,
|
|
56
59
|
onChange,
|
|
57
|
-
placeholder
|
|
60
|
+
placeholder,
|
|
58
61
|
className,
|
|
59
62
|
disabled,
|
|
60
63
|
categories,
|
|
61
64
|
modules,
|
|
62
65
|
excludeTriggerExcluded = true,
|
|
66
|
+
size = 'default',
|
|
63
67
|
}: EventSelectProps) {
|
|
68
|
+
const t = useT()
|
|
69
|
+
const resolvedPlaceholder = placeholder ?? t('ui.inputs.eventSelect.placeholder', 'Select an event...')
|
|
70
|
+
const loadingPlaceholder = t('ui.inputs.eventSelect.loading', 'Loading...')
|
|
71
|
+
const emptyPlaceholder = t('ui.inputs.eventSelect.empty', 'No events available')
|
|
72
|
+
|
|
64
73
|
// Fetch events from the API
|
|
65
74
|
const { data: allEvents = [], isLoading } = useQuery({
|
|
66
75
|
queryKey: ['declared-events', excludeTriggerExcluded],
|
|
@@ -115,9 +124,9 @@ export function EventSelect({
|
|
|
115
124
|
onValueChange={(next) => onChange(next ?? '')}
|
|
116
125
|
disabled={disabled || isLoading}
|
|
117
126
|
>
|
|
118
|
-
<SelectTrigger size=
|
|
127
|
+
<SelectTrigger size={size} className={className}>
|
|
119
128
|
<SelectValue
|
|
120
|
-
placeholder={isLoading ?
|
|
129
|
+
placeholder={isLoading ? loadingPlaceholder : isEmpty ? emptyPlaceholder : resolvedPlaceholder}
|
|
121
130
|
/>
|
|
122
131
|
</SelectTrigger>
|
|
123
132
|
<SelectContent>
|