From 5b463b0d83753fdaa81768d43feef3117f509a45 Mon Sep 17 00:00:00 2001 From: Kamil Frydel Date: Mon, 6 Oct 2025 14:10:33 +0200 Subject: [PATCH] Make details visualization configurable (#7535) - Added possibility to select visible columns and reordering - Added formatting options as in Table visualization - Set default alignment to left --- .../details/DetailsRenderer.tsx | 64 --- .../details/Editor/ColumnEditor.tsx | 31 + .../details/Editor/ColumnsSettings.test.tsx | 98 ++++ .../details/Editor/ColumnsSettings.tsx | 15 + .../ColumnsSettings.test.tsx.snap | 529 ++++++++++++++++++ .../visualizations/details/Editor/editor.less | 33 ++ .../visualizations/details/Editor/index.tsx | 9 + .../visualizations/details/Renderer.test.tsx | 179 ++++++ .../src/visualizations/details/Renderer.tsx | 82 +++ .../visualizations/details/getOptions.test.ts | 160 ++++++ .../src/visualizations/details/getOptions.ts | 17 + viz-lib/src/visualizations/details/index.ts | 14 +- .../src/visualizations/shared/columnUtils.ts | 126 +++++ .../__snapshots__/boolean.test.tsx.snap | 0 .../__snapshots__/datetime.test.tsx.snap | 0 .../columns/__snapshots__/image.test.tsx.snap | 0 .../columns/__snapshots__/link.test.tsx.snap | 0 .../__snapshots__/number.test.tsx.snap | 0 .../columns/__snapshots__/text.test.tsx.snap | 0 .../columns/boolean.test.tsx | 0 .../{table => shared}/columns/boolean.tsx | 0 .../columns/datetime.test.tsx | 0 .../{table => shared}/columns/datetime.tsx | 0 .../{table => shared}/columns/image.test.tsx | 0 .../{table => shared}/columns/image.tsx | 0 .../{table => shared}/columns/index.ts | 0 .../{table => shared}/columns/json.tsx | 0 .../{table => shared}/columns/link.test.tsx | 0 .../{table => shared}/columns/link.tsx | 0 .../{table => shared}/columns/number.test.tsx | 0 .../{table => shared}/columns/number.tsx | 0 .../{table => shared}/columns/text.test.tsx | 0 .../{table => shared}/columns/text.tsx | 0 .../shared/components/ColumnEditor.test.tsx | 225 ++++++++ .../shared/components/ColumnEditor.tsx | 117 ++++ .../shared/components/ColumnsSettings.tsx | 102 ++++ .../table/Editor/ColumnEditor.tsx | 88 +-- .../table/Editor/ColumnsSettings.tsx | 88 +-- .../ColumnsSettings.test.tsx.snap | 5 + .../src/visualizations/table/getOptions.ts | 126 +---- viz-lib/src/visualizations/table/utils.tsx | 2 +- 41 files changed, 1758 insertions(+), 352 deletions(-) delete mode 100644 viz-lib/src/visualizations/details/DetailsRenderer.tsx create mode 100644 viz-lib/src/visualizations/details/Editor/ColumnEditor.tsx create mode 100644 viz-lib/src/visualizations/details/Editor/ColumnsSettings.test.tsx create mode 100644 viz-lib/src/visualizations/details/Editor/ColumnsSettings.tsx create mode 100644 viz-lib/src/visualizations/details/Editor/__snapshots__/ColumnsSettings.test.tsx.snap create mode 100644 viz-lib/src/visualizations/details/Editor/editor.less create mode 100644 viz-lib/src/visualizations/details/Editor/index.tsx create mode 100644 viz-lib/src/visualizations/details/Renderer.test.tsx create mode 100644 viz-lib/src/visualizations/details/Renderer.tsx create mode 100644 viz-lib/src/visualizations/details/getOptions.test.ts create mode 100644 viz-lib/src/visualizations/details/getOptions.ts create mode 100644 viz-lib/src/visualizations/shared/columnUtils.ts rename viz-lib/src/visualizations/{table => shared}/columns/__snapshots__/boolean.test.tsx.snap (100%) rename viz-lib/src/visualizations/{table => shared}/columns/__snapshots__/datetime.test.tsx.snap (100%) rename viz-lib/src/visualizations/{table => shared}/columns/__snapshots__/image.test.tsx.snap (100%) rename viz-lib/src/visualizations/{table => shared}/columns/__snapshots__/link.test.tsx.snap (100%) rename viz-lib/src/visualizations/{table => shared}/columns/__snapshots__/number.test.tsx.snap (100%) rename viz-lib/src/visualizations/{table => shared}/columns/__snapshots__/text.test.tsx.snap (100%) rename viz-lib/src/visualizations/{table => shared}/columns/boolean.test.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/boolean.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/datetime.test.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/datetime.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/image.test.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/image.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/index.ts (100%) rename viz-lib/src/visualizations/{table => shared}/columns/json.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/link.test.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/link.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/number.test.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/number.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/text.test.tsx (100%) rename viz-lib/src/visualizations/{table => shared}/columns/text.tsx (100%) create mode 100644 viz-lib/src/visualizations/shared/components/ColumnEditor.test.tsx create mode 100644 viz-lib/src/visualizations/shared/components/ColumnEditor.tsx create mode 100644 viz-lib/src/visualizations/shared/components/ColumnsSettings.tsx diff --git a/viz-lib/src/visualizations/details/DetailsRenderer.tsx b/viz-lib/src/visualizations/details/DetailsRenderer.tsx deleted file mode 100644 index ce8a59bea..000000000 --- a/viz-lib/src/visualizations/details/DetailsRenderer.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useState } from "react"; -import { map, mapValues, keyBy } from "lodash"; -import moment from "moment"; -import { RendererPropTypes } from "@/visualizations/prop-types"; -import { visualizationsSettings } from "@/visualizations/visualizationsSettings"; -import Descriptions from "antd/lib/descriptions"; -import Pagination from "antd/lib/pagination"; - -import "./details.less"; - -function renderValue(value: any, type: any) { - const formats = { - date: visualizationsSettings.dateFormat, - datetime: visualizationsSettings.dateTimeFormat, - }; - - if (type === "date" || type === "datetime") { - if (moment.isMoment(value)) { - // @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message - return value.format(formats[type]); - } - } - - return "" + value; -} - -export default function DetailsRenderer({ data }: any) { - const [page, setPage] = useState(0); - - if (!data || !data.rows || data.rows.length === 0) { - return null; - } - - const types = mapValues(keyBy(data.columns, "name"), "type"); - - // We use columsn to maintain order of columns in the view. - const columns = data.columns.map((column: any) => column.name); - const row = data.rows[page]; - - return ( -
- - {map(columns, key => ( - - {renderValue(row[key], types[key])} - - ))} - - {data.rows.length > 1 && ( -
- setPage(p - 1)} - /> -
- )} -
- ); -} - -DetailsRenderer.propTypes = RendererPropTypes; diff --git a/viz-lib/src/visualizations/details/Editor/ColumnEditor.tsx b/viz-lib/src/visualizations/details/Editor/ColumnEditor.tsx new file mode 100644 index 000000000..56a51f972 --- /dev/null +++ b/viz-lib/src/visualizations/details/Editor/ColumnEditor.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import SharedColumnEditor from "../../shared/components/ColumnEditor"; + +type OwnProps = { + column: { + name: string; + title?: string; + visible?: boolean; + alignContent?: "left" | "center" | "right"; + displayAs?: any; + description?: string; + }; + onChange?: (...args: any[]) => any; +}; + +type Props = OwnProps & typeof ColumnEditor.defaultProps; + +export default function ColumnEditor({ column, onChange }: Props) { + return ( + + ); +} + +ColumnEditor.defaultProps = { + onChange: (...args: any[]) => {}, +}; diff --git a/viz-lib/src/visualizations/details/Editor/ColumnsSettings.test.tsx b/viz-lib/src/visualizations/details/Editor/ColumnsSettings.test.tsx new file mode 100644 index 000000000..b95801805 --- /dev/null +++ b/viz-lib/src/visualizations/details/Editor/ColumnsSettings.test.tsx @@ -0,0 +1,98 @@ +import React from "react"; +import enzyme from "enzyme"; + +import getOptions from "../getOptions"; +import ColumnsSettings from "./ColumnsSettings"; + +function findByTestID(wrapper: any, testId: any) { + return wrapper.find(`[data-test="${testId}"]`); +} + +function mount(options: any, done: any) { + const data = { + columns: [ + { name: "id", type: "integer" }, + { name: "name", type: "string" }, + { name: "created_at", type: "datetime" }, + ], + rows: [{ id: 1, name: "test", created_at: "2023-01-01T00:00:00Z" }], + }; + options = getOptions(options, data); + return enzyme.mount( + { + expect(changedOptions).toMatchSnapshot(); + done(); + }} + /> + ); +} + +describe("Visualizations -> Details -> Editor -> Columns Settings", () => { + test("Toggles column visibility", done => { + const el = mount({}, done); + + findByTestID(el, "Details.Column.id.Visibility") + .last() + .simulate("click"); + }); + + test("Changes column title", done => { + const el = mount({}, done); + findByTestID(el, "Details.Column.name.Name") + .last() + .simulate("click"); // expand settings + + findByTestID(el, "Details.Column.name.Title") + .last() + .simulate("change", { target: { value: "Full Name" } }); + }); + + test("Changes column alignment", done => { + const el = mount({}, done); + findByTestID(el, "Details.Column.id.Name") + .last() + .simulate("click"); // expand settings + + findByTestID(el, "Details.Column.id.TextAlignment") + .last() + .find('[data-test="TextAlignmentSelect.Center"] input') + .simulate("change", { target: { checked: true } }); + }); + + test("Changes column description", done => { + const el = mount({}, done); + findByTestID(el, "Details.Column.name.Name") + .last() + .simulate("click"); // expand settings + + findByTestID(el, "Details.Column.name.Description") + .last() + .simulate("change", { target: { value: "User full name" } }); + }); + + test("Changes column display type", done => { + const el = mount({}, done); + findByTestID(el, "Details.Column.created_at.Name") + .last() + .simulate("click"); // expand settings + + findByTestID(el, "Details.Column.created_at.DisplayAs") + .last() + .simulate("mouseDown"); + findByTestID(el, "Details.Column.created_at.DisplayAs.string") + .last() + .simulate("click"); + }); + + test("Hides multiple columns", done => { + const el = mount({}, done); + + findByTestID(el, "Details.Column.id.Visibility") + .last() + .simulate("click"); + }); +}); diff --git a/viz-lib/src/visualizations/details/Editor/ColumnsSettings.tsx b/viz-lib/src/visualizations/details/Editor/ColumnsSettings.tsx new file mode 100644 index 000000000..018ae23bd --- /dev/null +++ b/viz-lib/src/visualizations/details/Editor/ColumnsSettings.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import SharedColumnsSettings from "../../shared/components/ColumnsSettings"; +import { EditorPropTypes } from "@/visualizations/prop-types"; + +export default function ColumnsSettings({ options, onOptionsChange, data }: any) { + return ( + + ); +} + +ColumnsSettings.propTypes = EditorPropTypes; diff --git a/viz-lib/src/visualizations/details/Editor/__snapshots__/ColumnsSettings.test.tsx.snap b/viz-lib/src/visualizations/details/Editor/__snapshots__/ColumnsSettings.test.tsx.snap new file mode 100644 index 000000000..5e619ccc6 --- /dev/null +++ b/viz-lib/src/visualizations/details/Editor/__snapshots__/ColumnsSettings.test.tsx.snap @@ -0,0 +1,529 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column alignment 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "center", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "id", + "nullValue": "null", + "numberFormat": "0,0", + "order": 100000, + "title": "id", + "type": "integer", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "name", + "nullValue": "null", + "numberFormat": undefined, + "order": 100001, + "title": "name", + "type": "string", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": "DD/MM/YYYY HH:mm", + "description": "", + "displayAs": "datetime", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "created_at", + "nullValue": "null", + "numberFormat": undefined, + "order": 100002, + "title": "created_at", + "type": "datetime", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column description 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "id", + "nullValue": "null", + "numberFormat": "0,0", + "order": 100000, + "title": "id", + "type": "integer", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "User full name", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "name", + "nullValue": "null", + "numberFormat": undefined, + "order": 100001, + "title": "name", + "type": "string", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": "DD/MM/YYYY HH:mm", + "description": "", + "displayAs": "datetime", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "created_at", + "nullValue": "null", + "numberFormat": undefined, + "order": 100002, + "title": "created_at", + "type": "datetime", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column display type 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "id", + "nullValue": "null", + "numberFormat": "0,0", + "order": 100000, + "title": "id", + "type": "integer", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "name", + "nullValue": "null", + "numberFormat": undefined, + "order": 100001, + "title": "name", + "type": "string", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": "DD/MM/YYYY HH:mm", + "description": "", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "created_at", + "nullValue": "null", + "numberFormat": undefined, + "order": 100002, + "title": "created_at", + "type": "datetime", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Details -> Editor -> Columns Settings Changes column title 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "id", + "nullValue": "null", + "numberFormat": "0,0", + "order": 100000, + "title": "id", + "type": "integer", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "name", + "nullValue": "null", + "numberFormat": undefined, + "order": 100001, + "title": "Full Name", + "type": "string", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": "DD/MM/YYYY HH:mm", + "description": "", + "displayAs": "datetime", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "created_at", + "nullValue": "null", + "numberFormat": undefined, + "order": 100002, + "title": "created_at", + "type": "datetime", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Details -> Editor -> Columns Settings Hides multiple columns 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "id", + "nullValue": "null", + "numberFormat": "0,0", + "order": 100000, + "title": "id", + "type": "integer", + "visible": false, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "name", + "nullValue": "null", + "numberFormat": undefined, + "order": 100001, + "title": "name", + "type": "string", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": "DD/MM/YYYY HH:mm", + "description": "", + "displayAs": "datetime", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "created_at", + "nullValue": "null", + "numberFormat": undefined, + "order": 100002, + "title": "created_at", + "type": "datetime", + "visible": true, + }, + ], +} +`; + +exports[`Visualizations -> Details -> Editor -> Columns Settings Toggles column visibility 1`] = ` +Object { + "columns": Array [ + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "number", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "id", + "nullValue": "null", + "numberFormat": "0,0", + "order": 100000, + "title": "id", + "type": "integer", + "visible": false, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": undefined, + "description": "", + "displayAs": "string", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "name", + "nullValue": "null", + "numberFormat": undefined, + "order": 100001, + "title": "name", + "type": "string", + "visible": true, + }, + Object { + "alignContent": "left", + "allowHTML": false, + "booleanValues": Array [ + "false", + "true", + ], + "dateTimeFormat": "DD/MM/YYYY HH:mm", + "description": "", + "displayAs": "datetime", + "highlightLinks": false, + "imageHeight": "", + "imageTitleTemplate": "{{ @ }}", + "imageUrlTemplate": "{{ @ }}", + "imageWidth": "", + "linkOpenInNewTab": true, + "linkTextTemplate": "{{ @ }}", + "linkTitleTemplate": "{{ @ }}", + "linkUrlTemplate": "{{ @ }}", + "name": "created_at", + "nullValue": "null", + "numberFormat": undefined, + "order": 100002, + "title": "created_at", + "type": "datetime", + "visible": true, + }, + ], +} +`; diff --git a/viz-lib/src/visualizations/details/Editor/editor.less b/viz-lib/src/visualizations/details/Editor/editor.less new file mode 100644 index 000000000..c5330f5dd --- /dev/null +++ b/viz-lib/src/visualizations/details/Editor/editor.less @@ -0,0 +1,33 @@ +.details-visualization-editor-columns { + .ant-collapse { + background: transparent; + } + + .ant-collapse-item { + background: #ffffff; + + .drag-handle { + height: 20px; + margin-left: -16px; + padding: 0 16px; + } + } + + .details-editor-columns-dragged-item { + z-index: 1; + } +} + +.details-visualization-editor-column { + padding-left: 6px; + + .image-dimension-selector { + display: flex; + align-items: center; + + .image-dimension-selector-spacer { + padding-left: 5px; + padding-right: 5px; + } + } +} diff --git a/viz-lib/src/visualizations/details/Editor/index.tsx b/viz-lib/src/visualizations/details/Editor/index.tsx new file mode 100644 index 000000000..579e50ce3 --- /dev/null +++ b/viz-lib/src/visualizations/details/Editor/index.tsx @@ -0,0 +1,9 @@ +import createTabbedEditor from "@/components/visualizations/editor/createTabbedEditor"; + +import ColumnsSettings from "./ColumnsSettings"; + +import "./editor.less"; + +export default createTabbedEditor([ + { key: "Columns", title: "Columns", component: ColumnsSettings }, +]); diff --git a/viz-lib/src/visualizations/details/Renderer.test.tsx b/viz-lib/src/visualizations/details/Renderer.test.tsx new file mode 100644 index 000000000..60aa5ccdd --- /dev/null +++ b/viz-lib/src/visualizations/details/Renderer.test.tsx @@ -0,0 +1,179 @@ +import React from "react"; +import enzyme from "enzyme"; +import moment from "moment"; + +import Renderer from "./Renderer"; +import getOptions from "./getOptions"; + +function mount(data: any, options: any = {}) { + options = getOptions(options, data); + return enzyme.mount(); +} + +describe("Visualizations -> Details -> Renderer", () => { + const sampleData = { + columns: [ + { name: "id", type: "integer" }, + { name: "name", type: "string" }, + { name: "created_at", type: "datetime" }, + { name: "active", type: "boolean" }, + ], + rows: [ + { + id: 1, + name: "John Doe", + created_at: moment("2023-01-01T12:00:00Z"), + active: true, + }, + { + id: 2, + name: "Jane Smith", + created_at: moment("2023-02-01T12:00:00Z"), + active: false, + }, + ], + }; + + test("Renders all columns when no options provided", () => { + const el = mount(sampleData); + + // Check that the component renders with expected data + expect(el.text()).toContain("id"); + expect(el.text()).toContain("name"); + expect(el.text()).toContain("created_at"); + expect(el.text()).toContain("active"); + expect(el.text()).toContain("1"); // id value + expect(el.text()).toContain("John Doe"); // name value + }); + + test("Renders only visible columns", () => { + const options = { + columns: [ + { name: "id", visible: true, order: 0 }, + { name: "name", visible: false, order: 1 }, + { name: "created_at", visible: true, order: 2 }, + { name: "active", visible: false, order: 3 }, + ], + }; + + const el = mount(sampleData, options); + + // Should show id and created_at, but not name and active + expect(el.text()).toContain("id"); + expect(el.text()).toContain("created_at"); + expect(el.text()).not.toContain("name"); + expect(el.text()).not.toContain("active"); + }); + + test("Respects column order", () => { + const options = { + columns: [ + { name: "active", visible: true, order: 0 }, + { name: "name", visible: true, order: 1 }, + { name: "created_at", visible: true, order: 2 }, + { name: "id", visible: true, order: 3 }, + ], + }; + + const el = mount(sampleData, options); + + // Get all description item labels in order + const labels = el.find('.ant-descriptions-item-label').map(node => node.text()); + + // Should appear in order: active (0), name (1), created_at (2), id (3) + expect(labels).toEqual(['active', 'name', 'created_at', 'id']); + }); + + test("Uses custom column titles", () => { + const options = { + columns: [ + { name: "id", visible: true, title: "User ID", order: 0 }, + { name: "name", visible: true, title: "Full Name", order: 1 }, + ], + }; + + const el = mount(sampleData, options); + + expect(el.text()).toContain("User ID"); + expect(el.text()).toContain("Full Name"); + }); + + test("Applies text alignment", () => { + const options = { + columns: [ + { name: "id", visible: true, alignContent: "center", order: 0 }, + { name: "name", visible: true, alignContent: "right", order: 1 }, + ], + }; + + const el = mount(sampleData, options); + + // Check that alignment styles are applied + const alignedDivs = el.find('div[style]'); + expect(alignedDivs.length).toBeGreaterThan(0); + }); + + test("Shows pagination for multiple rows", () => { + const el = mount(sampleData); + + // Check that pagination is present - look for pagination elements + const paginationElements = el.find('[className*="paginator"]'); + expect(paginationElements.length).toBeGreaterThan(0); + }); + + test("Hides pagination for single row", () => { + const singleRowData = { + ...sampleData, + rows: [sampleData.rows[0]], + }; + + const el = mount(singleRowData); + + // Check that pagination is not present for single row + const paginationElements = el.find('[className*="paginator"]'); + expect(paginationElements.length).toBe(0); + }); + + test("Handles empty data", () => { + const emptyData = { + columns: [], + rows: [], + }; + + const el = mount(emptyData); + + expect(el.html()).toBeNull(); + }); + + test("Handles null data", () => { + // Suppress PropTypes warning for this test + const originalError = console.error; + console.error = jest.fn(); + + // Test the component directly with null data instead of using mount helper + const el = enzyme.mount(); + + expect(el.html()).toBeNull(); + + // Restore console.error + console.error = originalError; + }); + + test("Navigates between rows with pagination", () => { + const el = mount(sampleData); + + // Check first row is displayed + expect(el.text()).toContain("John Doe"); + expect(el.text()).not.toContain("Jane Smith"); + + // Find and click next button + const nextButton = el.find('button').filterWhere(n => n.text().includes('Next') || n.prop('aria-label') === 'Next Page'); + if (nextButton.length > 0) { + nextButton.first().simulate("click"); + + // Check second row is displayed after state update + el.update(); + expect(el.text()).toContain("Jane Smith"); + } + }); +}); diff --git a/viz-lib/src/visualizations/details/Renderer.tsx b/viz-lib/src/visualizations/details/Renderer.tsx new file mode 100644 index 000000000..3b92594ab --- /dev/null +++ b/viz-lib/src/visualizations/details/Renderer.tsx @@ -0,0 +1,82 @@ +import React, { useState, useMemo } from "react"; +import { map, filter, sortBy } from "lodash"; +import { RendererPropTypes } from "@/visualizations/prop-types"; +import Descriptions from "antd/lib/descriptions"; +import Pagination from "antd/lib/pagination"; +import Tooltip from "antd/lib/tooltip"; + +import ColumnTypes from "../shared/columns"; +import "./details.less"; + + +export default function Renderer({ data, options }: any) { + const [page, setPage] = useState(0); + + const visibleColumns = useMemo(() => { + if (!options?.columns) return []; + + const columns = sortBy(filter(options.columns, "visible"), "order"); + + return columns.map((column: any) => { + // @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 ColumnType = ColumnTypes[column.displayAs] || ColumnTypes.string; + const Component = ColumnType(column); + + return { + ...column, + Component, + }; + }); + }, [options?.columns]); + + if (!data || !data.rows || data.rows.length === 0) { + return null; + } + + const row = data.rows[page]; + + return ( +
+ + {map(visibleColumns, column => { + const { Component } = column; + + return ( + + {column.description && ( + + + + + + )} + {column.title || column.name} + + } + > +
+ +
+
+ ); + })} +
+ {data.rows.length > 1 && ( +
+ setPage(p - 1)} + /> +
+ )} +
+ ); +} + +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) {