+ );
+}
+
+Renderer.propTypes = RendererPropTypes;
diff --git a/viz-lib/src/visualizations/details/getOptions.test.ts b/viz-lib/src/visualizations/details/getOptions.test.ts
new file mode 100644
index 000000000..35f12e98f
--- /dev/null
+++ b/viz-lib/src/visualizations/details/getOptions.test.ts
@@ -0,0 +1,160 @@
+import getOptions from "./getOptions";
+
+describe("Visualizations -> Details -> getOptions", () => {
+ const sampleData = {
+ columns: [
+ { name: "id", type: "integer" },
+ { name: "name", type: "string" },
+ { name: "created_at", type: "datetime" },
+ { name: "is_active", type: "boolean" },
+ { name: "score", type: "float" },
+ ],
+ };
+
+ test("Returns default options when no options provided", () => {
+ const result = getOptions({}, sampleData);
+
+ expect(result.columns).toHaveLength(5);
+ expect(result.columns[0]).toEqual(
+ expect.objectContaining({
+ name: "id",
+ type: "integer",
+ displayAs: "number",
+ visible: true,
+ alignContent: "left",
+ title: "id",
+ description: "",
+ allowHTML: false,
+ highlightLinks: false,
+ })
+ );
+ });
+
+ test("Preserves existing column options", () => {
+ const existingOptions = {
+ columns: [
+ {
+ name: "id",
+ visible: false,
+ title: "User ID",
+ alignContent: "center",
+ },
+ ],
+ };
+
+ const result = getOptions(existingOptions, sampleData);
+
+ const idColumn = result.columns.find((col: any) => col.name === "id");
+ expect(idColumn).toEqual(
+ expect.objectContaining({
+ visible: false,
+ title: "User ID",
+ alignContent: "center",
+ })
+ );
+ });
+
+ test("Sets correct default display types", () => {
+ const result = getOptions({}, sampleData);
+
+ const columnsByName = result.columns.reduce((acc: any, col: any) => {
+ acc[col.name] = col;
+ return acc;
+ }, {} as any);
+
+ expect(columnsByName.id.displayAs).toBe("number");
+ expect(columnsByName.name.displayAs).toBe("string");
+ expect(columnsByName.created_at.displayAs).toBe("datetime");
+ expect(columnsByName.is_active.displayAs).toBe("boolean");
+ expect(columnsByName.score.displayAs).toBe("number");
+ });
+
+ test("Sets correct default alignments", () => {
+ const result = getOptions({}, sampleData);
+
+ const columnsByName = result.columns.reduce((acc: any, col: any) => {
+ acc[col.name] = col;
+ return acc;
+ }, {} as any);
+
+ expect(columnsByName.id.alignContent).toBe("left");
+ expect(columnsByName.name.alignContent).toBe("left");
+ expect(columnsByName.created_at.alignContent).toBe("left");
+ expect(columnsByName.is_active.alignContent).toBe("left");
+ expect(columnsByName.score.alignContent).toBe("left");
+ });
+
+ test("Handles column name type suffixes", () => {
+ const dataWithTypeSuffixes = {
+ columns: [
+ { name: "user::filter", type: "string" },
+ { name: "amount__multiFilter", type: "float" },
+ { name: "::date_field", type: "date" },
+ ],
+ };
+
+ const result = getOptions({}, dataWithTypeSuffixes);
+
+ expect(result.columns[0].title).toBe("user");
+ expect(result.columns[1].title).toBe("amount");
+ expect(result.columns[2].title).toBe("date_field");
+ });
+
+ test("Maintains column order from existing options", () => {
+ const existingOptions = {
+ columns: [
+ { name: "name", order: 0 },
+ { name: "id", order: 1 },
+ ],
+ };
+
+ const result = getOptions(existingOptions, sampleData);
+
+ expect(result.columns[0].name).toBe("name");
+ expect(result.columns[1].name).toBe("id");
+ });
+
+ test("Handles missing columns in existing options", () => {
+ const existingOptions = {
+ columns: [
+ { name: "id", visible: false },
+ { name: "nonexistent", visible: true },
+ ],
+ };
+
+ const result = getOptions(existingOptions, sampleData);
+
+ // Should include all data columns
+ expect(result.columns).toHaveLength(5);
+
+ // Should preserve settings for existing columns
+ const idColumn = result.columns.find((col: any) => col.name === "id");
+ expect(idColumn.visible).toBe(false);
+ });
+
+ test("Includes default format options", () => {
+ const result = getOptions({}, sampleData);
+
+ const column = result.columns[0];
+ expect(column).toEqual(
+ expect.objectContaining({
+ booleanValues: ["false", "true"],
+ imageUrlTemplate: "{{ @ }}",
+ imageTitleTemplate: "{{ @ }}",
+ imageWidth: "",
+ imageHeight: "",
+ linkUrlTemplate: "{{ @ }}",
+ linkTextTemplate: "{{ @ }}",
+ linkTitleTemplate: "{{ @ }}",
+ linkOpenInNewTab: true,
+ })
+ );
+ });
+
+ test("Handles empty data", () => {
+ const emptyData = { columns: [] };
+ const result = getOptions({}, emptyData);
+
+ expect(result.columns).toEqual([]);
+ });
+});
diff --git a/viz-lib/src/visualizations/details/getOptions.ts b/viz-lib/src/visualizations/details/getOptions.ts
new file mode 100644
index 000000000..26d351f77
--- /dev/null
+++ b/viz-lib/src/visualizations/details/getOptions.ts
@@ -0,0 +1,17 @@
+import _ from "lodash";
+import {
+ getDefaultFormatOptions,
+ getColumnsOptions,
+} from "@/visualizations/shared/columnUtils";
+
+const DEFAULT_OPTIONS = {};
+
+
+export default function getOptions(options: any, { columns }: any) {
+ options = { ...DEFAULT_OPTIONS, ...options };
+ options.columns = _.map(getColumnsOptions(columns, options.columns, { alignContent: "left" }), col => ({
+ ...getDefaultFormatOptions(col),
+ ...col,
+ }));
+ return options;
+}
diff --git a/viz-lib/src/visualizations/details/index.ts b/viz-lib/src/visualizations/details/index.ts
index aa321fd61..f81e0902e 100644
--- a/viz-lib/src/visualizations/details/index.ts
+++ b/viz-lib/src/visualizations/details/index.ts
@@ -1,15 +1,13 @@
-import DetailsRenderer from "./DetailsRenderer";
-
-const DEFAULT_OPTIONS = {};
+import getOptions from "./getOptions";
+import Renderer from "./Renderer";
+import Editor from "./Editor";
export default {
type: "DETAILS",
name: "Details View",
- getOptions: (options: any) => ({
- ...DEFAULT_OPTIONS,
- ...options,
- }),
- Renderer: DetailsRenderer,
+ getOptions,
+ Renderer,
+ Editor,
defaultColumns: 4,
defaultRows: 2,
};
diff --git a/viz-lib/src/visualizations/shared/columnUtils.ts b/viz-lib/src/visualizations/shared/columnUtils.ts
new file mode 100644
index 000000000..770dcc168
--- /dev/null
+++ b/viz-lib/src/visualizations/shared/columnUtils.ts
@@ -0,0 +1,126 @@
+import _ from "lodash";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
+
+const filterTypes = ["filter", "multi-filter", "multiFilter"];
+
+export function getColumnNameWithoutType(column: any) {
+ let typeSplit;
+ if (column.indexOf("::") !== -1) {
+ typeSplit = "::";
+ } else if (column.indexOf("__") !== -1) {
+ typeSplit = "__";
+ } else {
+ return column;
+ }
+
+ const parts = column.split(typeSplit);
+ if (parts[0] === "" && parts.length === 2) {
+ return parts[1];
+ }
+
+ if (!_.includes(filterTypes, parts[1])) {
+ return column;
+ }
+
+ return parts[0];
+}
+
+export function getColumnContentAlignment(type: any) {
+ return ["integer", "float", "boolean", "date", "datetime"].indexOf(type) >= 0 ? "right" : "left";
+}
+
+export function getDefaultColumnsOptions(columns: any, extraFields = {}) {
+ const displayAs = {
+ integer: "number",
+ float: "number",
+ boolean: "boolean",
+ date: "datetime",
+ datetime: "datetime",
+ };
+
+ const defaultFields = {
+ // `string` cell options
+ allowHTML: false,
+ highlightLinks: false,
+ };
+
+ return _.map(columns, (col, index) => ({
+ name: col.name,
+ type: col.type,
+ // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
+ displayAs: displayAs[col.type] || "string",
+ visible: true,
+ order: 100000 + index,
+ title: getColumnNameWithoutType(col.name),
+ alignContent: getColumnContentAlignment(col.type),
+ description: "",
+ ...defaultFields,
+ ...extraFields,
+ }));
+}
+
+export function getDefaultFormatOptions(column: any) {
+ const dateTimeFormat = {
+ date: visualizationsSettings.dateFormat || "DD/MM/YYYY",
+ datetime: visualizationsSettings.dateTimeFormat || "DD/MM/YYYY HH:mm",
+ };
+ const numberFormat = {
+ integer: visualizationsSettings.integerFormat || "0,0",
+ float: visualizationsSettings.floatFormat || "0,0.00",
+ };
+ return {
+ // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
+ dateTimeFormat: dateTimeFormat[column.type],
+ // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
+ numberFormat: numberFormat[column.type],
+ nullValue: visualizationsSettings.nullValue,
+ booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
+ // `image` cell options
+ imageUrlTemplate: "{{ @ }}",
+ imageTitleTemplate: "{{ @ }}",
+ imageWidth: "",
+ imageHeight: "",
+ // `link` cell options
+ linkUrlTemplate: "{{ @ }}",
+ linkTextTemplate: "{{ @ }}",
+ linkTitleTemplate: "{{ @ }}",
+ linkOpenInNewTab: true,
+ };
+}
+
+export function wereColumnsReordered(queryColumns: any, visualizationColumns: any) {
+ queryColumns = _.map(queryColumns, col => col.name);
+ visualizationColumns = _.map(visualizationColumns, col => col.name);
+
+ // Some columns may be removed - so skip them (but keep original order)
+ visualizationColumns = _.filter(visualizationColumns, col => _.includes(queryColumns, col));
+ // Pick query columns that were previously saved with viz (but keep order too)
+ queryColumns = _.filter(queryColumns, col => _.includes(visualizationColumns, col));
+
+ // Both array now have the same size as they both contains only common columns
+ // (in fact, it was an intersection, that kept order of items on both arrays).
+ // Now check for equality item-by-item; if common columns are in the same order -
+ // they were not reordered in editor
+ for (let i = 0; i < queryColumns.length; i += 1) {
+ if (visualizationColumns[i] !== queryColumns[i]) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function getColumnsOptions(columns: any, visualizationColumns: any, extraFields = {}) {
+ const options = getDefaultColumnsOptions(columns, extraFields);
+
+ if (wereColumnsReordered(columns, visualizationColumns)) {
+ visualizationColumns = _.fromPairs(
+ _.map(visualizationColumns, (col, index) => [col.name, _.extend({}, col, { order: index })])
+ );
+ } else {
+ visualizationColumns = _.fromPairs(_.map(visualizationColumns, col => [col.name, _.omit(col, "order")]));
+ }
+
+ _.each(options, col => _.extend(col, visualizationColumns[col.name]));
+
+ return _.sortBy(options, "order");
+}
diff --git a/viz-lib/src/visualizations/table/columns/__snapshots__/boolean.test.tsx.snap b/viz-lib/src/visualizations/shared/columns/__snapshots__/boolean.test.tsx.snap
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/__snapshots__/boolean.test.tsx.snap
rename to viz-lib/src/visualizations/shared/columns/__snapshots__/boolean.test.tsx.snap
diff --git a/viz-lib/src/visualizations/table/columns/__snapshots__/datetime.test.tsx.snap b/viz-lib/src/visualizations/shared/columns/__snapshots__/datetime.test.tsx.snap
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/__snapshots__/datetime.test.tsx.snap
rename to viz-lib/src/visualizations/shared/columns/__snapshots__/datetime.test.tsx.snap
diff --git a/viz-lib/src/visualizations/table/columns/__snapshots__/image.test.tsx.snap b/viz-lib/src/visualizations/shared/columns/__snapshots__/image.test.tsx.snap
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/__snapshots__/image.test.tsx.snap
rename to viz-lib/src/visualizations/shared/columns/__snapshots__/image.test.tsx.snap
diff --git a/viz-lib/src/visualizations/table/columns/__snapshots__/link.test.tsx.snap b/viz-lib/src/visualizations/shared/columns/__snapshots__/link.test.tsx.snap
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/__snapshots__/link.test.tsx.snap
rename to viz-lib/src/visualizations/shared/columns/__snapshots__/link.test.tsx.snap
diff --git a/viz-lib/src/visualizations/table/columns/__snapshots__/number.test.tsx.snap b/viz-lib/src/visualizations/shared/columns/__snapshots__/number.test.tsx.snap
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/__snapshots__/number.test.tsx.snap
rename to viz-lib/src/visualizations/shared/columns/__snapshots__/number.test.tsx.snap
diff --git a/viz-lib/src/visualizations/table/columns/__snapshots__/text.test.tsx.snap b/viz-lib/src/visualizations/shared/columns/__snapshots__/text.test.tsx.snap
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/__snapshots__/text.test.tsx.snap
rename to viz-lib/src/visualizations/shared/columns/__snapshots__/text.test.tsx.snap
diff --git a/viz-lib/src/visualizations/table/columns/boolean.test.tsx b/viz-lib/src/visualizations/shared/columns/boolean.test.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/boolean.test.tsx
rename to viz-lib/src/visualizations/shared/columns/boolean.test.tsx
diff --git a/viz-lib/src/visualizations/table/columns/boolean.tsx b/viz-lib/src/visualizations/shared/columns/boolean.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/boolean.tsx
rename to viz-lib/src/visualizations/shared/columns/boolean.tsx
diff --git a/viz-lib/src/visualizations/table/columns/datetime.test.tsx b/viz-lib/src/visualizations/shared/columns/datetime.test.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/datetime.test.tsx
rename to viz-lib/src/visualizations/shared/columns/datetime.test.tsx
diff --git a/viz-lib/src/visualizations/table/columns/datetime.tsx b/viz-lib/src/visualizations/shared/columns/datetime.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/datetime.tsx
rename to viz-lib/src/visualizations/shared/columns/datetime.tsx
diff --git a/viz-lib/src/visualizations/table/columns/image.test.tsx b/viz-lib/src/visualizations/shared/columns/image.test.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/image.test.tsx
rename to viz-lib/src/visualizations/shared/columns/image.test.tsx
diff --git a/viz-lib/src/visualizations/table/columns/image.tsx b/viz-lib/src/visualizations/shared/columns/image.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/image.tsx
rename to viz-lib/src/visualizations/shared/columns/image.tsx
diff --git a/viz-lib/src/visualizations/table/columns/index.ts b/viz-lib/src/visualizations/shared/columns/index.ts
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/index.ts
rename to viz-lib/src/visualizations/shared/columns/index.ts
diff --git a/viz-lib/src/visualizations/table/columns/json.tsx b/viz-lib/src/visualizations/shared/columns/json.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/json.tsx
rename to viz-lib/src/visualizations/shared/columns/json.tsx
diff --git a/viz-lib/src/visualizations/table/columns/link.test.tsx b/viz-lib/src/visualizations/shared/columns/link.test.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/link.test.tsx
rename to viz-lib/src/visualizations/shared/columns/link.test.tsx
diff --git a/viz-lib/src/visualizations/table/columns/link.tsx b/viz-lib/src/visualizations/shared/columns/link.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/link.tsx
rename to viz-lib/src/visualizations/shared/columns/link.tsx
diff --git a/viz-lib/src/visualizations/table/columns/number.test.tsx b/viz-lib/src/visualizations/shared/columns/number.test.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/number.test.tsx
rename to viz-lib/src/visualizations/shared/columns/number.test.tsx
diff --git a/viz-lib/src/visualizations/table/columns/number.tsx b/viz-lib/src/visualizations/shared/columns/number.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/number.tsx
rename to viz-lib/src/visualizations/shared/columns/number.tsx
diff --git a/viz-lib/src/visualizations/table/columns/text.test.tsx b/viz-lib/src/visualizations/shared/columns/text.test.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/text.test.tsx
rename to viz-lib/src/visualizations/shared/columns/text.test.tsx
diff --git a/viz-lib/src/visualizations/table/columns/text.tsx b/viz-lib/src/visualizations/shared/columns/text.tsx
similarity index 100%
rename from viz-lib/src/visualizations/table/columns/text.tsx
rename to viz-lib/src/visualizations/shared/columns/text.tsx
diff --git a/viz-lib/src/visualizations/shared/components/ColumnEditor.test.tsx b/viz-lib/src/visualizations/shared/components/ColumnEditor.test.tsx
new file mode 100644
index 000000000..8d4b9d17f
--- /dev/null
+++ b/viz-lib/src/visualizations/shared/components/ColumnEditor.test.tsx
@@ -0,0 +1,225 @@
+import React from "react";
+import enzyme from "enzyme";
+
+import ColumnEditor from "./ColumnEditor";
+
+function findByTestID(wrapper: any, testId: any) {
+ return wrapper.find(`[data-test="${testId}"]`);
+}
+
+function mount(column: any, variant: "table" | "details", onChange: any = jest.fn()) {
+ return enzyme.mount(
+
+ );
+}
+
+const mockColumn = {
+ name: "user_id",
+ title: "user_id",
+ visible: true,
+ alignContent: "left" as const,
+ displayAs: "string",
+ description: "",
+ allowSearch: false,
+};
+
+describe("Shared ColumnEditor", () => {
+ describe("Common functionality", () => {
+ test.each(["table", "details"] as const)("Changes column title - %s variant", async (variant) => {
+ return new Promise((resolve) => {
+ const onChange = jest.fn((changes) => {
+ expect(changes).toEqual({
+ ...mockColumn,
+ title: "User ID",
+ });
+ resolve();
+ });
+ const el = mount(mockColumn, variant, onChange);
+
+ const testPrefix = variant === "table" ? "Table" : "Details";
+ findByTestID(el, `${testPrefix}.Column.user_id.Title`)
+ .find("input")
+ .simulate("change", { target: { value: "User ID" } });
+ });
+ });
+
+ test.each(["table", "details"] as const)("Changes column alignment - %s variant", (variant) => {
+ const onChange = jest.fn();
+ const el = mount({
+ ...mockColumn,
+ name: "amount",
+ displayAs: "number",
+ }, variant, onChange);
+
+ const testPrefix = variant === "table" ? "Table" : "Details";
+ findByTestID(el, `${testPrefix}.Column.amount.TextAlignment`)
+ .find('input[value="right"]')
+ .simulate("change", { target: { value: "right" } });
+
+ expect(onChange).toHaveBeenCalledWith({
+ ...mockColumn,
+ name: "amount",
+ displayAs: "number",
+ alignContent: "right",
+ });
+ });
+
+ test.each(["table", "details"] as const)("Changes column description - %s variant", async (variant) => {
+ return new Promise((resolve) => {
+ const onChange = jest.fn((changes) => {
+ expect(changes).toEqual({
+ ...mockColumn,
+ name: "status",
+ title: "Status",
+ description: "Current order status",
+ });
+ resolve();
+ });
+ const el = mount({
+ ...mockColumn,
+ name: "status",
+ title: "Status",
+ }, variant, onChange);
+
+ const testPrefix = variant === "table" ? "Table" : "Details";
+ findByTestID(el, `${testPrefix}.Column.status.Description`)
+ .find("input")
+ .simulate("change", { target: { value: "Current order status" } });
+ });
+ });
+
+ test.each(["table", "details"] as const)("Changes display type - %s variant", (variant) => {
+ const onChange = jest.fn();
+ const el = mount({
+ ...mockColumn,
+ name: "created_at",
+ title: "Created At",
+ displayAs: "datetime",
+ }, variant, onChange);
+
+ const testPrefix = variant === "table" ? "Table" : "Details";
+ findByTestID(el, `${testPrefix}.Column.created_at.DisplayAs`)
+ .find(".ant-select-selector")
+ .simulate("mouseDown");
+ findByTestID(el, `${testPrefix}.Column.created_at.DisplayAs.string`)
+ .simulate("click");
+
+ expect(onChange).toHaveBeenCalledWith({
+ ...mockColumn,
+ name: "created_at",
+ title: "Created At",
+ displayAs: "string",
+ });
+ });
+ });
+
+ describe("Table variant specific", () => {
+ test("Shows search checkbox", () => {
+ const el = mount(mockColumn, "table");
+
+ const searchCheckbox = findByTestID(el, "Table.Column.user_id.UseForSearch");
+ expect(searchCheckbox.find("input[type='checkbox']")).toHaveLength(1);
+ });
+
+ test("Changes search setting", () => {
+ const onChange = jest.fn();
+ const el = mount({
+ ...mockColumn,
+ allowSearch: false,
+ }, "table", onChange);
+
+ findByTestID(el, "Table.Column.user_id.UseForSearch")
+ .find("input[type='checkbox']")
+ .simulate("change", { target: { checked: true } });
+
+ expect(onChange).toHaveBeenCalledWith({
+ ...mockColumn,
+ allowSearch: true,
+ });
+ });
+
+ test("Uses correct CSS class", () => {
+ const el = mount(mockColumn, "table");
+ expect(el.find(".table-visualization-editor-column")).toHaveLength(1);
+ });
+ });
+
+ describe("Details variant specific", () => {
+ test("Hides search checkbox", () => {
+ const el = mount(mockColumn, "details");
+
+ const searchCheckbox = findByTestID(el, "Details.Column.user_id.UseForSearch");
+ expect(searchCheckbox).toHaveLength(0);
+ });
+
+ test("Uses correct CSS class", () => {
+ const el = mount(mockColumn, "details");
+ expect(el.find(".details-visualization-editor-column")).toHaveLength(1);
+ });
+ });
+
+ describe("Props and defaults", () => {
+ test("Uses default showSearch based on variant", () => {
+ const tableEl = mount(mockColumn, "table");
+ const detailsEl = mount(mockColumn, "details");
+
+ expect(findByTestID(tableEl, "Table.Column.user_id.UseForSearch").find("input[type='checkbox']")).toHaveLength(1);
+ expect(findByTestID(detailsEl, "Details.Column.user_id.UseForSearch")).toHaveLength(0);
+ });
+
+ test("Allows custom testPrefix", () => {
+ const el = mount(mockColumn, "table");
+ el.setProps({ testPrefix: "Custom.Prefix" });
+ el.update();
+
+ expect(findByTestID(el, "Custom.Prefix.Title").find("input")).toHaveLength(1);
+ });
+
+ test("Handles missing onChange gracefully", () => {
+ const el = mount(mockColumn, "table", undefined);
+
+ expect(() => {
+ findByTestID(el, "Table.Column.user_id.Title")
+ .find("input")
+ .simulate("change", { target: { value: "New Title" } });
+ }).not.toThrow();
+ });
+ });
+
+ describe("Rendering", () => {
+ test("Table variant renders with correct structure", () => {
+ const el = mount({
+ ...mockColumn,
+ allowSearch: true,
+ description: "Sample description",
+ }, "table");
+
+ // Verify key elements are present
+ expect(el.find('.table-visualization-editor-column')).toHaveLength(1);
+ expect(findByTestID(el, "Table.Column.user_id.Title").find("input")).toHaveLength(1);
+ expect(findByTestID(el, "Table.Column.user_id.TextAlignment").find("input[type='radio']")).toHaveLength(3);
+ expect(findByTestID(el, "Table.Column.user_id.UseForSearch").find("input[type='checkbox']")).toHaveLength(1);
+ expect(findByTestID(el, "Table.Column.user_id.Description").find("input")).toHaveLength(1);
+ expect(findByTestID(el, "Table.Column.user_id.DisplayAs")).toHaveLength(7); // Expected count based on current behavior
+ });
+
+ test("Details variant renders with correct structure", () => {
+ const el = mount({
+ ...mockColumn,
+ description: "Sample description",
+ }, "details");
+
+ // Verify key elements are present
+ expect(el.find('.details-visualization-editor-column')).toHaveLength(1);
+ expect(findByTestID(el, "Details.Column.user_id.Title").find("input")).toHaveLength(1);
+ expect(findByTestID(el, "Details.Column.user_id.TextAlignment").find("input[type='radio']")).toHaveLength(3);
+ expect(findByTestID(el, "Details.Column.user_id.UseForSearch")).toHaveLength(0); // Should not exist
+ expect(findByTestID(el, "Details.Column.user_id.Description").find("input")).toHaveLength(1);
+ expect(findByTestID(el, "Details.Column.user_id.DisplayAs")).toHaveLength(7); // Expected count based on current behavior
+ });
+ });
+});
diff --git a/viz-lib/src/visualizations/shared/components/ColumnEditor.tsx b/viz-lib/src/visualizations/shared/components/ColumnEditor.tsx
new file mode 100644
index 000000000..37dc1e6ef
--- /dev/null
+++ b/viz-lib/src/visualizations/shared/components/ColumnEditor.tsx
@@ -0,0 +1,117 @@
+import { map } from "lodash";
+import React from "react";
+import { useDebouncedCallback } from "use-debounce";
+import * as Grid from "antd/lib/grid";
+import { Section, Select, Input, Checkbox, TextAlignmentSelect } from "@/components/visualizations/editor";
+
+import ColumnTypes from "../columns";
+
+type Column = {
+ name: string;
+ title?: string;
+ visible?: boolean;
+ alignContent?: "left" | "center" | "right";
+ displayAs?: any;
+ description?: string;
+ allowSearch?: boolean;
+};
+
+type ColumnEditorProps = {
+ column: Column;
+ onChange?: (changes: any) => any;
+ variant: "table" | "details";
+ showSearch?: boolean;
+ testPrefix?: string;
+};
+
+export default function ColumnEditor({
+ column,
+ onChange,
+ variant,
+ showSearch = variant === "table",
+ testPrefix,
+}: ColumnEditorProps) {
+ function handleChange(changes: any) {
+ if (onChange) {
+ onChange({ ...column, ...changes });
+ }
+ }
+
+ const [handleChangeDebounced] = useDebouncedCallback(handleChange, 200);
+
+ // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
+ const AdditionalOptions = ColumnTypes[column.displayAs].Editor || null;
+
+ const cssClass = `${variant}-visualization-editor-column`;
+ const dataTestPrefix = testPrefix || `${variant === "table" ? "Table" : "Details"}.Column.${column.name}`;
+
+ return (
+
+ {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
+
+ {/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element[]; gutter: number; type:... Remove this comment to see the full error message */}
+
+
+ handleChangeDebounced({ title: event.target.value })}
+ />
+
+
+ handleChange({ alignContent: event.target.value })}
+ />
+
+
+
+
+ {showSearch && (
+ /* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */
+
+ handleChange({ allowSearch: event.target.checked })}>
+ Use for search
+
+
+ )}
+
+ {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
+
+ handleChangeDebounced({ description: event.target.value })}
+ />
+
+
+ {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
+
+
+
+
+ {AdditionalOptions && }
+
+ );
+}
+
+ColumnEditor.defaultProps = {
+ onChange: () => {},
+};
diff --git a/viz-lib/src/visualizations/shared/components/ColumnsSettings.tsx b/viz-lib/src/visualizations/shared/components/ColumnsSettings.tsx
new file mode 100644
index 000000000..5cc5816d5
--- /dev/null
+++ b/viz-lib/src/visualizations/shared/components/ColumnsSettings.tsx
@@ -0,0 +1,102 @@
+import { map } from "lodash";
+import React from "react";
+import Collapse from "antd/lib/collapse";
+import Tooltip from "antd/lib/tooltip";
+import Typography from "antd/lib/typography";
+// @ts-expect-error ts-migrate(2724) FIXME: Module '"../../../../node_modules/react-sortable-h... Remove this comment to see the full error message
+import { sortableElement } from "react-sortable-hoc";
+import { SortableContainer, DragHandle } from "@/components/sortable";
+import PropTypes from "prop-types";
+
+import EyeOutlinedIcon from "@ant-design/icons/EyeOutlined";
+import EyeInvisibleOutlinedIcon from "@ant-design/icons/EyeInvisibleOutlined";
+
+import ColumnEditor from "./ColumnEditor";
+
+const { Text } = Typography;
+
+const SortableItem = sortableElement(Collapse.Panel);
+
+type ColumnsSettingsProps = {
+ options: any;
+ onOptionsChange: any;
+ variant: "table" | "details";
+};
+
+export default function ColumnsSettings({ options, onOptionsChange, variant }: ColumnsSettingsProps) {
+ function handleColumnChange(newColumn: any, event: any) {
+ if (event) {
+ event.stopPropagation();
+ }
+ const columns = map(options.columns, c => (c.name === newColumn.name ? newColumn : c));
+ onOptionsChange({ columns });
+ }
+
+ function handleColumnsReorder({ oldIndex, newIndex }: any) {
+ const columns = [...options.columns];
+ columns.splice(newIndex, 0, ...columns.splice(oldIndex, 1));
+ onOptionsChange({ columns });
+ }
+
+ const helperClass = `${variant}-editor-columns-dragged-item`;
+ const containerClass = `${variant}-visualization-editor-columns`;
+ const testPrefix = variant === "table" ? "Table" : "Details";
+
+ return (
+ container.firstChild}
+ onSortEnd={handleColumnsReorder}
+ containerProps={{
+ className: containerClass,
+ }}>
+ {/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
+
+ {map(options.columns, (column, index) => (
+
+
+
+ {column.name}
+ {column.title !== "" && column.title !== column.name && (
+
+ ({column.title})
+
+ )}
+
+
+ }
+ extra={
+
+ {column.visible ? (
+ handleColumnChange({ ...column, visible: !column.visible }, event)}
+ />
+ ) : (
+ handleColumnChange({ ...column, visible: !column.visible }, event)}
+ />
+ )}
+
+ }>
+ handleColumnChange(changes, undefined)} />
+
+ ))}
+
+
+ );
+}
+
+ColumnsSettings.propTypes = {
+ options: PropTypes.object.isRequired,
+ onOptionsChange: PropTypes.func.isRequired,
+ variant: PropTypes.oneOf(["table", "details"]).isRequired,
+};
diff --git a/viz-lib/src/visualizations/table/Editor/ColumnEditor.tsx b/viz-lib/src/visualizations/table/Editor/ColumnEditor.tsx
index af56802c0..a123033a5 100644
--- a/viz-lib/src/visualizations/table/Editor/ColumnEditor.tsx
+++ b/viz-lib/src/visualizations/table/Editor/ColumnEditor.tsx
@@ -1,10 +1,5 @@
-import { map, keys } from "lodash";
import React from "react";
-import { useDebouncedCallback } from "use-debounce";
-import * as Grid from "antd/lib/grid";
-import { Section, Select, Input, Checkbox, TextAlignmentSelect } from "@/components/visualizations/editor";
-
-import ColumnTypes from "../columns";
+import SharedColumnEditor from "../../shared/components/ColumnEditor";
type OwnProps = {
column: {
@@ -12,7 +7,9 @@ type OwnProps = {
title?: string;
visible?: boolean;
alignContent?: "left" | "center" | "right";
- displayAs?: any; // TODO: PropTypes.oneOf(keys(ColumnTypes))
+ displayAs?: any;
+ allowSearch?: boolean;
+ description?: string;
};
onChange?: (...args: any[]) => any;
};
@@ -20,78 +17,13 @@ type OwnProps = {
type Props = OwnProps & typeof ColumnEditor.defaultProps;
export default function ColumnEditor({ column, onChange }: Props) {
- function handleChange(changes: any) {
- onChange({ ...column, ...changes });
- }
-
- const [handleChangeDebounced] = useDebouncedCallback(handleChange, 200);
-
- // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- const AdditionalOptions = ColumnTypes[column.displayAs].Editor || null;
-
return (
-
- {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
-
- {/* @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: Element[]; gutter: number; type:... Remove this comment to see the full error message */}
-
-
- handleChangeDebounced({ title: event.target.value })}
- />
-
-
- handleChange({ alignContent: event.target.value })}
- />
-
-
-
-
- {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
-
- handleChange({ allowSearch: event.target.checked })}>
- Use for search
-
-
-
- {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
-
- handleChangeDebounced({ description: event.target.value })}
- />
-
-
- {/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
-
-
-
-
- {AdditionalOptions && }
-
+
);
}
diff --git a/viz-lib/src/visualizations/table/Editor/ColumnsSettings.tsx b/viz-lib/src/visualizations/table/Editor/ColumnsSettings.tsx
index 1a7521829..fb460b2d5 100644
--- a/viz-lib/src/visualizations/table/Editor/ColumnsSettings.tsx
+++ b/viz-lib/src/visualizations/table/Editor/ColumnsSettings.tsx
@@ -1,88 +1,14 @@
-import { map } from "lodash";
import React from "react";
-import Collapse from "antd/lib/collapse";
-import Tooltip from "antd/lib/tooltip";
-import Typography from "antd/lib/typography";
-// @ts-expect-error ts-migrate(2724) FIXME: Module '"../../../../node_modules/react-sortable-h... Remove this comment to see the full error message
-import { sortableElement } from "react-sortable-hoc";
-import { SortableContainer, DragHandle } from "@/components/sortable";
+import SharedColumnsSettings from "../../shared/components/ColumnsSettings";
import { EditorPropTypes } from "@/visualizations/prop-types";
-import EyeOutlinedIcon from "@ant-design/icons/EyeOutlined";
-import EyeInvisibleOutlinedIcon from "@ant-design/icons/EyeInvisibleOutlined";
-
-import ColumnEditor from "./ColumnEditor";
-
-const { Text } = Typography;
-
-const SortableItem = sortableElement(Collapse.Panel);
-
-export default function ColumnsSettings({ options, onOptionsChange }: any) {
- function handleColumnChange(newColumn: any, event: any) {
- if (event) {
- event.stopPropagation();
- }
- const columns = map(options.columns, c => (c.name === newColumn.name ? newColumn : c));
- onOptionsChange({ columns });
- }
-
- function handleColumnsReorder({ oldIndex, newIndex }: any) {
- const columns = [...options.columns];
- columns.splice(newIndex, 0, ...columns.splice(oldIndex, 1));
- onOptionsChange({ columns });
- }
-
+export default function ColumnsSettings({ options, onOptionsChange, data }: any) {
return (
- container.firstChild}
- onSortEnd={handleColumnsReorder}
- containerProps={{
- className: "table-visualization-editor-columns",
- }}>
- {/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Element' is not assignable to type 'null | u... Remove this comment to see the full error message */}
-
- {map(options.columns, (column, index) => (
-
-
-
- {column.name}
- {column.title !== "" && column.title !== column.name && (
-
- ({column.title})
-
- )}
-
-
- }
- extra={
-
- {column.visible ? (
- handleColumnChange({ ...column, visible: !column.visible }, event)}
- />
- ) : (
- handleColumnChange({ ...column, visible: !column.visible }, event)}
- />
- )}
-
- }>
- {/* @ts-expect-error ts-migrate(2322) FIXME: Type '(newColumn: any, event: any) => void' is not... Remove this comment to see the full error message */}
-
-
- ))}
-
-
+
);
}
diff --git a/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap b/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap
index 9d07ca3ff..20a5e8cc1 100644
--- a/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap
+++ b/viz-lib/src/visualizations/table/Editor/__snapshots__/ColumnsSettings.test.tsx.snap
@@ -12,6 +12,7 @@ Object {
"true",
],
"dateTimeFormat": undefined,
+ "description": "",
"displayAs": "string",
"highlightLinks": false,
"imageHeight": "",
@@ -46,6 +47,7 @@ Object {
"true",
],
"dateTimeFormat": undefined,
+ "description": "",
"displayAs": "number",
"highlightLinks": false,
"imageHeight": "",
@@ -80,6 +82,7 @@ Object {
"true",
],
"dateTimeFormat": undefined,
+ "description": "",
"displayAs": "string",
"highlightLinks": false,
"imageHeight": "",
@@ -114,6 +117,7 @@ Object {
"true",
],
"dateTimeFormat": undefined,
+ "description": "",
"displayAs": "string",
"highlightLinks": false,
"imageHeight": "",
@@ -148,6 +152,7 @@ Object {
"true",
],
"dateTimeFormat": undefined,
+ "description": "",
"displayAs": "string",
"highlightLinks": false,
"imageHeight": "",
diff --git a/viz-lib/src/visualizations/table/getOptions.ts b/viz-lib/src/visualizations/table/getOptions.ts
index bab412b05..adf76aa88 100644
--- a/viz-lib/src/visualizations/table/getOptions.ts
+++ b/viz-lib/src/visualizations/table/getOptions.ts
@@ -1,133 +1,19 @@
import _ from "lodash";
-import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
+import {
+ getDefaultColumnsOptions,
+ getDefaultFormatOptions,
+ getColumnsOptions,
+} from "@/visualizations/shared/columnUtils";
const DEFAULT_OPTIONS = {
itemsPerPage: 25,
paginationSize: "default", // not editable through Editor
};
-const filterTypes = ["filter", "multi-filter", "multiFilter"];
-
-function getColumnNameWithoutType(column: any) {
- let typeSplit;
- if (column.indexOf("::") !== -1) {
- typeSplit = "::";
- } else if (column.indexOf("__") !== -1) {
- typeSplit = "__";
- } else {
- return column;
- }
-
- const parts = column.split(typeSplit);
- if (parts[0] === "" && parts.length === 2) {
- return parts[1];
- }
-
- if (!_.includes(filterTypes, parts[1])) {
- return column;
- }
-
- return parts[0];
-}
-
-function getColumnContentAlignment(type: any) {
- return ["integer", "float", "boolean", "date", "datetime"].indexOf(type) >= 0 ? "right" : "left";
-}
-
-function getDefaultColumnsOptions(columns: any) {
- const displayAs = {
- integer: "number",
- float: "number",
- boolean: "boolean",
- date: "datetime",
- datetime: "datetime",
- };
-
- return _.map(columns, (col, index) => ({
- name: col.name,
- type: col.type,
- // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- displayAs: displayAs[col.type] || "string",
- visible: true,
- order: 100000 + index,
- title: getColumnNameWithoutType(col.name),
- allowSearch: false,
- alignContent: getColumnContentAlignment(col.type),
- // `string` cell options
- allowHTML: false,
- highlightLinks: false,
- }));
-}
-
-function getDefaultFormatOptions(column: any) {
- const dateTimeFormat = {
- date: visualizationsSettings.dateFormat || "DD/MM/YYYY",
- datetime: visualizationsSettings.dateTimeFormat || "DD/MM/YYYY HH:mm",
- };
- const numberFormat = {
- integer: visualizationsSettings.integerFormat || "0,0",
- float: visualizationsSettings.floatFormat || "0,0.00",
- };
- return {
- // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- dateTimeFormat: dateTimeFormat[column.type],
- // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
- numberFormat: numberFormat[column.type],
- nullValue: visualizationsSettings.nullValue,
- booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
- // `image` cell options
- imageUrlTemplate: "{{ @ }}",
- imageTitleTemplate: "{{ @ }}",
- imageWidth: "",
- imageHeight: "",
- // `link` cell options
- linkUrlTemplate: "{{ @ }}",
- linkTextTemplate: "{{ @ }}",
- linkTitleTemplate: "{{ @ }}",
- linkOpenInNewTab: true,
- };
-}
-
-function wereColumnsReordered(queryColumns: any, visualizationColumns: any) {
- queryColumns = _.map(queryColumns, col => col.name);
- visualizationColumns = _.map(visualizationColumns, col => col.name);
-
- // Some columns may be removed - so skip them (but keep original order)
- visualizationColumns = _.filter(visualizationColumns, col => _.includes(queryColumns, col));
- // Pick query columns that were previously saved with viz (but keep order too)
- queryColumns = _.filter(queryColumns, col => _.includes(visualizationColumns, col));
-
- // Both array now have the same size as they both contains only common columns
- // (in fact, it was an intersection, that kept order of items on both arrays).
- // Now check for equality item-by-item; if common columns are in the same order -
- // they were not reordered in editor
- for (let i = 0; i < queryColumns.length; i += 1) {
- if (visualizationColumns[i] !== queryColumns[i]) {
- return true;
- }
- }
- return false;
-}
-
-function getColumnsOptions(columns: any, visualizationColumns: any) {
- const options = getDefaultColumnsOptions(columns);
-
- if (wereColumnsReordered(columns, visualizationColumns)) {
- visualizationColumns = _.fromPairs(
- _.map(visualizationColumns, (col, index) => [col.name, _.extend({}, col, { order: index })])
- );
- } else {
- visualizationColumns = _.fromPairs(_.map(visualizationColumns, col => [col.name, _.omit(col, "order")]));
- }
-
- _.each(options, col => _.extend(col, visualizationColumns[col.name]));
-
- return _.sortBy(options, "order");
-}
export default function getOptions(options: any, { columns }: any) {
options = { ...DEFAULT_OPTIONS, ...options };
- options.columns = _.map(getColumnsOptions(columns, options.columns), col => ({
+ options.columns = _.map(getColumnsOptions(columns, options.columns, { allowSearch: false }), col => ({
...getDefaultFormatOptions(col),
...col,
}));
diff --git a/viz-lib/src/visualizations/table/utils.tsx b/viz-lib/src/visualizations/table/utils.tsx
index 298cdb7f9..316acda0a 100644
--- a/viz-lib/src/visualizations/table/utils.tsx
+++ b/viz-lib/src/visualizations/table/utils.tsx
@@ -2,7 +2,7 @@ import { isNil, map, get, filter, each, sortBy, some, findIndex, toString } from
import React from "react";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
-import ColumnTypes from "./columns";
+import ColumnTypes from "../shared/columns";
function nextOrderByDirection(direction: any) {
switch (direction) {