@open-mercato/core 0.10.1-develop.7865.1.9a16aa6194 → 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.
@@ -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 if (process.env.OM_TEST_APP_ROOT?.trim()) {\n return drainQueueInAppProcess(queueName, resolved)\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 return drainQueueInCurrentProcess(queueName, resolved)\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: () => commit({ ...model, rows: [...model.rows, newRow(sources[0]?.value ?? "")] }),
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 operators = operatorOptions(sourceOption);
96
- const operator = operators.includes(row.operator) ? row.operator : operators[0] ?? "eq";
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(Select, { value: row.source, onValueChange: (value) => updateRow({ source: value }), children: [
108
- /* @__PURE__ */ jsx(SelectTrigger, { className: "h-8 flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("forms.studio.logic.visibility.sourcePlaceholder") }) }),
109
- /* @__PURE__ */ jsx(SelectContent, { children: sources.map((source) => /* @__PURE__ */ jsx(SelectItem, { value: source.value, children: source.label }, source.value)) })
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: "h-8 w-32", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
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
- className: "h-8 w-32",
126
- value: valueDisabled ? "" : String(row.value ?? ""),
127
- disabled: valueDisabled,
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": "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",
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.7865.1.9a16aa6194",
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.7865.1.9a16aa6194",
270
- "@open-mercato/shared": "0.10.1-develop.7865.1.9a16aa6194",
271
- "@open-mercato/ui": "0.10.1-develop.7865.1.9a16aa6194",
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.7865.1.9a16aa6194",
277
- "@open-mercato/shared": "0.10.1-develop.7865.1.9a16aa6194",
278
- "@open-mercato/ui": "0.10.1-develop.7865.1.9a16aa6194",
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={() => commit({ ...model, rows: [...model.rows, newRow(sources[0]?.value ?? '')] })}
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 operators = operatorOptions(sourceOption)
128
- const operator = operators.includes(row.operator) ? row.operator : operators[0] ?? 'eq'
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 value={row.source} onValueChange={(value) => updateRow({ source: value })}>
148
- <SelectTrigger className="h-8 flex-1">
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="h-8 w-32">
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
- <Input
175
- className="h-8 w-32"
176
- value={valueDisabled ? '' : String(row.value ?? '')}
177
- disabled={valueDisabled}
178
- onChange={(event) => updateRow({ value: event.target.value })}
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