From caeba0e8c9f39c251e5d248b22af749801d6b742 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Tue, 6 Oct 2026 16:44:06 +0900 Subject: [PATCH 1/3] fix(data-table): preserve string-backed decimal filter values --- .changeset/tidy-decimals-flow.md | 7 + docs-manifest.json | 16 +- .../components/data-table.docs.outline.md | 33 +++-- docs/components/data-table.md | 33 +++-- .../components/data-table/data-table.test.tsx | 25 ++++ .../src/components/data-table/data-table.tsx | 10 +- .../data-table/field-helpers.test.ts | 23 +++ .../data-table/filter-value-utils.test.ts | 47 ++++++ .../data-table/filter-value-utils.ts | 36 +++++ .../components/data-table/toolbar.test.tsx | 138 ++++++++++++++++++ .../src/components/data-table/toolbar.tsx | 75 +++++++--- .../core/src/components/data-table/types.ts | 4 + .../hooks/use-collection-variables.test-d.ts | 28 ++++ .../hooks/use-collection-variables.test.ts | 34 ++++- .../core/src/lib/collection-url-state.test.ts | 17 +++ packages/core/src/types/collection.ts | 18 ++- packages/sdk-plugin/src/plugin.test.ts | 12 +- packages/sdk-plugin/src/plugin.ts | 2 +- 18 files changed, 501 insertions(+), 57 deletions(-) create mode 100644 .changeset/tidy-decimals-flow.md diff --git a/.changeset/tidy-decimals-flow.md b/.changeset/tidy-decimals-flow.md new file mode 100644 index 000000000..090e21e8e --- /dev/null +++ b/.changeset/tidy-decimals-flow.md @@ -0,0 +1,7 @@ +--- +"@tailor-platform/app-shell": patch +"@tailor-platform/sdk-plugin-app-shell": patch +--- + +Fix Decimal metadata and query types to use strings with numeric sort/filter operators, matching the GraphQL Decimal scalar. +Preserve decimal precision when adding, editing, or restoring DataTable filters. Regenerate SDK metadata to update decimal fields previously marked as `number`. diff --git a/docs-manifest.json b/docs-manifest.json index 63477342d..7b7ddc9ae 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -443,10 +443,10 @@ "withURLCollectionState" ], "hashes": { - "typeSurface": "014a9036a3b9b0af", - "outline": "f1bea006bdeb43a4", + "typeSurface": "9d5ce8a4539b7783", + "outline": "f81a7b9b8b3619ab", "snapshot": null, - "outputMd": "dd2da884d7550daf", + "outputMd": "e4078a35348b31db", "examples": null } }, @@ -605,7 +605,7 @@ "hashes": { "typeSurface": "b1614970f950ceb6", "outline": "bda40376d6e3e008", - "snapshot": "d4fa33f2da19d5ee", + "snapshot": "b845d4c54453b0f0", "outputMd": "ec6a0ac098d1d87a", "examples": "934c52d4e8761404" } @@ -1013,7 +1013,7 @@ "hashes": { "typeSurface": "943616def28cafb0", "outline": "8f06df28129676c8", - "snapshot": "02dfd02003105939", + "snapshot": "6233cd9de511210f", "outputMd": "ceb4a05822538d20", "examples": "cff0508bb5520ae6" } @@ -1166,7 +1166,7 @@ "hashes": { "typeSurface": "429710973dfa912b", "outline": "721e8995f53f0e8f", - "snapshot": "f6fcbd73e127f2f4", + "snapshot": "0ceac9786fb4ce3c", "outputMd": "ae287e2bb5abc1c6", "examples": "1ce89cec30045466" } @@ -1183,7 +1183,7 @@ "hashes": { "typeSurface": "e760949efcbe4c38", "outline": "464d91acc48f7c9d", - "snapshot": "12c20fc6f44d1b4c", + "snapshot": "40d15c3f1003fdb0", "outputMd": "1f81ebd562c50371", "examples": "b0eebb8c5e2219dc" } @@ -1673,7 +1673,7 @@ "packages/core/skills/app-shell-patterns/references/components/combobox.md": "dfb775c7c4307409", "packages/core/skills/app-shell-patterns/references/components/command-palette.md": "7e5bc675a593791a", "packages/core/skills/app-shell-patterns/references/components/csv-importer.md": "53d3c795ca21b9dc", - "packages/core/skills/app-shell-patterns/references/components/data-table.md": "dd2da884d7550daf", + "packages/core/skills/app-shell-patterns/references/components/data-table.md": "e4078a35348b31db", "packages/core/skills/app-shell-patterns/references/components/date-picker.md": "33902be121ff69db", "packages/core/skills/app-shell-patterns/references/components/default-header.md": "375e55f4b12fa2be", "packages/core/skills/app-shell-patterns/references/components/default-sidebar.md": "3422388a0cc78571", diff --git a/docs-src/components/data-table.docs.outline.md b/docs-src/components/data-table.docs.outline.md index 9c10ea07d..f23714c58 100644 --- a/docs-src/components/data-table.docs.outline.md +++ b/docs-src/components/data-table.docs.outline.md @@ -699,18 +699,27 @@ The `filter` property on a column accepts the same base shape as `FilterConfig`, ### Filter Types and Operators -| Type | Input editor | Supported operators | -| ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` | -| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | -| `datetime` | Datetime-local | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | -| `date` | **Calendar / DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** | -| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | -| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` | -| `boolean` | Toggle | `eq`, `ne` | -| `uuid` | Text | `eq`, `ne`, `in`, `nin` | - -When the `between` operator is selected on a `number`, `datetime`, `date`, or `time` column, the value editor renders a range input with **min**/**max** (or **From**/**To** for dates) bounds. +| Type | Input editor | Supported operators | +| ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` | +| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `decimal` | Decimal text (string value) | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `datetime` | Datetime-local | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `date` | **Calendar / DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** | +| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` | +| `boolean` | Toggle | `eq`, `ne` | +| `uuid` | Text | `eq`, `ne`, `in`, `nin` | + +When the `between` operator is selected on a `number`, `decimal`, `datetime`, `date`, or `time` column, the value editor renders a range input with **min**/**max** (or **From**/**To** for dates) bounds. + +### Decimal Filters + +TailorDB `db.decimal()` fields are generated as metadata `type: "decimal"`. They support numeric sort and filter operators, but values remain **strings**, matching the GraphQL `Decimal` scalar without JavaScript number rounding. Scalar filters, `in`/`nin` arrays, and `between` bounds all use strings in `variables.query`. + +The editor accepts signed decimal and scientific notation (for example, `"9007199254740993.123456789"` or `"1.23e-10"`) and checks range ordering without rounding. Sorting is delegated to the backend. As with `number`, `in`/`nin` are available programmatically, not in the built-in editor. + +Regenerate metadata with the updated SDK plugin to replace older decimal fields marked as `"number"`. Decimal metadata does not select a numeric cell renderer; the default text rendering preserves the original value. ### Date Filters diff --git a/docs/components/data-table.md b/docs/components/data-table.md index 6d5b14f72..980a9f1d7 100644 --- a/docs/components/data-table.md +++ b/docs/components/data-table.md @@ -693,18 +693,27 @@ The `filter` property on a column accepts the same base shape as `FilterConfig`, ### Filter Types and Operators -| Type | Input editor | Supported operators | -| ---------- | ------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` | -| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | -| `datetime` | Datetime-local | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | -| `date` | **Calendar / DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** | -| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | -| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` | -| `boolean` | Toggle | `eq`, `ne` | -| `uuid` | Text | `eq`, `ne`, `in`, `nin` | - -When the `between` operator is selected on a `number`, `datetime`, `date`, or `time` column, the value editor renders a range input with **min**/**max** (or **From**/**To** for dates) bounds. +| Type | Input editor | Supported operators | +| ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` | +| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `decimal` | Decimal text (string value) | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `datetime` | Datetime-local | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `date` | **Calendar / DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** | +| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` | +| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` | +| `boolean` | Toggle | `eq`, `ne` | +| `uuid` | Text | `eq`, `ne`, `in`, `nin` | + +When the `between` operator is selected on a `number`, `decimal`, `datetime`, `date`, or `time` column, the value editor renders a range input with **min**/**max** (or **From**/**To** for dates) bounds. + +### Decimal Filters + +TailorDB `db.decimal()` fields are generated as metadata `type: "decimal"`. They support numeric sort and filter operators, but values remain **strings**, matching the GraphQL `Decimal` scalar without JavaScript number rounding. Scalar filters, `in`/`nin` arrays, and `between` bounds all use strings in `variables.query`. + +The editor accepts signed decimal and scientific notation (for example, `"9007199254740993.123456789"` or `"1.23e-10"`) and checks range ordering without rounding. Sorting is delegated to the backend. As with `number`, `in`/`nin` are available programmatically, not in the built-in editor. + +Regenerate metadata with the updated SDK plugin to replace older decimal fields marked as `"number"`. Decimal metadata does not select a numeric cell renderer; the default text rendering preserves the original value. ### Date Filters diff --git a/packages/core/src/components/data-table/data-table.test.tsx b/packages/core/src/components/data-table/data-table.test.tsx index 1444d3d50..a6bbb2700 100644 --- a/packages/core/src/components/data-table/data-table.test.tsx +++ b/packages/core/src/components/data-table/data-table.test.tsx @@ -481,6 +481,31 @@ describe("DataTable", () => { expect(control.addFilter).toHaveBeenCalledWith("name", "contains", "Alice"); }); + it("preserves decimal strings when filtering by a cell value", async () => { + const user = userEvent.setup(); + const control = makeControl(); + const amount = "9007199254740993.123456789"; + function Harness() { + const table = useDataTable<{ amount: string }>({ + columns: [ + { id: "amount", label: "Amount", filter: { type: "decimal", field: "amount" } }, + ], + data: { rows: [{ amount }] }, + control, + }); + return ( + + + + ); + } + const { container } = render(, { wrapper }); + openCellContextMenu(container, amount); + hoverMenuItem("Add filter"); + await user.click(await screen.findByRole("menuitem", { name: "is" })); + expect(control.addFilter).toHaveBeenCalledWith("amount", "eq", amount); + }); + it("normalizes local date filters from Date values", async () => { const user = userEvent.setup(); const control = makeControl(); diff --git a/packages/core/src/components/data-table/data-table.tsx b/packages/core/src/components/data-table/data-table.tsx index 97271cde7..9fa202a3e 100644 --- a/packages/core/src/components/data-table/data-table.tsx +++ b/packages/core/src/components/data-table/data-table.tsx @@ -34,7 +34,11 @@ import type { import { DataTableContext, type DataTableContextValue } from "./data-table-context"; import { useDataTableT } from "./i18n"; import { getCellValue, renderTypedCell } from "./cell-renderers"; -import { isTemporalFilterType, normalizeTemporalFilterValue } from "./filter-value-utils"; +import { + isDecimalFilterValueValid, + isTemporalFilterType, + normalizeTemporalFilterValue, +} from "./filter-value-utils"; import { useCellContextMenu, type CellContextMenuState } from "./use-cell-context-menu"; import { DataTableToolbar, @@ -170,6 +174,10 @@ function toFilterValue(value: unknown, config: DataTableFilterConfig): unknown { const parsed = typeof value === "number" ? value : Number(value); return Number.isNaN(parsed) ? undefined : parsed; } + case "decimal": { + const decimal = String(value).trim(); + return isDecimalFilterValueValid(decimal) ? decimal : undefined; + } case "boolean": { if (typeof value === "boolean") return value; if (value === "true" || value === "false") return value === "true"; diff --git a/packages/core/src/components/data-table/field-helpers.test.ts b/packages/core/src/components/data-table/field-helpers.test.ts index 1201da66c..781796428 100644 --- a/packages/core/src/components/data-table/field-helpers.test.ts +++ b/packages/core/src/components/data-table/field-helpers.test.ts @@ -103,6 +103,29 @@ describe("inferColumns()", () => { }); }); +describe("decimal metadata", () => { + it("infers numeric sort and string-backed numeric filtering", () => { + const metadata = { + name: "order", + pluralForm: "orders", + fields: [{ name: "amount", type: "decimal", required: false }], + } as const; + const infer = createColumnHelper<{ amount: string }>().inferColumns(metadata); + expect(infer("amount")).toMatchObject({ + id: "amount", + sort: { field: "amount", type: "number" }, + filter: { field: "amount", type: "decimal" }, + }); + expect(infer("amount", { filter: { operators: ["gte", "between"] } }).filter).toEqual({ + field: "amount", + type: "decimal", + operators: ["gte", "between"], + }); + // @ts-expect-error decimal fields do not support string-only operators + infer("amount", { filter: { operators: ["contains"] } }); + }); +}); + describe("fieldTypeToSortConfig", () => { it("maps string to string sort", () => { expect(fieldTypeToSortConfig("name", "string")).toEqual({ diff --git a/packages/core/src/components/data-table/filter-value-utils.test.ts b/packages/core/src/components/data-table/filter-value-utils.test.ts index de4145d0b..9eee1d597 100644 --- a/packages/core/src/components/data-table/filter-value-utils.test.ts +++ b/packages/core/src/components/data-table/filter-value-utils.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { + isDecimalFilterValueValid, + isDecimalRangeOrdered, isTemporalFilterType, isTemporalFilterValueValid, normalizeTemporalFilterValue, @@ -28,6 +30,51 @@ function mockDate(local: { return value; } +describe("decimal filters", () => { + it.each(["0", "-0", "+001.20", ".5", "1.", "-9007199254740993.123456789", "1.23e-400"])( + "accepts decimal %s without converting it to Number", + (value) => expect(isDecimalFilterValueValid(value)).toBe(true), + ); + + it.each(["", " ", "NaN", "Infinity", "0x10", "1,23", "1e", "1.2.3", "--1"])( + "rejects invalid decimal %s", + (value) => expect(isDecimalFilterValueValid(value)).toBe(false), + ); + + it.each([ + ["9007199254740992", "9007199254740993"], + ["0.12345678901234567890", "0.12345678901234567891"], + ["-9007199254740993.1", "-9007199254740993.0"], + ["-0.001", "0"], + ["0", "0.001"], + ["-0", "+0"], + ["-1.2", "-1.20"], + ["001.20", "1.2"], + [".5", "0.5"], + ["1.", "1"], + ["123e-2", "1.2300"], + ["1e-400", "2e-400"], + ["9e99999999999999999999", "1e100000000000000000000"], + ])("compares %s ≤ %s exactly", (min, max) => { + expect(isDecimalRangeOrdered(min, max)).toBe(true); + if (min === "9007199254740992" || min.startsWith("0.123") || min === "1e-400") { + expect(isDecimalRangeOrdered(max, min)).toBe(false); + } + }); + + it.each([ + ["10", "2"], + ["-2", "-10"], + ["0", "-0.001"], + ["0.001", "-0"], + ["1e400", "9e399"], + ["invalid", "1"], + ["1", "invalid"], + ])("rejects unordered or invalid range %s … %s", (min, max) => { + expect(isDecimalRangeOrdered(min, max)).toBe(false); + }); +}); + describe("filter-value-utils", () => { it("detects temporal filter types", () => { expect(isTemporalFilterType("date")).toBe(true); diff --git a/packages/core/src/components/data-table/filter-value-utils.ts b/packages/core/src/components/data-table/filter-value-utils.ts index 199d45384..5e148b1d2 100644 --- a/packages/core/src/components/data-table/filter-value-utils.ts +++ b/packages/core/src/components/data-table/filter-value-utils.ts @@ -2,6 +2,42 @@ import type { FilterConfig } from "@/types/collection"; export type TemporalFilterType = Extract; +const DECIMAL_RE = /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:[eE]([+-]?\d+))?$/; + +export function isDecimalFilterValueValid(value: string): boolean { + return DECIMAL_RE.test(value.trim()); +} + +function decimalParts(value: string) { + const match = DECIMAL_RE.exec(value.trim()); + if (!match) return undefined; + const fraction = match[3] ?? match[4] ?? ""; + const digits = `${match[2] ?? ""}${fraction}`.replace(/^0+/, ""); + const sign = match[1] === "-" ? -1 : 1; + return { + digits, + sign: digits === "" ? 0 : sign, + magnitude: BigInt(digits.length - fraction.length) + BigInt(match[5] ?? "0"), + }; +} + +export function isDecimalRangeOrdered(min: string, max: string): boolean { + const lower = decimalParts(min); + const upper = decimalParts(max); + if (!lower || !upper) return false; + if (lower.sign !== upper.sign) return lower.sign < upper.sign; + if (lower.sign === 0) return true; + + // Compare magnitude, then significant digits, without rounding or expanding exponents. + if (lower.magnitude !== upper.magnitude) { + return lower.sign === 1 ? lower.magnitude < upper.magnitude : lower.magnitude > upper.magnitude; + } + const width = Math.max(lower.digits.length, upper.digits.length); + const left = lower.digits.padEnd(width, "0"); + const right = upper.digits.padEnd(width, "0"); + return lower.sign === 1 ? left <= right : left >= right; +} + const LOCAL_DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$/; const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; const TIME_RE = /^(?:[01]\d|2[0-3]):[0-5]\d$/; diff --git a/packages/core/src/components/data-table/toolbar.test.tsx b/packages/core/src/components/data-table/toolbar.test.tsx index b2d03208e..5b6726624 100644 --- a/packages/core/src/components/data-table/toolbar.test.tsx +++ b/packages/core/src/components/data-table/toolbar.test.tsx @@ -149,6 +149,12 @@ const numberColumn: Column = { filter: { type: "number", field: "count" }, }; +const decimalColumn: Column = { + id: "amount", + label: "Amount", + filter: { type: "decimal", field: "amount" }, +}; + const dateColumn: Column = { id: "createdAt", label: "Created At", @@ -842,6 +848,138 @@ describe("NumericFilterEditor", () => { }); }); +// --------------------------------------------------------------------------- +// Decimal filters — numeric operators with precision-preserving string values +// --------------------------------------------------------------------------- + +describe("Decimal filters", () => { + const preciseValue = "9007199254740993.123456789"; + const preciseMax = "9007199254740993.123456790"; + + it.each(["panel", "chip"] as const)( + "%s submits scalar decimals as exact strings", + async (surface) => { + const user = userEvent.setup(); + const control = makeControl({ + filters: surface === "chip" ? [{ field: "amount", operator: "eq", value: "1" }] : [], + }); + render(, { wrapper }); + + if (surface === "panel") await user.click(screen.getByRole("button", { name: "Add filter" })); + else await openValueEditor(user); + + const input = await screen.findByPlaceholderText("Enter amount"); + expect(input.getAttribute("type")).toBe("text"); + expect(input.getAttribute("inputmode")).toBe("decimal"); + await user.clear(input); + await user.type(input, preciseValue); + await user.click(screen.getByRole("button", { name: "Apply" })); + + if (surface === "panel") { + expect(control.addFilter).toHaveBeenCalledWith("amount", "eq", preciseValue, undefined); + } else { + expect(control.addFilter).toHaveBeenCalledWith("amount", "eq", preciseValue); + } + }, + ); + + it.each(["panel", "chip"] as const)( + "%s submits precise ranges and rejects reversed bounds", + async (surface) => { + const user = userEvent.setup(); + const control = makeControl({ + filters: + surface === "chip" + ? [ + { + field: "amount", + operator: "between", + value: { min: preciseValue, max: preciseMax }, + }, + ] + : [], + }); + render(, { wrapper }); + + if (surface === "panel") { + await user.click(screen.getByRole("button", { name: "Add filter" })); + await user.click(await screen.findByRole("button", { name: "is between" })); + } else { + expect(screen.getByText(`${preciseValue} - ${preciseMax}`)).toBeDefined(); + await openValueEditor(user); + } + + const min = await screen.findByRole("textbox", { name: "Min" }); + const max = screen.getByRole("textbox", { name: "Max" }); + fireEvent.change(min, { target: { value: "9007199254740993" } }); + fireEvent.change(max, { target: { value: "9007199254740992" } }); + expect((screen.getByRole("button", { name: "Apply" }) as HTMLButtonElement).disabled).toBe( + true, + ); + expect(screen.getByText(/must be greater than or equal to/i)).toBeDefined(); + fireEvent.keyDown(max, { key: "Enter" }); + expect(control.addFilter).not.toHaveBeenCalled(); + + fireEvent.change(min, { target: { value: preciseValue } }); + fireEvent.change(max, { target: { value: preciseMax } }); + await user.click(screen.getByRole("button", { name: "Apply" })); + expect(control.addFilter).toHaveBeenCalledWith("amount", "between", { + min: preciseValue, + max: preciseMax, + }); + }, + ); + + it("chip editing rejects invalid decimals without clearing the active filter", async () => { + const user = userEvent.setup(); + const control = makeControl({ + filters: [{ field: "amount", operator: "eq", value: preciseValue }], + }); + render(, { wrapper }); + await openValueEditor(user); + + const input = await screen.findByRole("textbox"); + fireEvent.change(input, { target: { value: "not-a-decimal" } }); + expect((screen.getByRole("button", { name: "Apply" }) as HTMLButtonElement).disabled).toBe( + true, + ); + fireEvent.keyDown(input, { key: "Enter" }); + expect(control.addFilter).not.toHaveBeenCalled(); + expect(control.removeFilter).not.toHaveBeenCalled(); + + fireEvent.change(input, { target: { value: "-1.23456789123456789e-100" } }); + fireEvent.keyDown(input, { key: "Enter" }); + expect(control.addFilter).toHaveBeenCalledWith("amount", "eq", "-1.23456789123456789e-100"); + }); + + it("operator transitions preserve decimal strings in both directions", async () => { + const user = userEvent.setup(); + const control = makeControl({ + filters: [{ field: "amount", operator: "eq", value: preciseValue }], + }); + const { rerender } = render(, { + wrapper, + }); + + await user.click(screen.getByRole("button", { name: "is" })); + await user.click(await screen.findByRole("button", { name: "is between" })); + expect(control.addFilter).toHaveBeenCalledWith("amount", "between", { + min: preciseValue, + max: preciseValue, + }); + + const rangeControl = makeControl({ + filters: [ + { field: "amount", operator: "between", value: { min: preciseValue, max: preciseMax } }, + ], + }); + rerender(); + await user.click(screen.getByRole("button", { name: "is between" })); + await user.click(await screen.findByRole("button", { name: "is" })); + expect(rangeControl.addFilter).toHaveBeenCalledWith("amount", "eq", preciseValue); + }); +}); + // --------------------------------------------------------------------------- // DateFilterEditor — Apply button // --------------------------------------------------------------------------- diff --git a/packages/core/src/components/data-table/toolbar.tsx b/packages/core/src/components/data-table/toolbar.tsx index d83455e98..2acecfac5 100644 --- a/packages/core/src/components/data-table/toolbar.tsx +++ b/packages/core/src/components/data-table/toolbar.tsx @@ -14,7 +14,12 @@ import { parseDate, DateFormatter } from "@internationalized/date"; import { useResolvedLocale } from "@/contexts/appshell-context"; import { DataTableColumnSettings } from "./column-settings"; import { useDataTableContext } from "./data-table-context"; -import { isTemporalFilterType, isTemporalFilterValueValid } from "./filter-value-utils"; +import { + isDecimalFilterValueValid, + isDecimalRangeOrdered, + isTemporalFilterType, + isTemporalFilterValueValid, +} from "./filter-value-utils"; import { useDataTableT } from "./i18n"; import type { CollectionControl, @@ -72,6 +77,7 @@ const DEFAULT_OPERATOR: Record = { boolean: "eq", string: "contains", number: "eq", + decimal: "eq", datetime: "eq", date: "eq", time: "eq", @@ -133,6 +139,7 @@ function getDefaultFilterOperators(type: FilterConfig["type"]): FilterOperator[] case "date": return [...DATE_OPERATORS]; case "number": + case "decimal": case "datetime": case "time": return [...NUMERIC_TEMPORAL_OPERATORS]; @@ -152,6 +159,7 @@ function isUiOperatorAllowedForType(type: FilterConfig["type"], operator: Filter case "date": return (DATE_OPERATORS as readonly string[]).includes(operator); case "number": + case "decimal": case "datetime": case "time": return (NUMERIC_TEMPORAL_OPERATORS as readonly string[]).includes(operator); @@ -865,7 +873,9 @@ function PanelValueEditor({ ); } else if (isBetween) { // Non-date range: two simple From/To (or Min/Max) text boxes. - const numeric = type === "number"; + const numeric = type === "number" || type === "decimal"; + const numericInputProps = + type === "decimal" ? { type: "text", inputMode: "decimal" as const } : { type: "number" }; const labels: [string, string] = numeric ? [t("filterBetweenMin"), t("filterBetweenMax")] : [t("filterBetweenFrom"), t("filterBetweenTo")]; @@ -878,7 +888,7 @@ function PanelValueEditor({ onChangeMax={setMax} onSubmit={apply} inputProps={ - numeric ? { type: "number" } : getTemporalInputProps(type as "datetime" | "time") + numeric ? numericInputProps : getTemporalInputProps(type as "datetime" | "time") } error={betweenOrderError(type, min, max, labels[0], labels[1], t)} /> @@ -918,6 +928,7 @@ function PanelValueEditor({
setText(e.target.value)} @@ -1048,6 +1059,8 @@ function betweenOrderError( let bothValid = true; if (type === "number") { bothValid = !Number.isNaN(Number(min)) && !Number.isNaN(Number(max)); + } else if (type === "decimal") { + bothValid = isDecimalFilterValueValid(min) && isDecimalFilterValueValid(max); } else if (isTemporalFilterType(type)) { bothValid = isTemporalFilterValueValid(type, min) && isTemporalFilterValueValid(type, max); } @@ -1424,6 +1437,7 @@ function FilterPopoverContent({ ); case "number": + case "decimal": return ( ; + config: Extract; filter: Filter; control: CollectionControl; onClose: () => void; @@ -1772,12 +1786,11 @@ function NumericFilterEditor({ if (minEmpty && maxEmpty) return true; // will removeFilter if (minEmpty || maxEmpty) return false; // both required return ( - !Number.isNaN(Number(localValue)) && - !Number.isNaN(Number(localValueMax)) && - isRangeOrdered("number", localValue, localValueMax) + isAddFilterDraftValueValid(config.type, "between", [localValue, localValueMax]) && + isRangeOrdered(config.type, localValue, localValueMax) ); } - return localValue.trim() === "" || !Number.isNaN(Number(localValue)); + return localValue.trim() === "" || isAddFilterDraftValueValid(config.type, localOp, localValue); })(); const handleCommit = useCallback(() => { @@ -1787,16 +1800,30 @@ function NumericFilterEditor({ if (minEmpty && maxEmpty) { control.removeFilter(config.field); } else if (!minEmpty && !maxEmpty) { - const min = Number(localValue); - const max = Number(localValueMax); - if (!Number.isNaN(min) && !Number.isNaN(max) && min <= max) { - control.addFilter(config.field, localOp, { min, max }); + const draft: AddFilterDraftValue = [localValue, localValueMax]; + if ( + isAddFilterDraftValueValid(config.type, localOp, draft) && + isRangeOrdered(config.type, localValue, localValueMax) + ) { + control.addFilter( + config.field, + localOp, + toAddFilterSubmittedValue(config.type, localOp, draft), + ); } else { return; } } else { return; } + } else if (config.type === "decimal") { + if (localValue.trim() === "") { + control.removeFilter(config.field); + } else if (isDecimalFilterValueValid(localValue)) { + control.addFilter(config.field, localOp, localValue.trim()); + } else { + return; + } } else { const num = Number(localValue); if (localValue.trim() === "" || Number.isNaN(num)) { @@ -1806,11 +1833,11 @@ function NumericFilterEditor({ } } onClose(); - }, [localValue, localValueMax, localOp, control, config.field, onClose]); + }, [localValue, localValueMax, localOp, control, config.field, config.type, onClose]); return (
{!hideOperator && ( @@ -1831,9 +1858,11 @@ function NumericFilterEditor({ onChangeMin={setLocalValue} onChangeMax={setLocalValueMax} onSubmit={handleCommit} - inputProps={{ type: "number" }} + inputProps={ + config.type === "decimal" ? { type: "text", inputMode: "decimal" } : { type: "number" } + } error={betweenOrderError( - "number", + config.type, localValue, localValueMax, t("filterBetweenMin"), @@ -1843,7 +1872,8 @@ function NumericFilterEditor({ /> ) : ( setLocalValue(e.target.value)} @@ -2064,6 +2094,9 @@ function isAddFilterDraftValueValid( if (type === "number") { return !Number.isNaN(Number(min)) && !Number.isNaN(Number(max)); } + if (type === "decimal") { + return isDecimalFilterValueValid(min) && isDecimalFilterValueValid(max); + } if (isTemporalFilterType(type)) { return isTemporalFilterValueValid(type, min) && isTemporalFilterValueValid(type, max); } @@ -2075,6 +2108,7 @@ function isAddFilterDraftValueValid( if (value.trim() === "") return false; return !Number.isNaN(Number(value)); } + if (type === "decimal") return isDecimalFilterValueValid(value); if (isTemporalFilterType(type)) { return isTemporalFilterValueValid(type, value); } @@ -2110,7 +2144,7 @@ function toAddFilterSubmittedValue( if (trimmedMin === "" || trimmedMax === "") return undefined; - // temporal types + // Decimal and temporal types keep their string representation. return { min: trimmedMin, max: trimmedMax }; } @@ -2129,6 +2163,7 @@ function toAddFilterSubmittedValue( */ function isRangeOrdered(type: FilterConfig["type"], min: string, max: string): boolean { if (type === "number") return Number(min) <= Number(max); + if (type === "decimal") return isDecimalRangeOrdered(min, max); if (isTemporalFilterType(type)) return min <= max; return true; } @@ -2269,7 +2304,7 @@ function formatFilterValue( return filter.value === true ? t("filterBooleanTrue") : t("filterBooleanFalse"); } - if (config.type === "number" && filter.operator === "between") { + if ((config.type === "number" || config.type === "decimal") && filter.operator === "between") { const range = filter.value as { min?: unknown; max?: unknown } | null; if (!range || typeof range !== "object") return ""; const min = range.min != null ? String(range.min) : ""; diff --git a/packages/core/src/components/data-table/types.ts b/packages/core/src/components/data-table/types.ts index ddcd71af4..aa3687ee5 100644 --- a/packages/core/src/components/data-table/types.ts +++ b/packages/core/src/components/data-table/types.ts @@ -127,6 +127,7 @@ type NonEmptyReadonlyArray = readonly [T, ...T[]]; type DataTableUiFilterOperatorByType = { string: DataTableStringFilterOperator; number: DataTableNumericTemporalFilterOperator; + decimal: DataTableNumericTemporalFilterOperator; datetime: DataTableNumericTemporalFilterOperator; date: DataTableDateFilterOperator; time: DataTableNumericTemporalFilterOperator; @@ -146,6 +147,9 @@ export type DataTableFilterConfig = | (Extract & { operators?: NonEmptyReadonlyArray; }) + | (Extract & { + operators?: NonEmptyReadonlyArray; + }) | (Extract & { operators?: NonEmptyReadonlyArray; }) diff --git a/packages/core/src/hooks/use-collection-variables.test-d.ts b/packages/core/src/hooks/use-collection-variables.test-d.ts index 1db183af1..89ad49054 100644 --- a/packages/core/src/hooks/use-collection-variables.test-d.ts +++ b/packages/core/src/hooks/use-collection-variables.test-d.ts @@ -32,6 +32,11 @@ type TestTable = { readonly type: "number"; readonly required: false; }, + { + readonly name: "decimalAmount"; + readonly type: "decimal"; + readonly required: false; + }, { readonly name: "status"; readonly type: "enum"; @@ -82,6 +87,24 @@ describe("BuildQueryVariables", () => { }>(); }); + it("decimal fields use numeric operators with GraphQL string values", () => { + type DecimalInput = NonNullable; + expectTypeOf().toEqualTypeOf<{ + eq?: string; + ne?: string; + gt?: string; + gte?: string; + lt?: string; + lte?: string; + between?: { min: string; max: string }; + in?: string[]; + nin?: string[]; + }>(); + expectTypeOf().toExtend< + { decimalAmount?: DecimalInput } | undefined + >(); + }); + it("enum fields produce literal union types from enumValues", () => { expectTypeOf().toExtend<{ status?: { @@ -140,6 +163,7 @@ describe("TableFieldName", () => { | "id" | "title" | "amount" + | "decimalAmount" | "status" | "isActive" | "createdAt" @@ -166,6 +190,10 @@ describe("TableOrderableFieldName", () => { expectTypeOf<"amount">().toExtend>(); }); + it("includes decimal fields", () => { + expectTypeOf<"decimalAmount">().toExtend>(); + }); + it("includes enum fields", () => { expectTypeOf<"status">().toExtend>(); }); diff --git a/packages/core/src/hooks/use-collection-variables.test.ts b/packages/core/src/hooks/use-collection-variables.test.ts index ad0e78002..cdc7574fd 100644 --- a/packages/core/src/hooks/use-collection-variables.test.ts +++ b/packages/core/src/hooks/use-collection-variables.test.ts @@ -1,5 +1,5 @@ import { renderHook, act } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, expectTypeOf, vi } from "vitest"; import type { TableMetadataMap } from "@/types/collection"; import { useCollectionVariables } from "./use-collection-variables"; @@ -644,10 +644,42 @@ describe("useCollectionVariables", () => { }, { name: "dueDate", type: "date", required: false }, { name: "count", type: "number", required: false }, + { name: "amount", type: "decimal", required: false }, ], }, } as const satisfies TableMetadataMap; + it.each(["gte", "in", "nin", "between"] as const)( + "builds string-based decimal %s variables compatible with GraphQL inputs", + (operator) => { + const min = "9007199254740993.123456789"; + const max = "9007199254740994.123456789"; + const value = { between: { min, max }, gte: min, in: [min, max], nin: [min, max] }[ + operator + ]; + const { result } = renderHook(() => + useCollectionVariables({ tableMetadata: testMetadata.task }), + ); + act(() => { + result.current.control.addFilter("amount", operator, value); + result.current.control.setSort("amount", "Asc"); + }); + expect(result.current.variables.query).toEqual({ amount: { [operator]: value } }); + expect(result.current.variables.order).toEqual([{ field: "amount", direction: "Asc" }]); + expectTypeOf(result.current.variables.query).toExtend< + | { + amount?: { + gte?: string; + in?: string[]; + nin?: string[]; + between?: { min: string; max: string }; + }; + } + | undefined + >(); + }, + ); + it("works with tableMetadata", () => { const { result } = renderHook(() => useCollectionVariables({ diff --git a/packages/core/src/lib/collection-url-state.test.ts b/packages/core/src/lib/collection-url-state.test.ts index 479ca86b4..8bd6d36e5 100644 --- a/packages/core/src/lib/collection-url-state.test.ts +++ b/packages/core/src/lib/collection-url-state.test.ts @@ -22,6 +22,7 @@ const tableMetadata = { { name: "createdAt", type: "datetime", required: true }, { name: "title", type: "string", required: true }, { name: "price", type: "number", required: true }, + { name: "decimalPrice", type: "decimal", required: true }, { name: "archived", type: "boolean", required: false }, ], }, @@ -105,6 +106,22 @@ describe("parseCollectionSearchParams", () => { ]); }); + it.each(["eq", "in", "nin", "between"] as const)( + "round-trips decimal %s values and sorting without losing precision", + (operator) => { + const min = "9007199254740993.123456789"; + const max = "9007199254740994.123456789"; + const value = { between: { min, max }, eq: min, in: [min, max], nin: [min, max] }[operator]; + const state = { + filters: [{ field: "decimalPrice", operator, value }], + sortStates: [{ field: "decimalPrice", direction: "Asc" as const }], + pageSize: 20, + }; + const written = writeCollectionSearchParams(new URLSearchParams(), state); + expect(parseCollectionSearchParams(tableMetadata.task, written)).toEqual(state); + }, + ); + it("leaves numeric-looking values as strings when no metadata is provided", () => { const result = parseCollectionSearchParams(new URLSearchParams("f.price:gt=130")); diff --git a/packages/core/src/types/collection.ts b/packages/core/src/types/collection.ts index 3bfc3a0ce..1a10b4f27 100644 --- a/packages/core/src/types/collection.ts +++ b/packages/core/src/types/collection.ts @@ -8,6 +8,7 @@ export type FieldType = | "string" | "number" + | "decimal" | "boolean" | "uuid" | "datetime" @@ -86,6 +87,7 @@ export interface SelectOption { export type FilterConfig = | { field: string; type: "string" } | { field: string; type: "number" } + | { field: string; type: "decimal" } | { field: string; type: "datetime" } | { field: string; type: "date" } | { field: string; type: "time" } @@ -115,6 +117,7 @@ export const OPERATORS_BY_FILTER_TYPE = { "regex", ], number: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"], + decimal: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"], datetime: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"], date: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"], time: ["eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "nin"], @@ -156,6 +159,7 @@ export type OperatorForField = [Extract = TTable["fields"][numb : never : never; -type OrderableFieldType = "string" | "number" | "boolean" | "datetime" | "date" | "time" | "enum"; +type OrderableFieldType = + | "string" + | "number" + | "decimal" + | "boolean" + | "datetime" + | "date" + | "time" + | "enum"; /** * Extract only orderable field names from a single table metadata object. @@ -518,6 +531,7 @@ export function fieldTypeToSortConfig(field: string, type: FieldType): SortConfi case "string": return { field, type: "string" }; case "number": + case "decimal": return { field, type: "number" }; case "boolean": return { field, type: "boolean" }; @@ -546,6 +560,8 @@ export function fieldTypeToFilterConfig( return { field, type: "string" }; case "number": return { field, type: "number" }; + case "decimal": + return { field, type: "decimal" }; case "boolean": return { field, type: "boolean" }; case "uuid": diff --git a/packages/sdk-plugin/src/plugin.test.ts b/packages/sdk-plugin/src/plugin.test.ts index b3e89f71f..8ade3b6a9 100644 --- a/packages/sdk-plugin/src/plugin.test.ts +++ b/packages/sdk-plugin/src/plugin.test.ts @@ -187,6 +187,10 @@ describe("appShellPlugin", () => { name: "decimalField", config: { type: "decimal", required: false }, }, + decimalArrayField: { + name: "decimalArrayField", + config: { type: "decimal", required: false, array: true }, + }, dateField: { name: "dateField", config: { type: "date", required: false }, @@ -217,7 +221,13 @@ describe("appShellPlugin", () => { expect(fields.find((f: { name: string }) => f.name === "intField").type).toBe("number"); expect(fields.find((f: { name: string }) => f.name === "floatField").type).toBe("number"); - expect(fields.find((f: { name: string }) => f.name === "decimalField").type).toBe("number"); + expect(fields.find((f: { name: string }) => f.name === "decimalField").type).toBe("decimal"); + expect(fields.find((f: { name: string }) => f.name === "decimalArrayField")).toEqual({ + name: "decimalArrayField", + type: "array", + arrayItemType: "decimal", + required: false, + }); expect(fields.find((f: { name: string }) => f.name === "dateField").type).toBe("date"); expect(fields.find((f: { name: string }) => f.name === "dtField").type).toBe("datetime"); diff --git a/packages/sdk-plugin/src/plugin.ts b/packages/sdk-plugin/src/plugin.ts index 0f64a3079..c8283bb73 100644 --- a/packages/sdk-plugin/src/plugin.ts +++ b/packages/sdk-plugin/src/plugin.ts @@ -25,7 +25,7 @@ function mapFieldType( string: "string", integer: "number", float: "number", - decimal: "number", + decimal: "decimal", boolean: "boolean", uuid: "uuid", datetime: "datetime", From 41502f5b1edbb77f01c12cbf6ef777b59fed757e Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Tue, 13 Oct 2026 12:38:08 +0900 Subject: [PATCH 2/3] fix(sdk-plugin): declare decimal metadata compatibility requirements --- .changeset/tidy-decimals-flow.md | 6 ++++-- packages/sdk-plugin/README.md | 12 ++++++++++++ packages/sdk-plugin/package.json | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.changeset/tidy-decimals-flow.md b/.changeset/tidy-decimals-flow.md index 090e21e8e..64a37ac03 100644 --- a/.changeset/tidy-decimals-flow.md +++ b/.changeset/tidy-decimals-flow.md @@ -1,7 +1,9 @@ --- "@tailor-platform/app-shell": patch -"@tailor-platform/sdk-plugin-app-shell": patch +"@tailor-platform/sdk-plugin-app-shell": minor --- Fix Decimal metadata and query types to use strings with numeric sort/filter operators, matching the GraphQL Decimal scalar. -Preserve decimal precision when adding, editing, or restoring DataTable filters. Regenerate SDK metadata to update decimal fields previously marked as `number`. +Preserve decimal precision when adding, editing, or restoring DataTable filters. + +The SDK plugin now requires `@tailor-platform/app-shell` `^1.16.1` as a peer dependency. Upgrade AppShell before upgrading the plugin, then run `tailor generate` to regenerate metadata: decimal fields and decimal array items now use `decimal` instead of `number`. AppShell 1.16.0 and earlier cannot consume the new decimal metadata; update hand-written decimal filter values from numbers to strings. diff --git a/packages/sdk-plugin/README.md b/packages/sdk-plugin/README.md index bfeac937c..852570a0e 100644 --- a/packages/sdk-plugin/README.md +++ b/packages/sdk-plugin/README.md @@ -14,10 +14,22 @@ The metadata is generated at SDK generate time from your TailorDB table definiti ## Installation +Requires `@tailor-platform/app-shell` `^1.16.1` as a peer dependency. + ```sh pnpm add -D @tailor-platform/sdk-plugin-app-shell ``` +The plugin and the AppShell consuming its generated metadata must be managed in the same project's dependency graph. Compatibility is not guaranteed when generated metadata is copied or distributed to another project. + +### Upgrading from 0.2.x + +Version 0.3.0 generates `type: "decimal"` metadata instead of `type: "number"` for decimal fields, including decimal array items. AppShell 1.16.0 and earlier do not support this metadata. + +1. Upgrade `@tailor-platform/app-shell` to a version satisfying `^1.16.1` before upgrading the plugin. +2. Upgrade `@tailor-platform/sdk-plugin-app-shell` to 0.3.0 or later and run `tailor generate` to regenerate metadata. +3. Update hand-written decimal filter values from numbers to strings to preserve precision. + ## Setup Supports `@tailor-platform/sdk` `^1 || ^2`. The examples below use the v2 CLI and TailorDB table API. diff --git a/packages/sdk-plugin/package.json b/packages/sdk-plugin/package.json index 00fd57c3f..d82564a49 100644 --- a/packages/sdk-plugin/package.json +++ b/packages/sdk-plugin/package.json @@ -49,6 +49,7 @@ "vitest": "catalog:" }, "peerDependencies": { + "@tailor-platform/app-shell": "^1.16.1", "@tailor-platform/sdk": "^1.0.0 || ^2.0.0" } } From a57c307f559753327b835851e31fcf2599d25eb1 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Tue, 13 Oct 2026 17:09:50 +0900 Subject: [PATCH 3/3] Revert "fix(sdk-plugin): declare decimal metadata compatibility requirements" This reverts commit 41502f5b1edbb77f01c12cbf6ef777b59fed757e. --- .changeset/tidy-decimals-flow.md | 6 ++---- packages/sdk-plugin/README.md | 12 ------------ packages/sdk-plugin/package.json | 1 - 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.changeset/tidy-decimals-flow.md b/.changeset/tidy-decimals-flow.md index 64a37ac03..090e21e8e 100644 --- a/.changeset/tidy-decimals-flow.md +++ b/.changeset/tidy-decimals-flow.md @@ -1,9 +1,7 @@ --- "@tailor-platform/app-shell": patch -"@tailor-platform/sdk-plugin-app-shell": minor +"@tailor-platform/sdk-plugin-app-shell": patch --- Fix Decimal metadata and query types to use strings with numeric sort/filter operators, matching the GraphQL Decimal scalar. -Preserve decimal precision when adding, editing, or restoring DataTable filters. - -The SDK plugin now requires `@tailor-platform/app-shell` `^1.16.1` as a peer dependency. Upgrade AppShell before upgrading the plugin, then run `tailor generate` to regenerate metadata: decimal fields and decimal array items now use `decimal` instead of `number`. AppShell 1.16.0 and earlier cannot consume the new decimal metadata; update hand-written decimal filter values from numbers to strings. +Preserve decimal precision when adding, editing, or restoring DataTable filters. Regenerate SDK metadata to update decimal fields previously marked as `number`. diff --git a/packages/sdk-plugin/README.md b/packages/sdk-plugin/README.md index 852570a0e..bfeac937c 100644 --- a/packages/sdk-plugin/README.md +++ b/packages/sdk-plugin/README.md @@ -14,22 +14,10 @@ The metadata is generated at SDK generate time from your TailorDB table definiti ## Installation -Requires `@tailor-platform/app-shell` `^1.16.1` as a peer dependency. - ```sh pnpm add -D @tailor-platform/sdk-plugin-app-shell ``` -The plugin and the AppShell consuming its generated metadata must be managed in the same project's dependency graph. Compatibility is not guaranteed when generated metadata is copied or distributed to another project. - -### Upgrading from 0.2.x - -Version 0.3.0 generates `type: "decimal"` metadata instead of `type: "number"` for decimal fields, including decimal array items. AppShell 1.16.0 and earlier do not support this metadata. - -1. Upgrade `@tailor-platform/app-shell` to a version satisfying `^1.16.1` before upgrading the plugin. -2. Upgrade `@tailor-platform/sdk-plugin-app-shell` to 0.3.0 or later and run `tailor generate` to regenerate metadata. -3. Update hand-written decimal filter values from numbers to strings to preserve precision. - ## Setup Supports `@tailor-platform/sdk` `^1 || ^2`. The examples below use the v2 CLI and TailorDB table API. diff --git a/packages/sdk-plugin/package.json b/packages/sdk-plugin/package.json index d82564a49..00fd57c3f 100644 --- a/packages/sdk-plugin/package.json +++ b/packages/sdk-plugin/package.json @@ -49,7 +49,6 @@ "vitest": "catalog:" }, "peerDependencies": { - "@tailor-platform/app-shell": "^1.16.1", "@tailor-platform/sdk": "^1.0.0 || ^2.0.0" } }