@open-mercato/core 0.10.1-develop.7866.1.71cf6e2699 → 0.10.1-develop.7870.1.63e4c252d3
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/queue.js +2 -2
- package/dist/helpers/integration/queue.js.map +2 -2
- package/dist/modules/forms/backend/forms/[id]/studio/logic/ConditionBuilder.js +34 -15
- package/dist/modules/forms/backend/forms/[id]/studio/logic/ConditionBuilder.js.map +2 -2
- package/dist/modules/forms/backend/forms/[id]/studio/logic/condition-model.js +6 -0
- package/dist/modules/forms/backend/forms/[id]/studio/logic/condition-model.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/queue.ts +3 -3
- package/src/modules/forms/backend/forms/[id]/studio/logic/ConditionBuilder.tsx +33 -14
- package/src/modules/forms/backend/forms/[id]/studio/logic/condition-model.ts +6 -0
|
@@ -86,11 +86,11 @@ async function drainIntegrationQueue(queueName, options = {}) {
|
|
|
86
86
|
appRoot: resolveAppRoot(options.appRoot),
|
|
87
87
|
jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT
|
|
88
88
|
};
|
|
89
|
+
const managed = parseBooleanToken(process.env.AUTO_SPAWN_WORKERS) ?? parseBooleanToken(process.env.OM_AUTO_SPAWN_WORKERS) ?? true;
|
|
90
|
+
if (managed) return waitForManagedQueue(queueName, resolved);
|
|
89
91
|
if (process.env.OM_TEST_APP_ROOT?.trim()) {
|
|
90
92
|
return drainQueueInAppProcess(queueName, resolved);
|
|
91
93
|
}
|
|
92
|
-
const managed = parseBooleanToken(process.env.AUTO_SPAWN_WORKERS) ?? parseBooleanToken(process.env.OM_AUTO_SPAWN_WORKERS) ?? true;
|
|
93
|
-
if (managed) return waitForManagedQueue(queueName, resolved);
|
|
94
94
|
return drainQueueInCurrentProcess(queueName, resolved);
|
|
95
95
|
}
|
|
96
96
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/helpers/integration/queue.ts"],
|
|
4
|
-
"sourcesContent": ["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { expect } from '@playwright/test'\nimport { createQueue, getQueuePendingProbe } from '@open-mercato/queue'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype DrainQueueOptions = {\n appRoot?: string\n jobLimit?: number\n}\n\nconst DEFAULT_JOB_LIMIT = 100\n\nfunction resolveAppRoot(input?: string): string {\n return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), 'apps/mercato'))\n}\n\nasync function drainQueueInCurrentProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n const { drainQueueFromAppRoot } = await import('./queue-runner')\n return drainQueueFromAppRoot(queueName, options)\n}\n\nfunction resolveAppRunnerPath(appRoot: string): string {\n const requireFromApp = createRequire(path.join(appRoot, 'package.json'))\n return requireFromApp.resolve('@open-mercato/core/helpers/integration/queue-runner')\n}\n\nfunction drainQueueInAppProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {\n cwd: options.appRoot,\n env: {\n ...process.env,\n OM_INTEGRATION_APP_ROOT: options.appRoot,\n OM_INTEGRATION_QUEUE_NAME: queueName,\n OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),\n QUEUE_BASE_DIR: process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue'),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n let stdout = ''\n let stderr = ''\n child.stdout.on('data', (chunk) => { stdout += chunk.toString() })\n child.stderr.on('data', (chunk) => { stderr += chunk.toString() })\n child.on('error', reject)\n child.on('close', (code) => {\n // The child's diagnostics (pino job logs on stdout, cache-fallback\n // console.warn on stderr) are otherwise discarded on exit 0, which makes\n // silently-failing jobs and silent memory-cache fallbacks in the drain\n // child undebuggable from CI logs. OM_DRAIN_DEBUG=1 forwards everything;\n // a non-empty stderr is forwarded unconditionally because it only carries\n // warning/error-class output.\n if (process.env.OM_DRAIN_DEBUG === '1') {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDOUT:\\n${stdout}\\nSTDERR:\\n${stderr}\\n`)\n } else if (stderr.trim().length > 0) {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDERR:\\n${stderr}\\n`)\n }\n if (code !== 0) {\n reject(new Error(`Queue drain failed for \"${queueName}\" in ${options.appRoot} (exit ${code}).\\n${stderr || stdout}`))\n return\n }\n const lastLine = stdout.trim().split(/\\r?\\n/).reverse().find((line) => line.trim().startsWith('{')) ?? '{}'\n try {\n const parsed = JSON.parse(lastLine) as { processed?: unknown }\n resolve(Number(parsed.processed ?? 0))\n } catch {\n resolve(0)\n }\n })\n })\n}\n\nasync function waitForManagedQueue(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n const baseDir = process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue')\n const queue = createQueue(queueName, 'local', { baseDir })\n try {\n const initial = await queue.getJobCounts()\n await expect.poll(async () => {\n const pending = await getQueuePendingProbe(queueName, 'local', { baseDir })\n if (pending.error) throw new Error(`[internal] Cannot observe integration queue ${queueName}: ${pending.errorMessage}`)\n return pending.ready + pending.active\n }, { message: `Integration queue ${queueName} should finish processing` }).toBe(0)\n const final = await queue.getJobCounts()\n return final.completed + final.failed - initial.completed - initial.failed\n } finally {\n await queue.close()\n }\n}\n\nexport async function drainIntegrationQueue(queueName: string, options: DrainQueueOptions = {}): Promise<number> {\n const resolved = {\n appRoot: resolveAppRoot(options.appRoot),\n jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,\n }\n
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { expect } from '@playwright/test'\nimport { createQueue, getQueuePendingProbe } from '@open-mercato/queue'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype DrainQueueOptions = {\n appRoot?: string\n jobLimit?: number\n}\n\nconst DEFAULT_JOB_LIMIT = 100\n\nfunction resolveAppRoot(input?: string): string {\n return path.resolve(input?.trim() || process.env.OM_TEST_APP_ROOT?.trim() || path.resolve(process.cwd(), 'apps/mercato'))\n}\n\nasync function drainQueueInCurrentProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n const { drainQueueFromAppRoot } = await import('./queue-runner')\n return drainQueueFromAppRoot(queueName, options)\n}\n\nfunction resolveAppRunnerPath(appRoot: string): string {\n const requireFromApp = createRequire(path.join(appRoot, 'package.json'))\n return requireFromApp.resolve('@open-mercato/core/helpers/integration/queue-runner')\n}\n\nfunction drainQueueInAppProcess(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [resolveAppRunnerPath(options.appRoot)], {\n cwd: options.appRoot,\n env: {\n ...process.env,\n OM_INTEGRATION_APP_ROOT: options.appRoot,\n OM_INTEGRATION_QUEUE_NAME: queueName,\n OM_INTEGRATION_QUEUE_JOB_LIMIT: String(options.jobLimit),\n QUEUE_BASE_DIR: process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue'),\n },\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n let stdout = ''\n let stderr = ''\n child.stdout.on('data', (chunk) => { stdout += chunk.toString() })\n child.stderr.on('data', (chunk) => { stderr += chunk.toString() })\n child.on('error', reject)\n child.on('close', (code) => {\n // The child's diagnostics (pino job logs on stdout, cache-fallback\n // console.warn on stderr) are otherwise discarded on exit 0, which makes\n // silently-failing jobs and silent memory-cache fallbacks in the drain\n // child undebuggable from CI logs. OM_DRAIN_DEBUG=1 forwards everything;\n // a non-empty stderr is forwarded unconditionally because it only carries\n // warning/error-class output.\n if (process.env.OM_DRAIN_DEBUG === '1') {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDOUT:\\n${stdout}\\nSTDERR:\\n${stderr}\\n`)\n } else if (stderr.trim().length > 0) {\n process.stderr.write(`[drain-child ${queueName} exit=${code}] STDERR:\\n${stderr}\\n`)\n }\n if (code !== 0) {\n reject(new Error(`Queue drain failed for \"${queueName}\" in ${options.appRoot} (exit ${code}).\\n${stderr || stdout}`))\n return\n }\n const lastLine = stdout.trim().split(/\\r?\\n/).reverse().find((line) => line.trim().startsWith('{')) ?? '{}'\n try {\n const parsed = JSON.parse(lastLine) as { processed?: unknown }\n resolve(Number(parsed.processed ?? 0))\n } catch {\n resolve(0)\n }\n })\n })\n}\n\nasync function waitForManagedQueue(queueName: string, options: Required<DrainQueueOptions>): Promise<number> {\n const baseDir = process.env.QUEUE_BASE_DIR?.trim() || path.resolve(options.appRoot, '.mercato/queue')\n const queue = createQueue(queueName, 'local', { baseDir })\n try {\n const initial = await queue.getJobCounts()\n await expect.poll(async () => {\n const pending = await getQueuePendingProbe(queueName, 'local', { baseDir })\n if (pending.error) throw new Error(`[internal] Cannot observe integration queue ${queueName}: ${pending.errorMessage}`)\n return pending.ready + pending.active\n }, { message: `Integration queue ${queueName} should finish processing` }).toBe(0)\n const final = await queue.getJobCounts()\n return final.completed + final.failed - initial.completed - initial.failed\n } finally {\n await queue.close()\n }\n}\n\nexport async function drainIntegrationQueue(queueName: string, options: DrainQueueOptions = {}): Promise<number> {\n const resolved = {\n appRoot: resolveAppRoot(options.appRoot),\n jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,\n }\n const managed = parseBooleanToken(process.env.AUTO_SPAWN_WORKERS)\n ?? parseBooleanToken(process.env.OM_AUTO_SPAWN_WORKERS)\n ?? true\n if (managed) return waitForManagedQueue(queueName, resolved)\n if (process.env.OM_TEST_APP_ROOT?.trim()) {\n return drainQueueInAppProcess(queueName, resolved)\n }\n return drainQueueInCurrentProcess(queueName, resolved)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -75,7 +75,7 @@ function ConditionBuilder({ predicate, sources, onChange }) {
|
|
|
75
75
|
)
|
|
76
76
|
] });
|
|
77
77
|
}
|
|
78
|
-
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", "data-testid": "condition-builder", children: [
|
|
78
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-3", "data-testid": "condition-builder", children: [
|
|
79
79
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
80
80
|
/* @__PURE__ */ jsx("h4", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: t("forms.studio.logic.visibility.heading") }),
|
|
81
81
|
/* @__PURE__ */ jsx(
|
|
@@ -84,7 +84,11 @@ function ConditionBuilder({ predicate, sources, onChange }) {
|
|
|
84
84
|
variant: "outline",
|
|
85
85
|
size: "sm",
|
|
86
86
|
type: "button",
|
|
87
|
-
onClick: () =>
|
|
87
|
+
onClick: () => {
|
|
88
|
+
const source = sources[0];
|
|
89
|
+
const row = { ...newRow(source?.value ?? ""), operator: operatorOptions(source)[0] ?? "eq" };
|
|
90
|
+
commit({ ...model, rows: [...model.rows, row] });
|
|
91
|
+
},
|
|
88
92
|
children: t("forms.studio.logic.visibility.addRule")
|
|
89
93
|
}
|
|
90
94
|
)
|
|
@@ -92,8 +96,9 @@ function ConditionBuilder({ predicate, sources, onChange }) {
|
|
|
92
96
|
model.rows.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("forms.studio.logic.visibility.always") }) : null,
|
|
93
97
|
/* @__PURE__ */ jsx("div", { className: "space-y-2", children: model.rows.map((row, index) => {
|
|
94
98
|
const sourceOption = sourceByValue.get(row.source);
|
|
95
|
-
const
|
|
96
|
-
const
|
|
99
|
+
const supportedOperators = operatorOptions(sourceOption);
|
|
100
|
+
const operators = supportedOperators.includes(row.operator) ? supportedOperators : [row.operator, ...supportedOperators];
|
|
101
|
+
const operator = row.operator;
|
|
97
102
|
const valueDisabled = operator === "is_empty" || operator === "is_not_empty" || operator === "is_true" || operator === "is_false";
|
|
98
103
|
const updateRow = (partial) => {
|
|
99
104
|
const nextRows = model.rows.map((entry) => entry.id === row.id ? { ...entry, ...partial } : entry);
|
|
@@ -102,38 +107,52 @@ function ConditionBuilder({ predicate, sources, onChange }) {
|
|
|
102
107
|
const removeRow = () => {
|
|
103
108
|
commit({ ...model, rows: model.rows.filter((entry) => entry.id !== row.id) });
|
|
104
109
|
};
|
|
105
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", "data-testid": "condition-row", children: [
|
|
110
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", "data-testid": "condition-row", children: [
|
|
106
111
|
index > 0 ? /* @__PURE__ */ jsx(Tag, { variant: "neutral", className: "shrink-0 uppercase", children: model.combine === "and" ? t("forms.studio.logic.visibility.combine.and") : t("forms.studio.logic.visibility.combine.or") }) : null,
|
|
107
|
-
/* @__PURE__ */ jsxs(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
/* @__PURE__ */ jsxs(
|
|
113
|
+
Select,
|
|
114
|
+
{
|
|
115
|
+
value: row.source,
|
|
116
|
+
onValueChange: (value) => {
|
|
117
|
+
const nextOperators = operatorOptions(sourceByValue.get(value));
|
|
118
|
+
updateRow({
|
|
119
|
+
source: value,
|
|
120
|
+
...nextOperators.includes(row.operator) ? {} : { operator: nextOperators[0] ?? "eq", value: "" }
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size: "sm", className: "min-w-0 max-w-full flex-1 basis-40", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("forms.studio.logic.visibility.sourcePlaceholder") }) }),
|
|
125
|
+
/* @__PURE__ */ jsx(SelectContent, { children: sources.map((source) => /* @__PURE__ */ jsx(SelectItem, { value: source.value, children: source.label }, source.value)) })
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
),
|
|
111
129
|
/* @__PURE__ */ jsxs(
|
|
112
130
|
Select,
|
|
113
131
|
{
|
|
114
132
|
value: operator,
|
|
115
133
|
onValueChange: (value) => updateRow({ operator: value }),
|
|
116
134
|
children: [
|
|
117
|
-
/* @__PURE__ */ jsx(SelectTrigger, { className: "
|
|
135
|
+
/* @__PURE__ */ jsx(SelectTrigger, { size: "sm", className: "min-w-0 max-w-full flex-1 basis-40", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
118
136
|
/* @__PURE__ */ jsx(SelectContent, { children: operators.map((op) => /* @__PURE__ */ jsx(SelectItem, { value: op, children: t(`forms.studio.logic.visibility.op.${op}`) }, op)) })
|
|
119
137
|
]
|
|
120
138
|
}
|
|
121
139
|
),
|
|
122
|
-
/* @__PURE__ */ jsx(
|
|
140
|
+
!valueDisabled ? /* @__PURE__ */ jsx(
|
|
123
141
|
Input,
|
|
124
142
|
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
size: "sm",
|
|
144
|
+
className: "min-w-0 max-w-full flex-1 basis-32",
|
|
145
|
+
value: String(row.value ?? ""),
|
|
128
146
|
onChange: (event) => updateRow({ value: event.target.value })
|
|
129
147
|
}
|
|
130
|
-
),
|
|
148
|
+
) : null,
|
|
131
149
|
/* @__PURE__ */ jsx(
|
|
132
150
|
IconButton,
|
|
133
151
|
{
|
|
134
152
|
"aria-label": t("forms.studio.logic.visibility.remove"),
|
|
135
153
|
variant: "ghost",
|
|
136
154
|
size: "sm",
|
|
155
|
+
className: "shrink-0",
|
|
137
156
|
type: "button",
|
|
138
157
|
onClick: removeRow,
|
|
139
158
|
children: /* @__PURE__ */ jsx(Trash2, { className: "size-4" })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/forms/backend/forms/%5Bid%5D/studio/logic/ConditionBuilder.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { RadioGroup } from '@open-mercato/ui/primitives/radio'\nimport { RadioField } from '@open-mercato/ui/primitives/radio-field'\nimport { Trash2 } from '../lucide-icons'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n type ConditionBuilderModel,\n type ConditionOperator,\n type ConditionRow,\n compilePredicate,\n emptyModel,\n newRow,\n parsePredicate,\n} from './condition-model'\nimport { resolveTypeLabel } from '../type-label'\nimport type { FormSchema } from '../schema-helpers'\n\nexport type ConditionSourceOption = {\n value: string\n label: string\n fieldType?: string | null\n namespace: 'field' | 'hidden' | 'variable'\n}\n\nexport type ConditionBuilderProps = {\n predicate: unknown | null\n sources: ConditionSourceOption[]\n onChange: (next: unknown | null) => void\n}\n\nconst OPERATORS_BY_TYPE: Record<string, ConditionOperator[]> = {\n text: ['eq', 'neq', 'contains', 'is_empty', 'is_not_empty'],\n textarea: ['eq', 'neq', 'contains', 'is_empty', 'is_not_empty'],\n date: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'is_empty', 'is_not_empty'],\n datetime: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'is_empty', 'is_not_empty'],\n number: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'],\n integer: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'],\n scale: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'],\n boolean: ['is_true', 'is_false'],\n yes_no: ['is_true', 'is_false'],\n yes_no_buttons: ['is_true', 'is_false'],\n select_one: ['eq', 'neq'],\n select_many: ['contains'],\n}\n\nconst FALLBACK_OPERATORS: ConditionOperator[] = ['eq', 'neq']\n\nfunction operatorOptions(source: ConditionSourceOption | undefined): ConditionOperator[] {\n if (!source) return FALLBACK_OPERATORS\n const fieldType = source.fieldType ?? undefined\n if (fieldType && OPERATORS_BY_TYPE[fieldType]) return OPERATORS_BY_TYPE[fieldType]\n if (source.namespace === 'variable') return ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'is_true', 'is_false']\n if (source.namespace === 'hidden') return ['eq', 'neq', 'contains', 'is_empty', 'is_not_empty']\n return FALLBACK_OPERATORS\n}\n\nexport function ConditionBuilder({ predicate, sources, onChange }: ConditionBuilderProps) {\n const t = useT()\n const [model, setModel] = React.useState<ConditionBuilderModel>(() => parsePredicate(predicate))\n\n React.useEffect(() => {\n setModel(parsePredicate(predicate))\n }, [predicate])\n\n const commit = React.useCallback(\n (nextModel: ConditionBuilderModel) => {\n setModel(nextModel)\n const compiled = compilePredicate(nextModel)\n onChange(compiled)\n },\n [onChange],\n )\n\n const sourceByValue = React.useMemo(() => {\n const map = new Map<string, ConditionSourceOption>()\n for (const entry of sources) map.set(entry.value, entry)\n return map\n }, [sources])\n\n if (model.raw !== null && model.rows.length === 0) {\n return (\n <div className=\"space-y-2\">\n <Tag variant=\"warning\">{t('forms.studio.logic.visibility.rawShape')}</Tag>\n <pre className=\"max-h-40 overflow-auto rounded-md border border-border bg-muted/30 p-2 text-xs\">\n {JSON.stringify(model.raw, null, 2)}\n </pre>\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={() => commit(emptyModel())}\n >\n {t('forms.studio.logic.visibility.clear')}\n </Button>\n </div>\n )\n }\n\n return (\n <div className=\"space-y-3\" data-testid=\"condition-builder\">\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('forms.studio.logic.visibility.heading')}\n </h4>\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={() => commit({ ...model, rows: [...model.rows, newRow(sources[0]?.value ?? '')] })}\n >\n {t('forms.studio.logic.visibility.addRule')}\n </Button>\n </div>\n {model.rows.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">{t('forms.studio.logic.visibility.always')}</p>\n ) : null}\n <div className=\"space-y-2\">\n {model.rows.map((row, index) => {\n const sourceOption = sourceByValue.get(row.source)\n const operators = operatorOptions(sourceOption)\n const operator = operators.includes(row.operator) ? row.operator : operators[0] ?? 'eq'\n const valueDisabled =\n operator === 'is_empty' || operator === 'is_not_empty' || operator === 'is_true' || operator === 'is_false'\n const updateRow = (partial: Partial<ConditionRow>) => {\n const nextRows = model.rows.map((entry) => (entry.id === row.id ? { ...entry, ...partial } : entry))\n commit({ ...model, rows: nextRows })\n }\n const removeRow = () => {\n commit({ ...model, rows: model.rows.filter((entry) => entry.id !== row.id) })\n }\n return (\n <div key={row.id} className=\"flex items-center gap-2\" data-testid=\"condition-row\">\n {index > 0 ? (\n <Tag variant=\"neutral\" className=\"shrink-0 uppercase\">\n {model.combine === 'and'\n ? t('forms.studio.logic.visibility.combine.and')\n : t('forms.studio.logic.visibility.combine.or')}\n </Tag>\n ) : null}\n <Select value={row.source} onValueChange={(value) => updateRow({ source: value })}>\n <SelectTrigger className=\"h-8 flex-1\">\n <SelectValue placeholder={t('forms.studio.logic.visibility.sourcePlaceholder')} />\n </SelectTrigger>\n <SelectContent>\n {sources.map((source) => (\n <SelectItem key={source.value} value={source.value}>\n {source.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select\n value={operator}\n onValueChange={(value) => updateRow({ operator: value as ConditionOperator })}\n >\n <SelectTrigger className=\"h-8 w-32\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {operators.map((op) => (\n <SelectItem key={op} value={op}>\n {t(`forms.studio.logic.visibility.op.${op}`)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Input\n className=\"h-8 w-32\"\n value={valueDisabled ? '' : String(row.value ?? '')}\n disabled={valueDisabled}\n onChange={(event) => updateRow({ value: event.target.value })}\n />\n <IconButton\n aria-label={t('forms.studio.logic.visibility.remove')}\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n onClick={removeRow}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n )\n })}\n </div>\n {model.rows.length > 1 ? (\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-muted-foreground\">{t('forms.studio.logic.visibility.combineLabel')}</span>\n <RadioGroup\n value={model.combine}\n onValueChange={(next) => commit({ ...model, combine: next as 'and' | 'or' })}\n className=\"flex-row gap-3\"\n >\n <RadioField value=\"and\" label={t('forms.studio.logic.visibility.combine.and')} />\n <RadioField value=\"or\" label={t('forms.studio.logic.visibility.combine.or')} />\n </RadioGroup>\n </div>\n ) : null}\n {model.rows.length > 0 ? (\n <details className=\"mt-2\">\n <summary className=\"cursor-pointer text-xs font-medium uppercase text-muted-foreground\">\n {t('forms.studio.logic.visibility.compiledJson')}\n </summary>\n <pre className=\"mt-1 max-h-40 overflow-auto rounded-md border border-border bg-muted/30 p-2 text-xs\">\n {JSON.stringify(compilePredicate(model), null, 2)}\n </pre>\n </details>\n ) : null}\n </div>\n )\n}\n\n/** Convenience builder of source options from a `FormSchema`. */\nexport function buildFieldSourceOptions(schema: FormSchema, currentLocale: string, t: ReturnType<typeof useT>): ConditionSourceOption[] {\n const result: ConditionSourceOption[] = []\n for (const [key, node] of Object.entries(schema.properties)) {\n const omType = String((node as Record<string, unknown>)['x-om-type'] ?? '')\n if (omType === 'info_block') continue\n const labelMap = (node as Record<string, unknown>)['x-om-label']\n const label =\n labelMap && typeof labelMap === 'object' && !Array.isArray(labelMap)\n ? String((labelMap as Record<string, unknown>)[currentLocale] ?? (labelMap as Record<string, unknown>).en ?? key)\n : key\n const typeLabel = resolveTypeLabel(omType, t)\n result.push({ value: key, label: `${label} \u2014 ${typeLabel}`, fieldType: omType, namespace: 'field' })\n }\n const hiddenFields = (schema as Record<string, unknown>)['x-om-hidden-fields']\n if (Array.isArray(hiddenFields)) {\n for (const entry of hiddenFields) {\n if (!entry || typeof entry !== 'object') continue\n const name = (entry as Record<string, unknown>).name\n if (typeof name !== 'string') continue\n result.push({\n value: `hidden.${name}`,\n label: `${name} \u2014 ${t('forms.studio.logic.source.hidden')}`,\n fieldType: null,\n namespace: 'hidden',\n })\n }\n }\n const variables = (schema as Record<string, unknown>)['x-om-variables']\n if (Array.isArray(variables)) {\n for (const entry of variables) {\n if (!entry || typeof entry !== 'object') continue\n const name = (entry as Record<string, unknown>).name\n if (typeof name !== 'string') continue\n const variableType = String((entry as Record<string, unknown>).type ?? '')\n result.push({\n value: `var.${name}`,\n label: `${name} \u2014 ${t('forms.studio.logic.source.variable')}`,\n fieldType: variableType,\n namespace: 'variable',\n })\n }\n }\n return result\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@open-mercato/ui/primitives/select'\nimport { Tag } from '@open-mercato/ui/primitives/tag'\nimport { RadioGroup } from '@open-mercato/ui/primitives/radio'\nimport { RadioField } from '@open-mercato/ui/primitives/radio-field'\nimport { Trash2 } from '../lucide-icons'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n type ConditionBuilderModel,\n type ConditionOperator,\n type ConditionRow,\n compilePredicate,\n emptyModel,\n newRow,\n parsePredicate,\n} from './condition-model'\nimport { resolveTypeLabel } from '../type-label'\nimport type { FormSchema } from '../schema-helpers'\n\nexport type ConditionSourceOption = {\n value: string\n label: string\n fieldType?: string | null\n namespace: 'field' | 'hidden' | 'variable'\n}\n\nexport type ConditionBuilderProps = {\n predicate: unknown | null\n sources: ConditionSourceOption[]\n onChange: (next: unknown | null) => void\n}\n\nconst OPERATORS_BY_TYPE: Record<string, ConditionOperator[]> = {\n text: ['eq', 'neq', 'contains', 'is_empty', 'is_not_empty'],\n textarea: ['eq', 'neq', 'contains', 'is_empty', 'is_not_empty'],\n date: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'is_empty', 'is_not_empty'],\n datetime: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'is_empty', 'is_not_empty'],\n number: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'],\n integer: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'],\n scale: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'],\n boolean: ['is_true', 'is_false'],\n yes_no: ['is_true', 'is_false'],\n yes_no_buttons: ['is_true', 'is_false'],\n select_one: ['eq', 'neq'],\n select_many: ['contains'],\n}\n\nconst FALLBACK_OPERATORS: ConditionOperator[] = ['eq', 'neq']\n\nfunction operatorOptions(source: ConditionSourceOption | undefined): ConditionOperator[] {\n if (!source) return FALLBACK_OPERATORS\n const fieldType = source.fieldType ?? undefined\n if (fieldType && OPERATORS_BY_TYPE[fieldType]) return OPERATORS_BY_TYPE[fieldType]\n if (source.namespace === 'variable') return ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'is_true', 'is_false']\n if (source.namespace === 'hidden') return ['eq', 'neq', 'contains', 'is_empty', 'is_not_empty']\n return FALLBACK_OPERATORS\n}\n\nexport function ConditionBuilder({ predicate, sources, onChange }: ConditionBuilderProps) {\n const t = useT()\n const [model, setModel] = React.useState<ConditionBuilderModel>(() => parsePredicate(predicate))\n\n React.useEffect(() => {\n setModel(parsePredicate(predicate))\n }, [predicate])\n\n const commit = React.useCallback(\n (nextModel: ConditionBuilderModel) => {\n setModel(nextModel)\n const compiled = compilePredicate(nextModel)\n onChange(compiled)\n },\n [onChange],\n )\n\n const sourceByValue = React.useMemo(() => {\n const map = new Map<string, ConditionSourceOption>()\n for (const entry of sources) map.set(entry.value, entry)\n return map\n }, [sources])\n\n if (model.raw !== null && model.rows.length === 0) {\n return (\n <div className=\"space-y-2\">\n <Tag variant=\"warning\">{t('forms.studio.logic.visibility.rawShape')}</Tag>\n <pre className=\"max-h-40 overflow-auto rounded-md border border-border bg-muted/30 p-2 text-xs\">\n {JSON.stringify(model.raw, null, 2)}\n </pre>\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={() => commit(emptyModel())}\n >\n {t('forms.studio.logic.visibility.clear')}\n </Button>\n </div>\n )\n }\n\n return (\n <div className=\"min-w-0 space-y-3\" data-testid=\"condition-builder\">\n <div className=\"flex items-center justify-between\">\n <h4 className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n {t('forms.studio.logic.visibility.heading')}\n </h4>\n <Button\n variant=\"outline\"\n size=\"sm\"\n type=\"button\"\n onClick={() => {\n const source = sources[0]\n const row = { ...newRow(source?.value ?? ''), operator: operatorOptions(source)[0] ?? 'eq' }\n commit({ ...model, rows: [...model.rows, row] })\n }}\n >\n {t('forms.studio.logic.visibility.addRule')}\n </Button>\n </div>\n {model.rows.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">{t('forms.studio.logic.visibility.always')}</p>\n ) : null}\n <div className=\"space-y-2\">\n {model.rows.map((row, index) => {\n const sourceOption = sourceByValue.get(row.source)\n const supportedOperators = operatorOptions(sourceOption)\n const operators = supportedOperators.includes(row.operator)\n ? supportedOperators\n : [row.operator, ...supportedOperators]\n const operator = row.operator\n const valueDisabled =\n operator === 'is_empty' || operator === 'is_not_empty' || operator === 'is_true' || operator === 'is_false'\n const updateRow = (partial: Partial<ConditionRow>) => {\n const nextRows = model.rows.map((entry) => (entry.id === row.id ? { ...entry, ...partial } : entry))\n commit({ ...model, rows: nextRows })\n }\n const removeRow = () => {\n commit({ ...model, rows: model.rows.filter((entry) => entry.id !== row.id) })\n }\n return (\n <div key={row.id} className=\"flex min-w-0 flex-wrap items-center gap-2\" data-testid=\"condition-row\">\n {index > 0 ? (\n <Tag variant=\"neutral\" className=\"shrink-0 uppercase\">\n {model.combine === 'and'\n ? t('forms.studio.logic.visibility.combine.and')\n : t('forms.studio.logic.visibility.combine.or')}\n </Tag>\n ) : null}\n <Select\n value={row.source}\n onValueChange={(value) => {\n const nextOperators = operatorOptions(sourceByValue.get(value))\n updateRow({\n source: value,\n ...(nextOperators.includes(row.operator) ? {} : { operator: nextOperators[0] ?? 'eq', value: '' }),\n })\n }}\n >\n <SelectTrigger size=\"sm\" className=\"min-w-0 max-w-full flex-1 basis-40\">\n <SelectValue placeholder={t('forms.studio.logic.visibility.sourcePlaceholder')} />\n </SelectTrigger>\n <SelectContent>\n {sources.map((source) => (\n <SelectItem key={source.value} value={source.value}>\n {source.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <Select\n value={operator}\n onValueChange={(value) => updateRow({ operator: value as ConditionOperator })}\n >\n <SelectTrigger size=\"sm\" className=\"min-w-0 max-w-full flex-1 basis-40\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {operators.map((op) => (\n <SelectItem key={op} value={op}>\n {t(`forms.studio.logic.visibility.op.${op}`)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {!valueDisabled ? (\n <Input\n size=\"sm\"\n className=\"min-w-0 max-w-full flex-1 basis-32\"\n value={String(row.value ?? '')}\n onChange={(event) => updateRow({ value: event.target.value })}\n />\n ) : null}\n <IconButton\n aria-label={t('forms.studio.logic.visibility.remove')}\n variant=\"ghost\"\n size=\"sm\"\n className=\"shrink-0\"\n type=\"button\"\n onClick={removeRow}\n >\n <Trash2 className=\"size-4\" />\n </IconButton>\n </div>\n )\n })}\n </div>\n {model.rows.length > 1 ? (\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-muted-foreground\">{t('forms.studio.logic.visibility.combineLabel')}</span>\n <RadioGroup\n value={model.combine}\n onValueChange={(next) => commit({ ...model, combine: next as 'and' | 'or' })}\n className=\"flex-row gap-3\"\n >\n <RadioField value=\"and\" label={t('forms.studio.logic.visibility.combine.and')} />\n <RadioField value=\"or\" label={t('forms.studio.logic.visibility.combine.or')} />\n </RadioGroup>\n </div>\n ) : null}\n {model.rows.length > 0 ? (\n <details className=\"mt-2\">\n <summary className=\"cursor-pointer text-xs font-medium uppercase text-muted-foreground\">\n {t('forms.studio.logic.visibility.compiledJson')}\n </summary>\n <pre className=\"mt-1 max-h-40 overflow-auto rounded-md border border-border bg-muted/30 p-2 text-xs\">\n {JSON.stringify(compilePredicate(model), null, 2)}\n </pre>\n </details>\n ) : null}\n </div>\n )\n}\n\n/** Convenience builder of source options from a `FormSchema`. */\nexport function buildFieldSourceOptions(schema: FormSchema, currentLocale: string, t: ReturnType<typeof useT>): ConditionSourceOption[] {\n const result: ConditionSourceOption[] = []\n for (const [key, node] of Object.entries(schema.properties)) {\n const omType = String((node as Record<string, unknown>)['x-om-type'] ?? '')\n if (omType === 'info_block') continue\n const labelMap = (node as Record<string, unknown>)['x-om-label']\n const label =\n labelMap && typeof labelMap === 'object' && !Array.isArray(labelMap)\n ? String((labelMap as Record<string, unknown>)[currentLocale] ?? (labelMap as Record<string, unknown>).en ?? key)\n : key\n const typeLabel = resolveTypeLabel(omType, t)\n result.push({ value: key, label: `${label} \u2014 ${typeLabel}`, fieldType: omType, namespace: 'field' })\n }\n const hiddenFields = (schema as Record<string, unknown>)['x-om-hidden-fields']\n if (Array.isArray(hiddenFields)) {\n for (const entry of hiddenFields) {\n if (!entry || typeof entry !== 'object') continue\n const name = (entry as Record<string, unknown>).name\n if (typeof name !== 'string') continue\n result.push({\n value: `hidden.${name}`,\n label: `${name} \u2014 ${t('forms.studio.logic.source.hidden')}`,\n fieldType: null,\n namespace: 'hidden',\n })\n }\n }\n const variables = (schema as Record<string, unknown>)['x-om-variables']\n if (Array.isArray(variables)) {\n for (const entry of variables) {\n if (!entry || typeof entry !== 'object') continue\n const name = (entry as Record<string, unknown>).name\n if (typeof name !== 'string') continue\n const variableType = String((entry as Record<string, unknown>).type ?? '')\n result.push({\n value: `var.${name}`,\n label: `${name} \u2014 ${t('forms.studio.logic.source.variable')}`,\n fieldType: variableType,\n namespace: 'variable',\n })\n }\n }\n return result\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -75,6 +75,12 @@ function parseSingleRow(expression) {
|
|
|
75
75
|
if (!(op in operatorMap)) return null;
|
|
76
76
|
if (!Array.isArray(args) || args.length < 2) return null;
|
|
77
77
|
const [left, right] = args;
|
|
78
|
+
if (op === "in" && typeof left === "string" && right && typeof right === "object" && !Array.isArray(right)) {
|
|
79
|
+
const path2 = right.var;
|
|
80
|
+
if (Object.keys(right).length === 1 && typeof path2 === "string") {
|
|
81
|
+
return { id: nextRowId(), source: path2, operator: "contains", value: left };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
78
84
|
if (!left || typeof left !== "object" || Array.isArray(left)) return null;
|
|
79
85
|
const leftKeys = Object.keys(left);
|
|
80
86
|
if (leftKeys.length !== 1 || leftKeys[0] !== "var") return null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/forms/backend/forms/%5Bid%5D/studio/logic/condition-model.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Pure helpers for the ConditionBuilder UI model.\n *\n * The builder UI presents a flat list of `(source, operator, value)` rows\n * combined with AND/OR. The persisted shape is a single jsonlogic node:\n *\n * - One row \u2192 `{ <op>: [{ var: <path> }, <value>] }`\n * - Multiple rows \u2192 `{ and|or: [<row>, \u2026] }`\n *\n * Operators are typed per-source: text/textarea \u2192 equals / contains / empty;\n * numeric \u2192 comparisons; bool \u2192 is true / is false; etc.\n *\n * Round-trip is intentionally lossy: if the persisted jsonlogic doesn't\n * match the shape this builder produces, the UI surfaces a raw-only view\n * and the user can clear the predicate but not edit it visually.\n */\n\nimport { classifyVarPath } from '../../../../../schema/jsonlogic-grammar'\n\nexport type ConditionOperator =\n | 'eq'\n | 'neq'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'contains'\n | 'is_empty'\n | 'is_not_empty'\n | 'is_true'\n | 'is_false'\n | 'in'\n\nexport type ConditionRow = {\n id: string\n source: string\n operator: ConditionOperator\n value: string | number | boolean | null\n}\n\nexport type ConditionBuilderModel = {\n rows: ConditionRow[]\n combine: 'and' | 'or'\n /** True when the persisted shape couldn't be parsed into rows. */\n raw: unknown | null\n}\n\nlet idCounter = 0\nfunction nextRowId(): string {\n idCounter += 1\n return `cond_${idCounter}`\n}\n\nexport function emptyModel(): ConditionBuilderModel {\n return { rows: [], combine: 'and', raw: null }\n}\n\nexport function newRow(source: string = ''): ConditionRow {\n return { id: nextRowId(), source, operator: 'eq', value: '' }\n}\n\n/**\n * Parses a persisted jsonlogic predicate into the builder model. Returns\n * `{ rows: [], combine: 'and', raw: <expression> }` when the shape is\n * unsupported so the UI can render a raw-only view.\n */\nexport function parsePredicate(predicate: unknown): ConditionBuilderModel {\n if (predicate === null || predicate === undefined) return emptyModel()\n if (typeof predicate !== 'object' || Array.isArray(predicate)) {\n return { rows: [], combine: 'and', raw: predicate }\n }\n const keys = Object.keys(predicate as Record<string, unknown>)\n if (keys.length !== 1) return { rows: [], combine: 'and', raw: predicate }\n const op = keys[0]\n if (op === 'and' || op === 'or') {\n const args = (predicate as Record<string, unknown>)[op]\n if (!Array.isArray(args)) return { rows: [], combine: 'and', raw: predicate }\n const rows: ConditionRow[] = []\n for (const entry of args) {\n const parsed = parseSingleRow(entry)\n if (!parsed) return { rows: [], combine: 'and', raw: predicate }\n rows.push(parsed)\n }\n return { rows, combine: op, raw: null }\n }\n const singleRow = parseSingleRow(predicate)\n if (!singleRow) return { rows: [], combine: 'and', raw: predicate }\n return { rows: [singleRow], combine: 'and', raw: null }\n}\n\nfunction parseSingleRow(expression: unknown): ConditionRow | null {\n if (!expression || typeof expression !== 'object' || Array.isArray(expression)) return null\n const keys = Object.keys(expression as Record<string, unknown>)\n if (keys.length !== 1) return null\n const op = keys[0]\n const args = (expression as Record<string, unknown>)[op]\n const operatorMap: Record<string, ConditionOperator> = {\n '==': 'eq',\n '!=': 'neq',\n '>': 'gt',\n '>=': 'gte',\n '<': 'lt',\n '<=': 'lte',\n in: 'in',\n }\n if (op === '!') {\n const inner = Array.isArray(args) ? args[0] : args\n if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null\n const innerKeys = Object.keys(inner as Record<string, unknown>)\n if (innerKeys.length === 1 && innerKeys[0] === 'var') {\n const path = (inner as Record<string, unknown>).var\n if (typeof path === 'string') {\n return { id: nextRowId(), source: path, operator: 'is_empty', value: null }\n }\n }\n return null\n }\n if (op === '!!') {\n const inner = Array.isArray(args) ? args[0] : args\n if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null\n const innerKeys = Object.keys(inner as Record<string, unknown>)\n if (innerKeys.length === 1 && innerKeys[0] === 'var') {\n const path = (inner as Record<string, unknown>).var\n if (typeof path === 'string') {\n return { id: nextRowId(), source: path, operator: 'is_not_empty', value: null }\n }\n }\n return null\n }\n if (!(op in operatorMap)) return null\n if (!Array.isArray(args) || args.length < 2) return null\n const [left, right] = args\n if (!left || typeof left !== 'object' || Array.isArray(left)) return null\n const leftKeys = Object.keys(left as Record<string, unknown>)\n if (leftKeys.length !== 1 || leftKeys[0] !== 'var') return null\n const path = (left as Record<string, unknown>).var\n if (typeof path !== 'string') return null\n const operator: ConditionOperator = operatorMap[op]\n if (operator === 'eq' && right === true) {\n return { id: nextRowId(), source: path, operator: 'is_true', value: null }\n }\n if (operator === 'eq' && right === false) {\n return { id: nextRowId(), source: path, operator: 'is_false', value: null }\n }\n if (operator === 'in' && typeof right === 'string') {\n return { id: nextRowId(), source: path, operator: 'contains', value: right }\n }\n if (typeof right !== 'string' && typeof right !== 'number' && typeof right !== 'boolean' && right !== null) {\n return null\n }\n return { id: nextRowId(), source: path, operator, value: right }\n}\n\n/**\n * Compiles the builder model into a jsonlogic expression. Returns `null`\n * for an empty rule list \u2014 the caller deletes the keyword in that case.\n * If `raw` is set (unsupported persisted shape), the raw is preserved.\n */\nexport function compilePredicate(model: ConditionBuilderModel): unknown | null {\n if (model.raw !== null && model.rows.length === 0) return model.raw\n if (model.rows.length === 0) return null\n const compiledRows = model.rows.map(compileRow).filter((entry): entry is object => entry !== null)\n if (compiledRows.length === 0) return null\n if (compiledRows.length === 1) return compiledRows[0]\n return { [model.combine]: compiledRows }\n}\n\nfunction compileRow(row: ConditionRow): object | null {\n if (!row.source) return null\n const source = { var: row.source }\n switch (row.operator) {\n case 'eq':\n return { '==': [source, row.value] }\n case 'neq':\n return { '!=': [source, row.value] }\n case 'gt':\n return { '>': [source, coerceNumber(row.value)] }\n case 'gte':\n return { '>=': [source, coerceNumber(row.value)] }\n case 'lt':\n return { '<': [source, coerceNumber(row.value)] }\n case 'lte':\n return { '<=': [source, coerceNumber(row.value)] }\n case 'contains':\n return { in: [String(row.value ?? ''), source] }\n case 'is_empty':\n return { '!': [source] }\n case 'is_not_empty':\n return { '!!': [source] }\n case 'is_true':\n return { '==': [source, true] }\n case 'is_false':\n return { '==': [source, false] }\n case 'in':\n return { in: [source, row.value] }\n }\n}\n\nfunction coerceNumber(value: unknown): number {\n if (typeof value === 'number') return value\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nexport function describeSource(source: string): { namespace: 'field' | 'hidden' | 'variable'; name: string } {\n return classifyVarPath(source)\n}\n"],
|
|
5
|
-
"mappings": "AAiBA,SAAS,uBAAuB;AA8BhC,IAAI,YAAY;AAChB,SAAS,YAAoB;AAC3B,eAAa;AACb,SAAO,QAAQ,SAAS;AAC1B;AAEO,SAAS,aAAoC;AAClD,SAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,KAAK,KAAK;AAC/C;AAEO,SAAS,OAAO,SAAiB,IAAkB;AACxD,SAAO,EAAE,IAAI,UAAU,GAAG,QAAQ,UAAU,MAAM,OAAO,GAAG;AAC9D;AAOO,SAAS,eAAe,WAA2C;AACxE,MAAI,cAAc,QAAQ,cAAc,OAAW,QAAO,WAAW;AACrE,MAAI,OAAO,cAAc,YAAY,MAAM,QAAQ,SAAS,GAAG;AAC7D,WAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,KAAK,UAAU;AAAA,EACpD;AACA,QAAM,OAAO,OAAO,KAAK,SAAoC;AAC7D,MAAI,KAAK,WAAW,EAAG,QAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,KAAK,UAAU;AACzE,QAAM,KAAK,KAAK,CAAC;AACjB,MAAI,OAAO,SAAS,OAAO,MAAM;AAC/B,UAAM,OAAQ,UAAsC,EAAE;AACtD,QAAI,CAAC,MAAM,QAAQ,IAAI,EAAG,QAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,KAAK,UAAU;AAC5E,UAAM,OAAuB,CAAC;AAC9B,eAAW,SAAS,MAAM;AACxB,YAAM,SAAS,eAAe,KAAK;AACnC,UAAI,CAAC,OAAQ,QAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,KAAK,UAAU;AAC/D,WAAK,KAAK,MAAM;AAAA,IAClB;AACA,WAAO,EAAE,MAAM,SAAS,IAAI,KAAK,KAAK;AAAA,EACxC;AACA,QAAM,YAAY,eAAe,SAAS;AAC1C,MAAI,CAAC,UAAW,QAAO,EAAE,MAAM,CAAC,GAAG,SAAS,OAAO,KAAK,UAAU;AAClE,SAAO,EAAE,MAAM,CAAC,SAAS,GAAG,SAAS,OAAO,KAAK,KAAK;AACxD;AAEA,SAAS,eAAe,YAA0C;AAChE,MAAI,CAAC,cAAc,OAAO,eAAe,YAAY,MAAM,QAAQ,UAAU,EAAG,QAAO;AACvF,QAAM,OAAO,OAAO,KAAK,UAAqC;AAC9D,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,KAAK,KAAK,CAAC;AACjB,QAAM,OAAQ,WAAuC,EAAE;AACvD,QAAM,cAAiD;AAAA,IACrD,MAAM;AAAA,IACN,MAAM;AAAA,IACN,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,IACL,MAAM;AAAA,IACN,IAAI;AAAA,EACN;AACA,MAAI,OAAO,KAAK;AACd,UAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI;AAC9C,QAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,UAAM,YAAY,OAAO,KAAK,KAAgC;AAC9D,QAAI,UAAU,WAAW,KAAK,UAAU,CAAC,MAAM,OAAO;AACpD,YAAMA,QAAQ,MAAkC;AAChD,UAAI,OAAOA,UAAS,UAAU;AAC5B,eAAO,EAAE,IAAI,UAAU,GAAG,QAAQA,OAAM,UAAU,YAAY,OAAO,KAAK;AAAA,MAC5E;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,MAAM;AACf,UAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI;AAC9C,QAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,UAAM,YAAY,OAAO,KAAK,KAAgC;AAC9D,QAAI,UAAU,WAAW,KAAK,UAAU,CAAC,MAAM,OAAO;AACpD,YAAMA,QAAQ,MAAkC;AAChD,UAAI,OAAOA,UAAS,UAAU;AAC5B,eAAO,EAAE,IAAI,UAAU,GAAG,QAAQA,OAAM,UAAU,gBAAgB,OAAO,KAAK;AAAA,MAChF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,MAAI,EAAE,MAAM,aAAc,QAAO;AACjC,MAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,KAAK,SAAS,EAAG,QAAO;AACpD,QAAM,CAAC,MAAM,KAAK,IAAI;AACtB,MAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,EAAG,QAAO;AACrE,QAAM,WAAW,OAAO,KAAK,IAA+B;AAC5D,MAAI,SAAS,WAAW,KAAK,SAAS,CAAC,MAAM,MAAO,QAAO;AAC3D,QAAM,OAAQ,KAAiC;AAC/C,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,QAAM,WAA8B,YAAY,EAAE;AAClD,MAAI,aAAa,QAAQ,UAAU,MAAM;AACvC,WAAO,EAAE,IAAI,UAAU,GAAG,QAAQ,MAAM,UAAU,WAAW,OAAO,KAAK;AAAA,EAC3E;AACA,MAAI,aAAa,QAAQ,UAAU,OAAO;AACxC,WAAO,EAAE,IAAI,UAAU,GAAG,QAAQ,MAAM,UAAU,YAAY,OAAO,KAAK;AAAA,EAC5E;AACA,MAAI,aAAa,QAAQ,OAAO,UAAU,UAAU;AAClD,WAAO,EAAE,IAAI,UAAU,GAAG,QAAQ,MAAM,UAAU,YAAY,OAAO,MAAM;AAAA,EAC7E;AACA,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,aAAa,UAAU,MAAM;AAC1G,WAAO;AAAA,EACT;AACA,SAAO,EAAE,IAAI,UAAU,GAAG,QAAQ,MAAM,UAAU,OAAO,MAAM;AACjE;AAOO,SAAS,iBAAiB,OAA8C;AAC7E,MAAI,MAAM,QAAQ,QAAQ,MAAM,KAAK,WAAW,EAAG,QAAO,MAAM;AAChE,MAAI,MAAM,KAAK,WAAW,EAAG,QAAO;AACpC,QAAM,eAAe,MAAM,KAAK,IAAI,UAAU,EAAE,OAAO,CAAC,UAA2B,UAAU,IAAI;AACjG,MAAI,aAAa,WAAW,EAAG,QAAO;AACtC,MAAI,aAAa,WAAW,EAAG,QAAO,aAAa,CAAC;AACpD,SAAO,EAAE,CAAC,MAAM,OAAO,GAAG,aAAa;AACzC;AAEA,SAAS,WAAW,KAAkC;AACpD,MAAI,CAAC,IAAI,OAAQ,QAAO;AACxB,QAAM,SAAS,EAAE,KAAK,IAAI,OAAO;AACjC,UAAQ,IAAI,UAAU;AAAA,IACpB,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,QAAQ,IAAI,KAAK,EAAE;AAAA,IACrC,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,QAAQ,IAAI,KAAK,EAAE;AAAA,IACrC,KAAK;AACH,aAAO,EAAE,KAAK,CAAC,QAAQ,aAAa,IAAI,KAAK,CAAC,EAAE;AAAA,IAClD,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,QAAQ,aAAa,IAAI,KAAK,CAAC,EAAE;AAAA,IACnD,KAAK;AACH,aAAO,EAAE,KAAK,CAAC,QAAQ,aAAa,IAAI,KAAK,CAAC,EAAE;AAAA,IAClD,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,QAAQ,aAAa,IAAI,KAAK,CAAC,EAAE;AAAA,IACnD,KAAK;AACH,aAAO,EAAE,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,GAAG,MAAM,EAAE;AAAA,IACjD,KAAK;AACH,aAAO,EAAE,KAAK,CAAC,MAAM,EAAE;AAAA,IACzB,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,MAAM,EAAE;AAAA,IAC1B,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,QAAQ,IAAI,EAAE;AAAA,IAChC,KAAK;AACH,aAAO,EAAE,MAAM,CAAC,QAAQ,KAAK,EAAE;AAAA,IACjC,KAAK;AACH,aAAO,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK,EAAE;AAAA,EACrC;AACF;AAEA,SAAS,aAAa,OAAwB;AAC5C,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,SAAS,OAAO,KAAK;AAC3B,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AAEO,SAAS,eAAe,QAA8E;AAC3G,SAAO,gBAAgB,MAAM;AAC/B;",
|
|
4
|
+
"sourcesContent": ["/**\n * Pure helpers for the ConditionBuilder UI model.\n *\n * The builder UI presents a flat list of `(source, operator, value)` rows\n * combined with AND/OR. The persisted shape is a single jsonlogic node:\n *\n * - One row \u2192 `{ <op>: [{ var: <path> }, <value>] }`\n * - Multiple rows \u2192 `{ and|or: [<row>, \u2026] }`\n *\n * Operators are typed per-source: text/textarea \u2192 equals / contains / empty;\n * numeric \u2192 comparisons; bool \u2192 is true / is false; etc.\n *\n * Round-trip is intentionally lossy: if the persisted jsonlogic doesn't\n * match the shape this builder produces, the UI surfaces a raw-only view\n * and the user can clear the predicate but not edit it visually.\n */\n\nimport { classifyVarPath } from '../../../../../schema/jsonlogic-grammar'\n\nexport type ConditionOperator =\n | 'eq'\n | 'neq'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'contains'\n | 'is_empty'\n | 'is_not_empty'\n | 'is_true'\n | 'is_false'\n | 'in'\n\nexport type ConditionRow = {\n id: string\n source: string\n operator: ConditionOperator\n value: string | number | boolean | null\n}\n\nexport type ConditionBuilderModel = {\n rows: ConditionRow[]\n combine: 'and' | 'or'\n /** True when the persisted shape couldn't be parsed into rows. */\n raw: unknown | null\n}\n\nlet idCounter = 0\nfunction nextRowId(): string {\n idCounter += 1\n return `cond_${idCounter}`\n}\n\nexport function emptyModel(): ConditionBuilderModel {\n return { rows: [], combine: 'and', raw: null }\n}\n\nexport function newRow(source: string = ''): ConditionRow {\n return { id: nextRowId(), source, operator: 'eq', value: '' }\n}\n\n/**\n * Parses a persisted jsonlogic predicate into the builder model. Returns\n * `{ rows: [], combine: 'and', raw: <expression> }` when the shape is\n * unsupported so the UI can render a raw-only view.\n */\nexport function parsePredicate(predicate: unknown): ConditionBuilderModel {\n if (predicate === null || predicate === undefined) return emptyModel()\n if (typeof predicate !== 'object' || Array.isArray(predicate)) {\n return { rows: [], combine: 'and', raw: predicate }\n }\n const keys = Object.keys(predicate as Record<string, unknown>)\n if (keys.length !== 1) return { rows: [], combine: 'and', raw: predicate }\n const op = keys[0]\n if (op === 'and' || op === 'or') {\n const args = (predicate as Record<string, unknown>)[op]\n if (!Array.isArray(args)) return { rows: [], combine: 'and', raw: predicate }\n const rows: ConditionRow[] = []\n for (const entry of args) {\n const parsed = parseSingleRow(entry)\n if (!parsed) return { rows: [], combine: 'and', raw: predicate }\n rows.push(parsed)\n }\n return { rows, combine: op, raw: null }\n }\n const singleRow = parseSingleRow(predicate)\n if (!singleRow) return { rows: [], combine: 'and', raw: predicate }\n return { rows: [singleRow], combine: 'and', raw: null }\n}\n\nfunction parseSingleRow(expression: unknown): ConditionRow | null {\n if (!expression || typeof expression !== 'object' || Array.isArray(expression)) return null\n const keys = Object.keys(expression as Record<string, unknown>)\n if (keys.length !== 1) return null\n const op = keys[0]\n const args = (expression as Record<string, unknown>)[op]\n const operatorMap: Record<string, ConditionOperator> = {\n '==': 'eq',\n '!=': 'neq',\n '>': 'gt',\n '>=': 'gte',\n '<': 'lt',\n '<=': 'lte',\n in: 'in',\n }\n if (op === '!') {\n const inner = Array.isArray(args) ? args[0] : args\n if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null\n const innerKeys = Object.keys(inner as Record<string, unknown>)\n if (innerKeys.length === 1 && innerKeys[0] === 'var') {\n const path = (inner as Record<string, unknown>).var\n if (typeof path === 'string') {\n return { id: nextRowId(), source: path, operator: 'is_empty', value: null }\n }\n }\n return null\n }\n if (op === '!!') {\n const inner = Array.isArray(args) ? args[0] : args\n if (!inner || typeof inner !== 'object' || Array.isArray(inner)) return null\n const innerKeys = Object.keys(inner as Record<string, unknown>)\n if (innerKeys.length === 1 && innerKeys[0] === 'var') {\n const path = (inner as Record<string, unknown>).var\n if (typeof path === 'string') {\n return { id: nextRowId(), source: path, operator: 'is_not_empty', value: null }\n }\n }\n return null\n }\n if (!(op in operatorMap)) return null\n if (!Array.isArray(args) || args.length < 2) return null\n const [left, right] = args\n if (op === 'in' && typeof left === 'string' && right && typeof right === 'object' && !Array.isArray(right)) {\n const path = (right as Record<string, unknown>).var\n if (Object.keys(right).length === 1 && typeof path === 'string') {\n return { id: nextRowId(), source: path, operator: 'contains', value: left }\n }\n }\n if (!left || typeof left !== 'object' || Array.isArray(left)) return null\n const leftKeys = Object.keys(left as Record<string, unknown>)\n if (leftKeys.length !== 1 || leftKeys[0] !== 'var') return null\n const path = (left as Record<string, unknown>).var\n if (typeof path !== 'string') return null\n const operator: ConditionOperator = operatorMap[op]\n if (operator === 'eq' && right === true) {\n return { id: nextRowId(), source: path, operator: 'is_true', value: null }\n }\n if (operator === 'eq' && right === false) {\n return { id: nextRowId(), source: path, operator: 'is_false', value: null }\n }\n if (operator === 'in' && typeof right === 'string') {\n return { id: nextRowId(), source: path, operator: 'contains', value: right }\n }\n if (typeof right !== 'string' && typeof right !== 'number' && typeof right !== 'boolean' && right !== null) {\n return null\n }\n return { id: nextRowId(), source: path, operator, value: right }\n}\n\n/**\n * Compiles the builder model into a jsonlogic expression. Returns `null`\n * for an empty rule list \u2014 the caller deletes the keyword in that case.\n * If `raw` is set (unsupported persisted shape), the raw is preserved.\n */\nexport function compilePredicate(model: ConditionBuilderModel): unknown | null {\n if (model.raw !== null && model.rows.length === 0) return model.raw\n if (model.rows.length === 0) return null\n const compiledRows = model.rows.map(compileRow).filter((entry): entry is object => entry !== null)\n if (compiledRows.length === 0) return null\n if (compiledRows.length === 1) return compiledRows[0]\n return { [model.combine]: compiledRows }\n}\n\nfunction compileRow(row: ConditionRow): object | null {\n if (!row.source) return null\n const source = { var: row.source }\n switch (row.operator) {\n case 'eq':\n return { '==': [source, row.value] }\n case 'neq':\n return { '!=': [source, row.value] }\n case 'gt':\n return { '>': [source, coerceNumber(row.value)] }\n case 'gte':\n return { '>=': [source, coerceNumber(row.value)] }\n case 'lt':\n return { '<': [source, coerceNumber(row.value)] }\n case 'lte':\n return { '<=': [source, coerceNumber(row.value)] }\n case 'contains':\n return { in: [String(row.value ?? ''), source] }\n case 'is_empty':\n return { '!': [source] }\n case 'is_not_empty':\n return { '!!': [source] }\n case 'is_true':\n return { '==': [source, true] }\n case 'is_false':\n return { '==': [source, false] }\n case 'in':\n return { in: [source, row.value] }\n }\n}\n\nfunction coerceNumber(value: unknown): number {\n if (typeof value === 'number') return value\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\nexport function describeSource(source: string): { namespace: 'field' | 'hidden' | 'variable'; name: string } {\n return classifyVarPath(source)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["path"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7870.1.63e4c252d3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.6.6"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7870.1.63e4c252d3",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7870.1.63e4c252d3",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7870.1.63e4c252d3",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7870.1.63e4c252d3",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7870.1.63e4c252d3",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7870.1.63e4c252d3",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -94,12 +94,12 @@ export async function drainIntegrationQueue(queueName: string, options: DrainQue
|
|
|
94
94
|
appRoot: resolveAppRoot(options.appRoot),
|
|
95
95
|
jobLimit: options.jobLimit ?? DEFAULT_JOB_LIMIT,
|
|
96
96
|
}
|
|
97
|
-
if (process.env.OM_TEST_APP_ROOT?.trim()) {
|
|
98
|
-
return drainQueueInAppProcess(queueName, resolved)
|
|
99
|
-
}
|
|
100
97
|
const managed = parseBooleanToken(process.env.AUTO_SPAWN_WORKERS)
|
|
101
98
|
?? parseBooleanToken(process.env.OM_AUTO_SPAWN_WORKERS)
|
|
102
99
|
?? true
|
|
103
100
|
if (managed) return waitForManagedQueue(queueName, resolved)
|
|
101
|
+
if (process.env.OM_TEST_APP_ROOT?.trim()) {
|
|
102
|
+
return drainQueueInAppProcess(queueName, resolved)
|
|
103
|
+
}
|
|
104
104
|
return drainQueueInCurrentProcess(queueName, resolved)
|
|
105
105
|
}
|
|
@@ -104,7 +104,7 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
return (
|
|
107
|
-
<div className="space-y-3" data-testid="condition-builder">
|
|
107
|
+
<div className="min-w-0 space-y-3" data-testid="condition-builder">
|
|
108
108
|
<div className="flex items-center justify-between">
|
|
109
109
|
<h4 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
110
110
|
{t('forms.studio.logic.visibility.heading')}
|
|
@@ -113,7 +113,11 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
113
113
|
variant="outline"
|
|
114
114
|
size="sm"
|
|
115
115
|
type="button"
|
|
116
|
-
onClick={() =>
|
|
116
|
+
onClick={() => {
|
|
117
|
+
const source = sources[0]
|
|
118
|
+
const row = { ...newRow(source?.value ?? ''), operator: operatorOptions(source)[0] ?? 'eq' }
|
|
119
|
+
commit({ ...model, rows: [...model.rows, row] })
|
|
120
|
+
}}
|
|
117
121
|
>
|
|
118
122
|
{t('forms.studio.logic.visibility.addRule')}
|
|
119
123
|
</Button>
|
|
@@ -124,8 +128,11 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
124
128
|
<div className="space-y-2">
|
|
125
129
|
{model.rows.map((row, index) => {
|
|
126
130
|
const sourceOption = sourceByValue.get(row.source)
|
|
127
|
-
const
|
|
128
|
-
const
|
|
131
|
+
const supportedOperators = operatorOptions(sourceOption)
|
|
132
|
+
const operators = supportedOperators.includes(row.operator)
|
|
133
|
+
? supportedOperators
|
|
134
|
+
: [row.operator, ...supportedOperators]
|
|
135
|
+
const operator = row.operator
|
|
129
136
|
const valueDisabled =
|
|
130
137
|
operator === 'is_empty' || operator === 'is_not_empty' || operator === 'is_true' || operator === 'is_false'
|
|
131
138
|
const updateRow = (partial: Partial<ConditionRow>) => {
|
|
@@ -136,7 +143,7 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
136
143
|
commit({ ...model, rows: model.rows.filter((entry) => entry.id !== row.id) })
|
|
137
144
|
}
|
|
138
145
|
return (
|
|
139
|
-
<div key={row.id} className="flex items-center gap-2" data-testid="condition-row">
|
|
146
|
+
<div key={row.id} className="flex min-w-0 flex-wrap items-center gap-2" data-testid="condition-row">
|
|
140
147
|
{index > 0 ? (
|
|
141
148
|
<Tag variant="neutral" className="shrink-0 uppercase">
|
|
142
149
|
{model.combine === 'and'
|
|
@@ -144,8 +151,17 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
144
151
|
: t('forms.studio.logic.visibility.combine.or')}
|
|
145
152
|
</Tag>
|
|
146
153
|
) : null}
|
|
147
|
-
<Select
|
|
148
|
-
|
|
154
|
+
<Select
|
|
155
|
+
value={row.source}
|
|
156
|
+
onValueChange={(value) => {
|
|
157
|
+
const nextOperators = operatorOptions(sourceByValue.get(value))
|
|
158
|
+
updateRow({
|
|
159
|
+
source: value,
|
|
160
|
+
...(nextOperators.includes(row.operator) ? {} : { operator: nextOperators[0] ?? 'eq', value: '' }),
|
|
161
|
+
})
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<SelectTrigger size="sm" className="min-w-0 max-w-full flex-1 basis-40">
|
|
149
165
|
<SelectValue placeholder={t('forms.studio.logic.visibility.sourcePlaceholder')} />
|
|
150
166
|
</SelectTrigger>
|
|
151
167
|
<SelectContent>
|
|
@@ -160,7 +176,7 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
160
176
|
value={operator}
|
|
161
177
|
onValueChange={(value) => updateRow({ operator: value as ConditionOperator })}
|
|
162
178
|
>
|
|
163
|
-
<SelectTrigger className="
|
|
179
|
+
<SelectTrigger size="sm" className="min-w-0 max-w-full flex-1 basis-40">
|
|
164
180
|
<SelectValue />
|
|
165
181
|
</SelectTrigger>
|
|
166
182
|
<SelectContent>
|
|
@@ -171,16 +187,19 @@ export function ConditionBuilder({ predicate, sources, onChange }: ConditionBuil
|
|
|
171
187
|
))}
|
|
172
188
|
</SelectContent>
|
|
173
189
|
</Select>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
{!valueDisabled ? (
|
|
191
|
+
<Input
|
|
192
|
+
size="sm"
|
|
193
|
+
className="min-w-0 max-w-full flex-1 basis-32"
|
|
194
|
+
value={String(row.value ?? '')}
|
|
195
|
+
onChange={(event) => updateRow({ value: event.target.value })}
|
|
196
|
+
/>
|
|
197
|
+
) : null}
|
|
180
198
|
<IconButton
|
|
181
199
|
aria-label={t('forms.studio.logic.visibility.remove')}
|
|
182
200
|
variant="ghost"
|
|
183
201
|
size="sm"
|
|
202
|
+
className="shrink-0"
|
|
184
203
|
type="button"
|
|
185
204
|
onClick={removeRow}
|
|
186
205
|
>
|
|
@@ -130,6 +130,12 @@ function parseSingleRow(expression: unknown): ConditionRow | null {
|
|
|
130
130
|
if (!(op in operatorMap)) return null
|
|
131
131
|
if (!Array.isArray(args) || args.length < 2) return null
|
|
132
132
|
const [left, right] = args
|
|
133
|
+
if (op === 'in' && typeof left === 'string' && right && typeof right === 'object' && !Array.isArray(right)) {
|
|
134
|
+
const path = (right as Record<string, unknown>).var
|
|
135
|
+
if (Object.keys(right).length === 1 && typeof path === 'string') {
|
|
136
|
+
return { id: nextRowId(), source: path, operator: 'contains', value: left }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
133
139
|
if (!left || typeof left !== 'object' || Array.isArray(left)) return null
|
|
134
140
|
const leftKeys = Object.keys(left as Record<string, unknown>)
|
|
135
141
|
if (leftKeys.length !== 1 || leftKeys[0] !== 'var') return null
|