feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { Plus, Trash2 } from "lucide-react";
|
|
|
|
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
|
|
|
|
|
|
import { FormMappingPicker } from "./form-mapping-picker";
|
|
|
|
|
|
|
|
|
|
type FormField = {
|
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
|
|
|
|
type: string;
|
|
|
|
|
required: boolean;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
helpText?: string;
|
|
|
|
|
options?: { label: string; value: string }[];
|
|
|
|
|
defaultValue?: unknown;
|
|
|
|
|
mappedProperty: string | null;
|
|
|
|
|
validation?: {
|
|
|
|
|
min?: number;
|
|
|
|
|
max?: number;
|
|
|
|
|
pattern?: string;
|
|
|
|
|
maxLength?: number;
|
|
|
|
|
};
|
|
|
|
|
conditionals?: {
|
|
|
|
|
fieldId: string;
|
|
|
|
|
operator: string;
|
|
|
|
|
value: unknown;
|
|
|
|
|
action: string;
|
|
|
|
|
}[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const OPERATORS = ["eq", "neq", "contains", "isEmpty"] as const;
|
|
|
|
|
const ACTIONS = ["show", "hide"] as const;
|
|
|
|
|
|
|
|
|
|
const CHOICE_TYPES = new Set([
|
|
|
|
|
"select",
|
|
|
|
|
"multi_select",
|
|
|
|
|
"radio",
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
export function FormFieldConfig({
|
|
|
|
|
field,
|
|
|
|
|
allFields,
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
workspaceHandle,
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
onChange,
|
|
|
|
|
}: {
|
|
|
|
|
field: FormField;
|
|
|
|
|
allFields: FormField[];
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
workspaceHandle: string;
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
onChange: (patch: Partial<FormField>) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const otherFields = allFields.filter((f) => f.id !== field.id);
|
|
|
|
|
const v = field.validation ?? {};
|
|
|
|
|
|
|
|
|
|
const conditionals = field.conditionals ?? [];
|
|
|
|
|
|
|
|
|
|
const updateOption = (
|
|
|
|
|
index: number,
|
|
|
|
|
patch: Partial<{ label: string; value: string }>,
|
|
|
|
|
) => {
|
|
|
|
|
const options = [...(field.options ?? [])];
|
|
|
|
|
const current = options[index] ?? { label: "", value: "" };
|
|
|
|
|
options[index] = { ...current, ...patch };
|
|
|
|
|
onChange({ options });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addOption = () => {
|
|
|
|
|
const n = (field.options?.length ?? 0) + 1;
|
|
|
|
|
onChange({
|
|
|
|
|
options: [
|
|
|
|
|
...(field.options ?? []),
|
|
|
|
|
{ label: `Option ${n}`, value: `option_${n}` },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const removeOption = (index: number) => {
|
|
|
|
|
const options = [...(field.options ?? [])];
|
|
|
|
|
options.splice(index, 1);
|
|
|
|
|
onChange({ options: options.length ? options : undefined });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-4 rounded-lg border bg-card p-4 shadow-sm">
|
|
|
|
|
<div>
|
|
|
|
|
<h3 className="text-sm font-semibold text-foreground">Field settings</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{field.label} · {field.type.replaceAll("_", " ")}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label className="text-xs font-medium text-foreground" htmlFor="ff-label">
|
|
|
|
|
Label
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="ff-label"
|
|
|
|
|
value={field.label}
|
|
|
|
|
onChange={(e) => onChange({ label: e.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
className="text-xs font-medium text-foreground"
|
|
|
|
|
htmlFor="ff-placeholder"
|
|
|
|
|
>
|
|
|
|
|
Placeholder
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="ff-placeholder"
|
|
|
|
|
value={field.placeholder ?? ""}
|
|
|
|
|
placeholder="Optional"
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onChange({
|
|
|
|
|
placeholder: e.target.value || undefined,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label
|
|
|
|
|
className="text-xs font-medium text-foreground"
|
|
|
|
|
htmlFor="ff-help"
|
|
|
|
|
>
|
|
|
|
|
Help text
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
id="ff-help"
|
|
|
|
|
value={field.helpText ?? ""}
|
|
|
|
|
placeholder="Shown below the field"
|
|
|
|
|
rows={3}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onChange({
|
|
|
|
|
helpText: e.target.value || undefined,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
className="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<label className="flex cursor-pointer items-center gap-2 text-sm">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={field.required}
|
|
|
|
|
onChange={(e) => onChange({ required: e.target.checked })}
|
|
|
|
|
className="size-4 rounded border-input accent-primary"
|
|
|
|
|
/>
|
|
|
|
|
Required
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<span className="text-xs font-medium text-foreground">Validation</span>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="Min"
|
|
|
|
|
value={v.min ?? ""}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onChange({
|
|
|
|
|
validation: {
|
|
|
|
|
...v,
|
|
|
|
|
min:
|
|
|
|
|
e.target.value === ""
|
|
|
|
|
? undefined
|
|
|
|
|
: Number(e.target.value),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="Max"
|
|
|
|
|
value={v.max ?? ""}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onChange({
|
|
|
|
|
validation: {
|
|
|
|
|
...v,
|
|
|
|
|
max:
|
|
|
|
|
e.target.value === ""
|
|
|
|
|
? undefined
|
|
|
|
|
: Number(e.target.value),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Pattern (regex)"
|
|
|
|
|
value={v.pattern ?? ""}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onChange({
|
|
|
|
|
validation: {
|
|
|
|
|
...v,
|
|
|
|
|
pattern: e.target.value || undefined,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
placeholder="Max length"
|
|
|
|
|
value={v.maxLength ?? ""}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
onChange({
|
|
|
|
|
validation: {
|
|
|
|
|
...v,
|
|
|
|
|
maxLength:
|
|
|
|
|
e.target.value === ""
|
|
|
|
|
? undefined
|
|
|
|
|
: Number(e.target.value),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{CHOICE_TYPES.has(field.type) ? (
|
|
|
|
|
<>
|
|
|
|
|
<Separator />
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<span className="text-xs font-medium text-foreground">Options</span>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
{(field.options ?? []).map((opt, i) => (
|
|
|
|
|
<div key={i} className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Label"
|
|
|
|
|
value={opt.label}
|
|
|
|
|
onChange={(e) => updateOption(i, { label: e.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Value"
|
|
|
|
|
value={opt.value}
|
|
|
|
|
onChange={(e) => updateOption(i, { value: e.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="shrink-0"
|
|
|
|
|
onClick={() => removeOption(i)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="size-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="w-full gap-1"
|
|
|
|
|
onClick={addOption}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="size-4" />
|
|
|
|
|
Add option
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<span className="text-xs font-medium text-foreground">
|
|
|
|
|
Map to task property
|
|
|
|
|
</span>
|
|
|
|
|
<FormMappingPicker
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
workspaceHandle={workspaceHandle}
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
value={field.mappedProperty}
|
|
|
|
|
onChange={(next) => onChange({ mappedProperty: next })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Separator />
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<span className="text-xs font-medium text-foreground">
|
|
|
|
|
Conditional rules
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 gap-1 text-xs"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
onChange({
|
|
|
|
|
conditionals: [
|
|
|
|
|
...conditionals,
|
|
|
|
|
{
|
|
|
|
|
fieldId: otherFields[0]?.id ?? "",
|
|
|
|
|
operator: "eq",
|
|
|
|
|
value: "",
|
|
|
|
|
action: "show",
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="size-3.5" />
|
|
|
|
|
Add rule
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
{conditionals.length === 0 ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
No rules. Show or hide this field based on another field's value.
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<ul className="flex flex-col gap-3">
|
|
|
|
|
{conditionals.map((rule, index) => (
|
|
|
|
|
<li
|
|
|
|
|
key={index}
|
|
|
|
|
className="space-y-2 rounded-md border border-border p-2"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex justify-end">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="size-7"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const next = conditionals.filter((_, i) => i !== index);
|
|
|
|
|
onChange({
|
|
|
|
|
conditionals: next.length ? next : undefined,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="size-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<select
|
|
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
|
|
|
|
|
value={rule.fieldId}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const next = [...conditionals];
|
|
|
|
|
next[index] = { ...rule, fieldId: e.target.value };
|
|
|
|
|
onChange({ conditionals: next });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<option value="">Select field</option>
|
|
|
|
|
{otherFields.map((f) => (
|
|
|
|
|
<option key={f.id} value={f.id}>
|
|
|
|
|
{f.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
<select
|
|
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
|
|
|
|
|
value={rule.operator}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const next = [...conditionals];
|
|
|
|
|
next[index] = { ...rule, operator: e.target.value };
|
|
|
|
|
onChange({ conditionals: next });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{OPERATORS.map((op) => (
|
|
|
|
|
<option key={op} value={op}>
|
|
|
|
|
{op}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Value"
|
|
|
|
|
value={
|
|
|
|
|
rule.value === undefined || rule.value === null
|
|
|
|
|
? ""
|
|
|
|
|
: String(rule.value)
|
|
|
|
|
}
|
|
|
|
|
disabled={rule.operator === "isEmpty"}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const next = [...conditionals];
|
|
|
|
|
next[index] = { ...rule, value: e.target.value };
|
|
|
|
|
onChange({ conditionals: next });
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<select
|
|
|
|
|
className="flex h-9 w-full rounded-md border border-input bg-background px-2 text-sm"
|
|
|
|
|
value={rule.action}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const next = [...conditionals];
|
|
|
|
|
next[index] = { ...rule, action: e.target.value };
|
|
|
|
|
onChange({ conditionals: next });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{ACTIONS.map((a) => (
|
|
|
|
|
<option key={a} value={a}>
|
|
|
|
|
{a}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|