diff --git a/packages/perspective-viewer-datagrid/src/js/custom_elements/datagrid.js b/packages/perspective-viewer-datagrid/src/js/custom_elements/datagrid.js index ab743d1da8..951da8e4f8 100644 --- a/packages/perspective-viewer-datagrid/src/js/custom_elements/datagrid.js +++ b/packages/perspective-viewer-datagrid/src/js/custom_elements/datagrid.js @@ -15,6 +15,7 @@ import { restore } from "../plugin/restore.js"; import { connectedCallback } from "../plugin/connected"; import { save } from "../plugin/save"; import { draw } from "../plugin/draw"; +import getDefaultConfig from "../default_config.js"; /** * The custom element class for this plugin. The interface methods for this @@ -66,6 +67,11 @@ export class HTMLPerspectiveViewerDatagridPluginElement extends HTMLElement { return 1; } + /** opt-in to column styling */ + get default_config() { + return getDefaultConfig.call(this); + } + async draw(view) { return await draw.call(this, view); } diff --git a/packages/perspective-viewer-datagrid/src/js/default_config.js b/packages/perspective-viewer-datagrid/src/js/default_config.js new file mode 100644 index 0000000000..7c733094e3 --- /dev/null +++ b/packages/perspective-viewer-datagrid/src/js/default_config.js @@ -0,0 +1,54 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +/** + * Gets the default column configurations used for styling. + * @returns The default configuration per type. + */ +export default function getDefaultConfig() { + const get_type_default = (column_type) => { + let type_default; + if (column_type === "integer" || column_type === "float") { + type_default = { + fg_gradient: 0, + pos_fg_color: this.model._pos_fg_color[0], + neg_fg_color: this.model._neg_fg_color[0], + number_fg_mode: "color", + bg_gradient: 0, + pos_bg_color: this.model._pos_bg_color[0], + neg_bg_color: this.model._neg_bg_color[0], + number_bg_mode: "disabled", + fixed: column_type === "float" ? 2 : 0, + }; + } else { + // date, datetime, string, boolean + type_default = { + color: this.model._color[0], + bg_color: this.model._color[0], + }; + } + return type_default; + }; + + let default_config = {}; + for (let val of [ + "string", + "float", + "integer", + "bool", + "date", + "datetime", + ]) { + default_config[val] = get_type_default(val); + } + return default_config; +} diff --git a/packages/perspective-viewer-datagrid/src/js/event_handlers/header_click.js b/packages/perspective-viewer-datagrid/src/js/event_handlers/header_click.js index 7beda0d814..92a8b5aac4 100644 --- a/packages/perspective-viewer-datagrid/src/js/event_handlers/header_click.js +++ b/packages/perspective-viewer-datagrid/src/js/event_handlers/header_click.js @@ -11,10 +11,9 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { sortHandler } from "./sort.js"; -import { activate_plugin_menu } from "../style_menu.js"; import { expandCollapseHandler } from "./expand_collapse.js"; -export async function mousedown_listener(regularTable, event) { +export async function mousedown_listener(regularTable, viewer, event) { if (event.which !== 1) { return; } @@ -38,27 +37,9 @@ export async function mousedown_listener(regularTable, event) { } if (target.classList.contains("psp-menu-enabled")) { - target.classList.add("psp-menu-open"); const meta = regularTable.getMeta(target); const column_name = meta.column_header?.[this._config.split_by.length]; - const column_type = this._schema[column_name]; - this._open_column_styles_menu.unshift(meta._virtual_x); - if ( - column_type === "string" || - column_type === "date" || - column_type === "datetime" - ) { - activate_plugin_menu.call(this, regularTable, target); - } else { - const [min, max] = await this._view.get_min_max(column_name); - let bound = Math.max(Math.abs(min), Math.abs(max)); - if (bound > 1) { - bound = Math.round(bound * 100) / 100; - } - - activate_plugin_menu.call(this, regularTable, target, bound); - } - + await viewer.toggleColumnSettings(column_name); event.preventDefault(); event.stopImmediatePropagation(); } else if (target.classList.contains("psp-sort-enabled")) { diff --git a/packages/perspective-viewer-datagrid/src/js/model/create.js b/packages/perspective-viewer-datagrid/src/js/model/create.js index 0d2e7d7456..52e04c5993 100644 --- a/packages/perspective-viewer-datagrid/src/js/model/create.js +++ b/packages/perspective-viewer-datagrid/src/js/model/create.js @@ -129,7 +129,6 @@ export async function createModel(regular, table, view, extend = {}) { _num_rows: num_rows, _schema, _ids: [], - _open_column_styles_menu: [], _plugin_background, _color, _pos_fg_color, diff --git a/packages/perspective-viewer-datagrid/src/js/plugin/activate.js b/packages/perspective-viewer-datagrid/src/js/plugin/activate.js index 61454c6307..dbb5faf8ea 100644 --- a/packages/perspective-viewer-datagrid/src/js/plugin/activate.js +++ b/packages/perspective-viewer-datagrid/src/js/plugin/activate.js @@ -10,7 +10,10 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { column_header_style_listener } from "../style_handlers/column_header.js"; +import { + column_header_style_listener, + style_selected_column, +} from "../style_handlers/column_header.js"; import { group_header_style_listener } from "../style_handlers/group_header.js"; import { table_cell_style_listener } from "../style_handlers/table_cell"; import { @@ -68,7 +71,7 @@ export async function activate(view) { this.regular_table.addEventListener( "mousedown", - mousedown_listener.bind(this.model, this.regular_table) + mousedown_listener.bind(this.model, this.regular_table, viewer) ); // Row selection @@ -153,6 +156,24 @@ export async function activate(view) { ) ); + // viewer event listeners + viewer.addEventListener( + "perspective-toggle-column-settings", + (event) => { + style_selected_column( + this.regular_table, + event.detail.column_name + ); + if (!event.detail.open) { + this.model._column_settings_selected_column = null; + return; + } + + this.model._column_settings_selected_column = + event.detail.column_name; + } + ); + this._initialized = true; } else { await createModel.call( diff --git a/packages/perspective-viewer-datagrid/src/js/plugin/restore.js b/packages/perspective-viewer-datagrid/src/js/plugin/restore.js index 589413de30..75f4ae0d1c 100644 --- a/packages/perspective-viewer-datagrid/src/js/plugin/restore.js +++ b/packages/perspective-viewer-datagrid/src/js/plugin/restore.js @@ -68,13 +68,6 @@ export function restore(token) { } const datagrid = this.regular_table; - try { - datagrid._resetAutoSize(); - } catch (e) { - // Do nothing; this may fail if no auto size info has been read. - // TODO fix this regular-table API - } - restore_column_size_overrides.call(this, overrides, true); datagrid[PRIVATE_PLUGIN_SYMBOL] = token.columns; } diff --git a/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js b/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js index be7b26faf0..1b8e225d53 100644 --- a/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js +++ b/packages/perspective-viewer-datagrid/src/js/style_handlers/column_header.js @@ -18,12 +18,61 @@ function get_psp_type(metadata) { } } +export function style_selected_column(regularTable, selectedColumn) { + const group_header_trs = Array.from( + regularTable.children[0].children[0].children + ); + const len = group_header_trs.length; + if (len <= 1) { + group_header_trs[0]?.setAttribute("id", "psp-column-edit-buttons"); + } else { + group_header_trs.forEach((tr, i) => { + let id = + i === len - 2 + ? "psp-column-titles" + : i === len - 1 + ? "psp-column-edit-buttons" + : null; + id ? tr.setAttribute("id", id) : tr.removeAttribute("id"); + }); + } + + const settings_open = + regularTable.parentElement.parentElement.hasAttribute("settings"); + if (settings_open) { + // if settings_open, you will never have less than 2 trs but possibly more e.g. with group-by. + // edit and title are guaranteed to be the last two rows + let titles = Array.from(group_header_trs[len - 2].children); + let editBtns = Array.from(group_header_trs[len - 1].children); + if (titles && editBtns) { + // clear any sticky styles from tr changes + group_header_trs.slice(0, len - 2).forEach((tr) => { + Array.from(tr.children).forEach((th) => { + th.classList.toggle("psp-menu-open", false); + }); + }); + let zipped = titles.map((title, i) => [title, editBtns[i]]); + zipped.forEach(([title, editBtn]) => { + let open = title.innerText === selectedColumn; + title.classList.toggle("psp-menu-open", open); + editBtn.classList.toggle("psp-menu-open", open); + }); + } + } +} + export function column_header_style_listener(regularTable) { let group_header_trs = Array.from( regularTable.children[0].children[0].children ); if (group_header_trs.length > 0) { + style_selected_column.call( + this, + regularTable, + this._column_settings_selected_column + ); + let [col_headers] = group_header_trs.splice( this._config.split_by.length, 1 @@ -48,6 +97,7 @@ export function column_header_style_listener(regularTable) { } function style_column_header_row(regularTable, col_headers, is_menu_row) { + // regular header styling const header_depth = regularTable._view_cache.config.row_pivots.length - 1; for (const td of col_headers?.children) { const metadata = regularTable.getMeta(td); @@ -88,10 +138,6 @@ function style_column_header_row(regularTable, col_headers, is_menu_row) { const is_datetime = type === "datetime"; td.classList.toggle("psp-align-right", is_numeric); td.classList.toggle("psp-align-left", !is_numeric); - td.classList.toggle( - "psp-menu-open", - this._open_column_styles_menu[0] === metadata._virtual_x - ); td.classList.toggle( "psp-menu-enabled", diff --git a/packages/perspective-viewer-datagrid/src/js/style_menu.js b/packages/perspective-viewer-datagrid/src/js/style_menu.js deleted file mode 100644 index 639783cf85..0000000000 --- a/packages/perspective-viewer-datagrid/src/js/style_menu.js +++ /dev/null @@ -1,150 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { make_color_record } from "./color_utils.js"; -import { PRIVATE_PLUGIN_SYMBOL } from "./model"; - -export function activate_plugin_menu(regularTable, target, column_max) { - const target_meta = regularTable.getMeta(target); - const column_name = target_meta.column_header[this._config.split_by.length]; - const column_type = this._schema[column_name]; - const is_numeric = column_type === "integer" || column_type === "float"; - const MENU = document.createElement( - `perspective-${ - { - float: "number", - integer: "number", - string: "string", - date: "date", - datetime: "datetime", - }[column_type] - }-column-style` - ); - - let default_config; - if (is_numeric) { - default_config = { - fg_gradient: column_max, - pos_fg_color: this._pos_fg_color[0], - neg_fg_color: this._neg_fg_color[0], - number_fg_mode: "color", - bg_gradient: column_max, - pos_bg_color: this._pos_bg_color[0], - neg_bg_color: this._neg_bg_color[0], - number_bg_mode: "disabled", - }; - } else { - // date, datetime, string, boolean - default_config = { - color: this._color[0], - bg_color: this._color[0], - }; - } - - if ( - column_type === "string" || - column_type === "date" || - column_type === "datetime" - ) { - // do nothing - } else if (column_type === "float") { - default_config.fixed = 2; - } else if (column_type === "integer") { - default_config.fixed = 0; - } else { - this._open_column_styles_menu.pop(); - target.classList.remove("psp-menu-open"); - return; - } - - const scroll_handler = () => MENU.blur(); - const update_handler = (event) => { - const config = event.detail; - if (config.pos_fg_color) { - config.pos_fg_color = make_color_record(config.pos_fg_color); - config.neg_fg_color = make_color_record(config.neg_fg_color); - } - - if (config.pos_bg_color) { - config.pos_bg_color = make_color_record(config.pos_bg_color); - config.neg_bg_color = make_color_record(config.neg_bg_color); - } - - if (config.color) { - config.color = make_color_record(config.color); - } - - if (config.bg_color) { - config.bg_color = make_color_record(config.bg_color); - } - - regularTable[PRIVATE_PLUGIN_SYMBOL] = - regularTable[PRIVATE_PLUGIN_SYMBOL] || {}; - regularTable[PRIVATE_PLUGIN_SYMBOL][column_name] = config; - regularTable.draw({ preserve_width: true }); - regularTable.parentElement.parentElement.dispatchEvent( - new Event("perspective-config-update") - ); - }; - - const blur_handler = async () => { - regularTable.removeEventListener( - "regular-table-scroll", - scroll_handler - ); - - MENU.removeEventListener( - "perspective-column-style-change", - update_handler - ); - - MENU.removeEventListener("blur", blur_handler); - const popped = this._open_column_styles_menu.pop(); - regularTable.parentElement.parentElement.dispatchEvent( - new Event("perspective-config-update") - ); - - if (popped !== this._open_column_styles_menu[0]) { - target.classList.remove("psp-menu-open"); - } - - MENU.destroy(); - }; - - MENU.addEventListener("perspective-column-style-change", update_handler); - MENU.addEventListener("blur", blur_handler); - regularTable.addEventListener("regular-table-scroll", scroll_handler); - - // Get the current column style config - const pset = regularTable[PRIVATE_PLUGIN_SYMBOL] || {}; - const config = Object.assign( - {}, - (pset[column_name] = pset[column_name] || {}) - ); - - if (config.pos_fg_color || config.pos_bg_color) { - config.pos_fg_color = config.pos_fg_color?.[0]; - config.neg_fg_color = config.neg_fg_color?.[0]; - config.pos_bg_color = config.pos_bg_color?.[0]; - config.neg_bg_color = config.neg_bg_color?.[0]; - } - - if (config.color) { - config.color = config.color[0]; - } - - if (config.bg_color) { - config.bg_color = config.bg_color[0]; - } - - MENU.open(target, config, default_config); -} diff --git a/packages/perspective-viewer-datagrid/test/js/column_style.spec.js b/packages/perspective-viewer-datagrid/test/js/column_style.spec.js index c5a24932a7..120cb8a35b 100644 --- a/packages/perspective-viewer-datagrid/test/js/column_style.spec.js +++ b/packages/perspective-viewer-datagrid/test/js/column_style.spec.js @@ -13,20 +13,6 @@ import { test, expect } from "@playwright/test"; import { compareContentsToSnapshot } from "@finos/perspective-test"; -async function get_contents( - page, - selector = "perspective-viewer perspective-viewer-datagrid regular-table", - shadow = false -) { - return await page.evaluate( - async ({ selector, shadow }) => { - const viewer = document.querySelector(selector); - return (shadow ? viewer.shadowRoot : viewer).innerHTML || "MISSING"; - }, - { selector, shadow } - ); -} - async function test_column(page, selector, selector2) { const { x, y } = await page.evaluate(async (selector) => { const viewer = document.querySelector("perspective-viewer"); @@ -49,13 +35,14 @@ async function test_column(page, selector, selector2) { }, selector); await page.mouse.click(x, y); - const style_menu = await page.waitForSelector( - `perspective-${selector2}-column-style` - ); + const column_style_selector = `#column-style-container.${selector2}-column-style-container`; + await page.waitForSelector(column_style_selector); await new Promise((x) => setTimeout(x, 3000)); - return get_contents(page, ` perspective-${selector2}-column-style`, true); + return await page + .locator(`perspective-viewer ${column_style_selector}`) + .innerHTML(); } test.describe("Column Style Tests", () => { @@ -88,6 +75,12 @@ test.describe("Column Style Tests", () => { viewer.addEventListener("perspective-config-update", (evt) => { window.__events__.push(evt); }); + viewer.addEventListener( + "perspective-column-style-change", + (evt) => { + window.__events__.push(evt); + } + ); // Find the column config menu button const header_button = viewer.querySelector( @@ -109,12 +102,12 @@ test.describe("Column Style Tests", () => { // Await the style menu existing on the page const style_menu = await page.waitForSelector( - "perspective-number-column-style" + "#column-style-container" ); const { x: xx, y: yy } = await page.evaluate(async (style_menu) => { // Find the 'bar' button - const bar_button = style_menu.shadowRoot.querySelector( + const bar_button = style_menu.querySelector( '#radio-list-1[name="foreground-list"]' ); @@ -139,7 +132,7 @@ test.describe("Column Style Tests", () => { }); // Expect 1 event - expect(count).toEqual(1); + expect(count).toEqual(2); }); test("Column style menu opens for numeric columns", async ({ page }) => { diff --git a/rust/perspective-viewer/src/less/column-settings-panel.less b/rust/perspective-viewer/src/less/column-settings-panel.less new file mode 100644 index 0000000000..431541226c --- /dev/null +++ b/rust/perspective-viewer/src/less/column-settings-panel.less @@ -0,0 +1,32 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +@import (reference) url(./column-selector.less); +:host { + #column_settings_icon:before { + @include icon; + height: 14px; + width: 14px; + -webkit-mask-image: var(--column-settings-icon--mask-image); + mask-image: var(--column-settings-icon--mask-image); + } + #column_settings_sidebar { + .item_title { + font-size: 12px; + margin: 8px; + margin-bottom: 0px; + } + .style_contents { + margin: 8px; + } + } +} diff --git a/rust/perspective-viewer/src/less/column-style.less b/rust/perspective-viewer/src/less/column-style.less index 1a1e749d49..fc22ad89e5 100644 --- a/rust/perspective-viewer/src/less/column-style.less +++ b/rust/perspective-viewer/src/less/column-style.less @@ -20,225 +20,221 @@ } :host { - position: fixed; - z-index: 10000; - padding: 8px 12px; - outline: none; - background-color: #ffffff; - font-size: 12px; - border: inherit; - user-select: none; - - select { - height: auto; - padding-bottom: 2px; - border-bottom: 1px solid var(--input--border-color, #ccc); - } - #column-style-container { - margin-bottom: 4px; - } - - label { - font-size: 8px; - width: 100%; - } - - input[type="checkbox"] { - float: left; - } - - input.parameter { - width: 80px; - background: none; - color: inherit; - border: 0px solid transparent; + padding: 8px 12px; outline: none; - } - - input.parameter[type="number"] { - flex: 1 1 auto; - text-align: right; - border-bottom-width: 1px; - border-color: var( - --input--border-color, - var(--inactive--color, inherit) - ); - } - - input[type="number"]::-webkit-inner-spin-button, - input[type="number"]::-webkit-outer-spin-button { - opacity: 1; - background: transparent; - background-color: transparent; - } - - .column-style-label { - display: flex; - padding: 4px 0px; - } - - .indent { - margin-left: 24px; - } - - input[type="checkbox"], - & > div > div > span:first-child { - width: 24px; - margin: 0; - } + font-size: 12px; + border: inherit; + user-select: none; - input[type="checkbox"] { - appearance: none; + &.no-style { + font-style: italic; + background-color: #8b868045; + } - &:checked:after { - -webkit-mask-image: var(--column-checkbox-on--mask-image); - mask-image: var(--column-checkbox-on--mask-image); + select { + height: auto; + padding-bottom: 2px; + border-bottom: 1px solid var(--input--border-color, #ccc); } - &[disabled]:after { - opacity: 0.2s; + label { + font-size: 8px; + width: 100%; } - &:after { - @include icon; - height: 13px; - width: 13px; - -webkit-mask-image: var(--column-checkbox-off--mask-image); - mask-image: var(--column-checkbox-off--mask-image); + input.parameter { + background: none; + color: inherit; + border: 0px solid transparent; + outline: none; } - &:hover:after { - -webkit-mask-image: var(--column-checkbox-hover--mask-image); - mask-image: var(--column-checkbox-hover--mask-image); + input.parameter[type="number"] { + flex: 1 1 auto; + text-align: right; + border-bottom-width: 1px; + border-color: var( + --input--border-color, + var(--inactive--color, inherit) + ); } - } - input[type="radio"] { - appearance: none; + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + opacity: 1; + background: transparent; + background-color: transparent; + } - &:checked:after { - -webkit-mask-image: var(--column-radio-on--mask-image); - mask-image: var(--column-radio-on--mask-image); + .column-style-label { + display: flex; + padding: 4px 0px; } - &:after { - @include icon; - width: 10px; - height: 10px; - -webkit-mask-image: var(--column-radio-off--mask-image); - mask-image: var(--column-radio-off--mask-image); + .indent { + margin-left: 24px; } - &:hover:after { - -webkit-mask-image: var(--column-radio-hover--mask-image); - mask-image: var(--column-radio-hover--mask-image); + input[type="checkbox"], + & > div > div > span:first-child { + width: 24px; + margin: 0; } - } - div.section { - margin-right: 6px; - margin-bottom: 8px; - flex: 1 1 100%; - } + input[type="checkbox"] { + float: left; + appearance: none; + + &:checked:before { + -webkit-mask-image: var(--column-checkbox-on--mask-image); + mask-image: var(--column-checkbox-on--mask-image); + } + + &[disabled]:before { + opacity: 0.2s; + } + + &:before { + @include icon; + height: 13px; + width: 13px; + -webkit-mask-image: var(--column-checkbox-off--mask-image); + mask-image: var(--column-checkbox-off--mask-image); + } + + &:hover:before { + -webkit-mask-image: var(--column-checkbox-hover--mask-image); + mask-image: var(--column-checkbox-hover--mask-image); + } + } - div.inner_section { - margin-top: 4px; - width: 0px; - margin-bottom: 8px; - flex: 1 1 100%; - } + input[type="radio"] { + appearance: none; + + &:checked:before { + -webkit-mask-image: var(--column-radio-on--mask-image); + mask-image: var(--column-radio-on--mask-image); + } + + &:before { + @include icon; + width: 10px; + height: 10px; + -webkit-mask-image: var(--column-radio-off--mask-image); + mask-image: var(--column-radio-off--mask-image); + } + + &:hover:before { + -webkit-mask-image: var(--column-radio-hover--mask-image); + mask-image: var(--column-radio-hover--mask-image); + } + } - div.row { - display: flex; - align-items: center; - flex-wrap: wrap; - } + div.section { + margin-right: 6px; + margin-bottom: 8px; + flex: 1 1 100%; + } - input[type="color"] { - width: 36px; - height: 36px; - cursor: pointer; - padding: 0; - margin-right: 4px; - font-family: inherit; - overflow: hidden; - border-radius: 3px; - - &:before { - position: absolute; - font-family: var(--button--font-family, inherit); + div.inner_section { margin-top: 4px; - margin-left: 12px; - font-size: 20px; - content: var(--column-style-pos-color--content, "+"); - color: white; + width: 0px; + margin-bottom: 8px; + flex: 1 1 100%; } - &#neg-color-param:before { - content: var(--column-style-neg-color--content, "-"); + div.row { + display: flex; + align-items: center; + flex-wrap: wrap; } - } - ::-webkit-color-swatch-wrapper { - padding: 0; - } - - ::-webkit-color-swatch { - border: 0; - border-radius: 0; - } - - ::-moz-color-swatch, - ::-moz-focus-inner { - border: 0; - } + input[type="color"] { + width: 36px; + height: 36px; + cursor: pointer; + padding: 0; + margin-right: 4px; + font-family: inherit; + overflow: hidden; + border-radius: 3px; + + &:before { + position: absolute; + font-family: var(--button--font-family, inherit); + margin-top: 4px; + margin-left: 12px; + font-size: 20px; + content: var(--column-style-pos-color--content, "+"); + color: white; + } + + &#neg-color-param:before { + content: var(--column-style-neg-color--content, "-"); + } + } - ::-moz-focus-inner { - padding: 0; - } + ::-webkit-color-swatch-wrapper { + padding: 0; + } - .operator { - font-family: "Roboto Mono", monospace; - white-space: pre; - } + ::-webkit-color-swatch { + border: 0; + border-radius: 0; + } - input[disabled]:after { - opacity: 0.5; - } + ::-moz-color-swatch, + ::-moz-focus-inner { + border: 0; + } - input.parameter[disabled] { - opacity: 0.5; - } + ::-moz-focus-inner { + padding: 0; + } - input[type="checkbox"]:not(:disabled) { - cursor: pointer; - } + .operator { + font-family: "Roboto Mono", monospace; + white-space: pre; + } - button#datetime_format { - appearance: none; - background: none; - color: inherit; - border: 1px solid var(--inactive--color); - border-radius: 3px; - font-family: inherit; - padding: 6px; - font-size: 10px; - width: 100%; - cursor: pointer; + input[disabled]:before { + opacity: 0.5; + } - &:before { - content: attr(data-title); + input.parameter[disabled] { + opacity: 0.5; } - &:hover { - border-color: var(--icon--color); - background-color: var(--icon--color); - color: var(--plugin--background); + input[type="checkbox"]:not(:disabled) { + cursor: pointer; } - &:hover:before { - content: attr(data-title-hover); + button#datetime_format { + appearance: none; + background: none; + color: inherit; + border: 1px solid var(--inactive--color); + border-radius: 3px; + font-family: inherit; + padding: 6px; + font-size: 10px; + width: 100%; + cursor: pointer; + + &:before { + content: attr(data-title); + } + + &:hover { + border-color: var(--icon--color); + background-color: var(--icon--color); + color: var(--plugin--background); + } + + &:hover:before { + content: attr(data-title-hover); + } } } } diff --git a/rust/perspective-viewer/src/less/containers/tabs.less b/rust/perspective-viewer/src/less/containers/tabs.less new file mode 100644 index 0000000000..471219184c --- /dev/null +++ b/rust/perspective-viewer/src/less/containers/tabs.less @@ -0,0 +1,63 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +:host { + .tab-gutter { + border-color: var(--inactive--color,#6e6e6e); + display: flex; + + .tab.tab-padding { + flex: 1; + cursor: unset; + .tab-title { + border-right: none; + } + .tab-border { + border-right: none; + } + } + + .tab { + //TODO: This needs to be a variable color. Which one? + background: rgba(0,0,0,0.125); + border-right: 1px solid var(--inactive--color,#6e6e6e); + user-select: none; + cursor: pointer; + + .tab-title { + font-size: 12px; + padding: 10px; + border-bottom: 1px solid var(--inactive--color,#6e6e6e); + } + .tab-border { + height: 2px; + width: 100%; + background-color: var(--inactive--color,#6e6e6e); + margin-top: 1px; + } + + &.selected { + background: unset; + border-bottom: 1px transparent; + + .tab-title { + border-bottom: 1px transparent; + border-right: none; + } + .tab-border { + background-color: transparent; + border-right: none; + } + } + } + } +} diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 1bf0b9f24f..2913c2dec8 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -275,7 +275,8 @@ max-width: 300px; } - #expr_panel_header { + #expr_panel_header, + .sidebar_header { min-height: 48px; overflow: hidden; display: flex; @@ -284,7 +285,8 @@ border-bottom: 1px solid var(--inactive--color, #6e6e6e); } - #expr_panel_header_title { + #expr_panel_header_title, + .sidebar_header_title { padding-left: 9px; font-size: 12px; overflow: hidden; @@ -293,14 +295,16 @@ margin-right: 30px; } - #expr_panel_border { + #expr_panel_border, + .sidebar_border { height: 2px; width: 100%; background-color: var(--inactive--color, #6e6e6e); margin-top: 1px; } - .expr_editor_column { + .expr_editor_column, + .sidebar_column { z-index: 2; width: 100%; min-width: 250px; diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index 8c29a91f1e..6da31143a5 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -34,8 +34,8 @@ use self::config_selector::ConfigSelector; use self::inactive_column::*; use super::containers::scroll_panel::*; use super::containers::split_panel::{Orientation, SplitPanel}; -use super::expression_panel_sidebar::EditorState; use super::style::LocalStyle; +use super::viewer::ColumnLocator; use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; use crate::model::*; @@ -50,10 +50,10 @@ pub struct ColumnSelectorProps { pub renderer: Renderer, pub dragdrop: DragDrop, - pub on_open_expr_panel: Callback>, + pub on_open_expr_panel: Callback, /// This is passed to the add_expression_button for styling. - pub editor_state: EditorState, + pub selected_column: Option, #[prop_or_default] pub on_resize: Option>>, @@ -320,7 +320,7 @@ impl Component for ColumnSelector { + selected_column={ ctx.props().selected_column.clone() }> }; diff --git a/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs index 35537a519d..d9e6dc37b6 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs @@ -19,6 +19,7 @@ use super::aggregate_selector::*; use super::expression_toolbar::*; use super::InPlaceColumn; use crate::components::column_selector::EmptyColumn; +use crate::components::viewer::ColumnLocator; use crate::config::*; use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; @@ -40,7 +41,7 @@ pub struct ActiveColumnProps { pub ondragenter: Callback<()>, pub ondragend: Callback<()>, pub onselect: Callback<()>, - pub on_open_expr_panel: Callback>, + pub on_open_expr_panel: Callback, #[prop_or_default] pub is_aggregated: bool, @@ -366,12 +367,11 @@ impl Component for ActiveColumn { } - if is_expression { - - } + diff --git a/rust/perspective-viewer/src/rust/components/column_selector/add_expression_button.rs b/rust/perspective-viewer/src/rust/components/column_selector/add_expression_button.rs index 0ae1b41ad8..c4ebe8a981 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/add_expression_button.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/add_expression_button.rs @@ -12,12 +12,11 @@ use yew::prelude::*; -use crate::components::expression_panel_sidebar::EditorState; - +use super::ColumnLocator; #[derive(Clone, PartialEq, Properties)] pub struct AddExpressionButtonProps { - pub on_open_expr_panel: Callback>, - pub editor_state: EditorState, + pub on_open_expr_panel: Callback, + pub selected_column: Option, } /// `onmouseover` is triggered incorrectly on the `DragTarget` of a @@ -48,8 +47,8 @@ pub fn AddExpressionButton(p: &AddExpressionButtonProps) -> Html { is_mouseover.setter(), ); - let onmousedown = p.on_open_expr_panel.reform(|_| None); - let class = if *is_mouseover || matches!(p.editor_state, EditorState::NewExpr) { + let onmousedown = p.on_open_expr_panel.reform(|_| ColumnLocator::Expr(None)); + let class = if *is_mouseover || matches!(p.selected_column, Some(ColumnLocator::Expr(None))) { classes!("dragdrop-hover") } else { classes!() diff --git a/rust/perspective-viewer/src/rust/components/column_selector/expression_toolbar.rs b/rust/perspective-viewer/src/rust/components/column_selector/expression_toolbar.rs index 7e9a899970..4e19d23dbd 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/expression_toolbar.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/expression_toolbar.rs @@ -12,18 +12,32 @@ use yew::prelude::*; +use super::ColumnLocator; + #[derive(PartialEq, Clone, Properties)] pub struct ExprEditButtonProps { pub name: String, - pub on_open_expr_panel: Callback>, + pub is_expression: bool, + pub on_open_expr_panel: Callback, } +// TODO: Move this logic to ColumnSettingsSidebar +// Button should just pass the name to the on_open callback, and the sidebar +// should render it. generally, rendering logic should live on the closest +// component /// A button that goes into a column-list for a custom expression /// when pressed, it opens up the expression editor side-panel. #[function_component] pub fn ExprEditButton(p: &ExprEditButtonProps) -> Html { let onmousedown = yew::use_callback( - |_, p| p.on_open_expr_panel.emit(Some(p.name.clone())), + |_, p| { + let name = if p.is_expression { + ColumnLocator::Expr(Some(p.name.clone())) + } else { + ColumnLocator::Plain(p.name.clone()) + }; + p.on_open_expr_panel.emit(name) + }, p.clone(), ); html! { diff --git a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs index de9e2457d8..88064e900d 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs @@ -15,6 +15,7 @@ use web_sys::*; use yew::prelude::*; use super::expression_toolbar::*; +use crate::components::viewer::ColumnLocator; use crate::config::*; use crate::dragdrop::*; use crate::js::plugin::*; @@ -34,7 +35,7 @@ pub struct InactiveColumnProps { pub renderer: Renderer, pub ondragend: Callback<()>, pub onselect: Callback<()>, - pub on_open_expr_panel: Callback>, + pub on_open_expr_panel: Callback, } impl PartialEq for InactiveColumnProps { @@ -209,6 +210,7 @@ impl Component for InactiveColumn { } diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs new file mode 100644 index 0000000000..3d435c1126 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs @@ -0,0 +1,138 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +mod attributes_tab; +mod style_tab; + +use std::fmt::Display; + +use yew::{function_component, html, Callback, Html, Properties}; + +use super::containers::tablist::Tab; +use super::viewer::ColumnLocator; +use crate::components::column_settings_sidebar::attributes_tab::AttributesTab; +use crate::components::column_settings_sidebar::style_tab::StyleTab; +use crate::components::containers::sidebar::Sidebar; +use crate::components::containers::tablist::TabList; +use crate::components::expression_editor::get_new_column_name; +use crate::components::style::LocalStyle; +use crate::presentation::Presentation; +use crate::renderer::Renderer; +use crate::session::Session; +use crate::{clone, css, html_template}; + +#[derive(Debug, Default, Clone, PartialEq)] +pub enum ColumnSettingsTab { + #[default] + Attributes, + Style, +} +impl Display for ColumnSettingsTab { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{self:?}")) + } +} +impl Tab for ColumnSettingsTab {} + +#[derive(Clone, Properties)] +pub struct ColumnSettingsProps { + pub selected_column: ColumnLocator, + pub on_close: Callback<()>, + pub session: Session, + pub renderer: Renderer, + pub presentation: Presentation, +} +impl PartialEq for ColumnSettingsProps { + fn eq(&self, other: &Self) -> bool { + self.selected_column == other.selected_column + } +} + +#[function_component] +pub fn ColumnSettingsSidebar(p: &ColumnSettingsProps) -> Html { + let column_name = match p.selected_column.clone() { + ColumnLocator::Expr(Some(name)) | ColumnLocator::Plain(name) => name, + ColumnLocator::Expr(None) => get_new_column_name(&p.session), + }; + + let column_type = p.session.metadata().get_column_view_type(&column_name); + let is_active = column_type.is_some(); + + let mut tabs = vec![]; + + // Eventually the Attributes tab will have more properties than + // just the expression editor. Once that happens, stop hiding it. + if matches!(p.selected_column, ColumnLocator::Expr(_)) { + tabs.push(ColumnSettingsTab::Attributes); + } + if !matches!(p.selected_column, ColumnLocator::Expr(None)) && is_active { + tabs.push(ColumnSettingsTab::Style); + } + + let title = format!("Editing ‘{column_name}’..."); + + clone!( + p.selected_column, + p.on_close, + p.session, + p.renderer, + p.presentation, + column_name + ); + let match_fn = Callback::from(move |tab| { + clone!( + selected_column, + on_close, + session, + renderer, + presentation, + column_name + ); + match tab { + ColumnSettingsTab::Attributes => { + let selected_column = match selected_column { + ColumnLocator::Expr(s) => s, + _ => panic!("Tried to open Attributes tab for non-expression column!"), + }; + html! { + + } + } + ColumnSettingsTab::Style => html! { + + }, + } + }); + + html_template! { + + + {tabs} {match_fn} /> + + + } +} diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs new file mode 100644 index 0000000000..ca4e43b2dd --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs @@ -0,0 +1,122 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use wasm_bindgen::JsValue; +use yew::{function_component, html, Callback, Html, Properties}; + +use crate::components::expression_editor::ExpressionEditor; +use crate::config::ViewConfigUpdate; +use crate::model::UpdateAndRender; +use crate::renderer::Renderer; +use crate::session::Session; +use crate::utils::ApiFuture; +use crate::{derive_model, html_template}; + +#[derive(PartialEq, Clone, Properties)] +pub struct AttributesTabProps { + pub selected_column: Option, + pub on_close: Callback<()>, + pub session: Session, + pub renderer: Renderer, +} +derive_model!(Renderer, Session for AttributesTabProps); + +#[function_component] +pub fn AttributesTab(p: &AttributesTabProps) -> Html { + let is_validating = yew::use_state_eq(|| false); + let on_save = yew::use_callback( + |v, p| { + match &p.selected_column { + None => save_expr(v, p), + Some(alias) => update_expr(alias, &v, p), + } + + p.on_close.emit(()); + }, + p.clone(), + ); + + let on_validate = yew::use_callback( + |b, validating| { + validating.set(b); + }, + is_validating.setter(), + ); + + let on_delete = yew::use_callback( + |(), p| { + if let Some(ref s) = p.selected_column { + delete_expr(s, p); + } + + p.on_close.emit(()); + }, + p.clone(), + ); + + html_template! { +
+
{"Expression Editor"}
+ +
+ } +} +fn update_expr(name: &str, new_expression: &JsValue, props: &AttributesTabProps) { + let n = name.to_string(); + let exp = new_expression.clone(); + let sesh = props.session.clone(); + let props = props.clone(); + ApiFuture::spawn(async move { + let update = sesh.create_replace_expression_update(&n, &exp).await; + props.update_and_render(update).await?; + Ok(()) + }); +} + +fn save_expr(expression: JsValue, props: &AttributesTabProps) { + let task = { + let expression = expression.as_string().unwrap(); + let mut expressions = props.session.get_view_config().expressions.clone(); + expressions.retain(|x| x != &expression); + expressions.push(expression); + props.update_and_render(ViewConfigUpdate { + expressions: Some(expressions), + ..Default::default() + }) + }; + + ApiFuture::spawn(task); +} + +fn delete_expr(expr_name: &str, props: &AttributesTabProps) { + let session = &props.session; + let expression = session + .metadata() + .get_expression_by_alias(expr_name) + .unwrap(); + + let mut expressions = session.get_view_config().expressions.clone(); + expressions.retain(|x| x != &expression); + let config = ViewConfigUpdate { + expressions: Some(expressions), + ..ViewConfigUpdate::default() + }; + + let task = props.update_and_render(config); + ApiFuture::spawn(task); +} diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs new file mode 100644 index 0000000000..50078a25c9 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs @@ -0,0 +1,245 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use std::collections::HashMap; +use std::fmt::Debug; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::{JsCast, JsValue}; +use yew::{function_component, html, Callback, Html, Properties}; + +use crate::components::datetime_column_style::DatetimeColumnStyle; +use crate::components::number_column_style::NumberColumnStyle; +use crate::components::string_column_style::StringColumnStyle; +use crate::components::style::LocalStyle; +use crate::config::{ + DatetimeColumnStyleConfig, DatetimeColumnStyleDefaultConfig, NumberColumnStyleConfig, + NumberColumnStyleDefaultConfig, StringColumnStyleConfig, StringColumnStyleDefaultConfig, Type, +}; +use crate::presentation::Presentation; +use crate::renderer::Renderer; +use crate::session::Session; +use crate::utils::{ApiFuture, JsValueSerdeExt}; +use crate::{clone, css, html_template}; + +#[derive(Clone, PartialEq, Properties)] +pub struct StyleTabProps { + pub session: Session, + pub renderer: Renderer, + pub presentation: Presentation, + pub column_name: String, +} + +#[derive(Serialize, Deserialize, Debug)] +struct DefaultConfig { + string: serde_json::Value, + datetime: serde_json::Value, + date: serde_json::Value, + integer: serde_json::Value, + float: serde_json::Value, + bool: serde_json::Value, +} + +#[derive(Serialize, Deserialize, Debug)] +struct Config { + columns: HashMap, +} + +/// This function sends the config to the plugin using its `restore` method +fn send_config( + renderer: &Renderer, + presentation: &Presentation, + view: JsValue, + column_name: String, + column_config: T, +) { + let current_config = get_config(renderer); + let elem = renderer.get_active_plugin().unwrap(); + if let Some((mut current_config, _)) = current_config { + current_config + .columns + .insert(column_name, serde_json::to_value(column_config).unwrap()); + let js_config = JsValue::from_serde_ext(¤t_config).unwrap(); + elem.restore(&js_config); + ApiFuture::spawn(async move { + let view = view.unchecked_into(); + elem.update(&view, None, None, false).await + }); + // send a config update event in case we need to listen for it outside of the + // viewer + presentation.column_settings_updated.emit_all(js_config); + } else { + tracing::warn!("Could not restore and restyle plugin!"); + } +} + +fn jsval_to_type(val: &JsValue) -> Result { + let stringval = js_sys::JSON::stringify(val) + .ok() + .and_then(|s| s.as_string()) + .unwrap_or_default(); + serde_json::from_str(&stringval) +} + +/// This function retrieves the plugin's config using its `save` method. +/// It also introduces the `default_config` field for the plugin. +/// If this field does not exist, the plugin is considered to be unstylable. +fn get_config(renderer: &Renderer) -> Option<(Config, DefaultConfig)> { + let plugin = renderer.get_active_plugin().unwrap(); + let config = plugin.save(); + let default_config = JsValue::from(plugin.default_config()); + let config = jsval_to_type(&config).ok(); + let default_config = jsval_to_type(&default_config).ok(); + config.zip(default_config) +} + +fn get_column_config< + ConfigType: DeserializeOwned + Debug, + DefaultConfigType: DeserializeOwned + Debug, +>( + renderer: &Renderer, + column_name: &str, + ty: Type, +) -> Result<(Option, DefaultConfigType), String> { + get_config(renderer) + .ok_or_else(|| "Could not get_config!".into()) + .and_then(|(mut config, default_config)| { + let current_config = if let Some(config) = config.columns.remove(column_name) { + serde_json::from_value(config) + .map_err(|e| format!("Could not deserialize config with error {e:?}"))? + } else { + None + }; + + let val = match ty { + Type::String => default_config.string, + Type::Datetime => default_config.datetime, + Type::Date => default_config.date, + Type::Integer => default_config.integer, + Type::Float => default_config.float, + Type::Bool => default_config.bool, + }; + serde_json::from_value(val) + .map_err(|e| format!("Could not deserialize default_config with error {e:?}")) + .map(|default_config| (current_config, default_config)) + }) +} + +#[function_component] +pub fn StyleTab(p: &StyleTabProps) -> Html { + let opts = p + .session + .metadata() + .get_column_view_type(&p.column_name) + .zip(p.session.get_view()); + if opts.is_none() { + return html! {}; + } + let (ty, view) = opts.unwrap(); + let view = (*view).clone(); + let title = format!("{} Styling", ty.to_capitalized()); + + clone!(p.renderer, p.presentation, p.column_name); + let opt_html = + match ty { + Type::String => get_column_config::< + StringColumnStyleConfig, + StringColumnStyleDefaultConfig, + >(&renderer, &column_name, ty) + .map(|(config, default_config)| { + let on_change = Callback::from(move |config| { + send_config( + &renderer, + &presentation, + view.clone(), + column_name.clone(), + config, + ); + }); + html_template! { +
{title.clone()}
+
+ +
+ } + }), + Type::Datetime | Type::Date => get_column_config::< + DatetimeColumnStyleConfig, + DatetimeColumnStyleDefaultConfig, + >(&renderer, &column_name, ty) + .map(|(config, default_config)| { + let on_change = Callback::from(move |config| { + send_config( + &renderer, + &presentation, + view.clone(), + column_name.clone(), + config, + ); + }); + html_template! { +
{title.clone()}
+
+ +
+ } + }), + Type::Integer | Type::Float => get_column_config::< + NumberColumnStyleConfig, + NumberColumnStyleDefaultConfig, + >(&renderer, &column_name, ty) + .map(|(config, default_config)| { + let on_change = Callback::from(move |config| { + send_config( + &renderer, + &presentation, + view.clone(), + column_name.clone(), + config, + ); + }); + html_template! { +
{title.clone()}
+
+ +
+ } + }), + _ => Err("Booleans aren't styled yet.".into()), + }; + let inner = if let Ok(html) = opt_html { + html + } else { + // do the tracing logs + tracing::warn!("{}", opt_html.unwrap_err()); + // return the default impl + html_template! { +
{title}
+
+ +
+
{ "No styles available" }
+
+
+ } + }; + + html! { +
{inner}
+ } +} diff --git a/rust/perspective-viewer/src/rust/components/containers/mod.rs b/rust/perspective-viewer/src/rust/components/containers/mod.rs index 5ef45c05b1..f1d5d975d4 100644 --- a/rust/perspective-viewer/src/rust/components/containers/mod.rs +++ b/rust/perspective-viewer/src/rust/components/containers/mod.rs @@ -19,7 +19,9 @@ pub mod radio_list; pub mod radio_list_item; pub mod scroll_panel; pub mod select; +pub mod sidebar; pub mod split_panel; +pub mod tablist; #[cfg(test)] mod tests; diff --git a/rust/perspective-viewer/src/rust/components/containers/sidebar.rs b/rust/perspective-viewer/src/rust/components/containers/sidebar.rs new file mode 100644 index 0000000000..3f1b277fc8 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/containers/sidebar.rs @@ -0,0 +1,65 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use yew::{function_component, html, AttrValue, Callback, Children, Html, Properties}; + +#[derive(PartialEq, Clone, Properties)] +pub struct SidebarProps { + /// The component's children. + pub children: Children, + /// When this callback is called, the sidebar will close + pub on_close: Callback<()>, + pub title: String, + pub id_prefix: String, + pub icon: Option, +} + +/// Sidebars are designed to live in a [SplitPanel] +#[function_component] +pub fn Sidebar(p: &SidebarProps) -> Html { + let id = &p.id_prefix; + html! { + + } +} + +#[derive(PartialEq, Clone, Properties)] +pub struct SidebarCloseButtonProps { + pub on_close_sidebar: Callback<()>, + pub id: AttrValue, +} + +#[function_component] +pub fn SidebarCloseButton(p: &SidebarCloseButtonProps) -> Html { + let onclick = yew::use_callback(|_, cb| cb.emit(()), p.on_close_sidebar.clone()); + let id = &p.id; + html! { + + } +} diff --git a/rust/perspective-viewer/src/rust/components/containers/tablist.rs b/rust/perspective-viewer/src/rust/components/containers/tablist.rs new file mode 100644 index 0000000000..a010769f7a --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/containers/tablist.rs @@ -0,0 +1,100 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use yew::{classes, html, Callback, Component, Html, Properties}; + +use crate::components::style::LocalStyle; +use crate::{css, html_template}; + +pub trait Tab: PartialEq + std::fmt::Display + Clone + Default + 'static {} +impl Tab for String {} +impl Tab for &'static str {} + +#[derive(Properties, Debug, PartialEq)] +pub struct TabListProps { + pub tabs: Vec, + pub match_fn: Callback, +} + +pub enum TabListMsg { + SetSelected(usize), +} + +pub struct TabList { + t: std::marker::PhantomData, + selected_idx: usize, +} + +impl Component for TabList { + type Message = TabListMsg; + type Properties = TabListProps; + + fn create(_ctx: &yew::Context) -> Self { + Self { + t: std::marker::PhantomData, + selected_idx: 0, + } + } + + fn update(&mut self, _ctx: &yew::Context, msg: Self::Message) -> bool { + match msg { + TabListMsg::SetSelected(idx) => { + self.selected_idx = idx; + true + } + } + } + + fn changed(&mut self, _ctx: &yew::Context, _old_props: &Self::Properties) -> bool { + self.selected_idx = 0; + true + } + + fn view(&self, ctx: &yew::Context) -> Html { + let p = ctx.props(); + let gutter_tabs = p + .tabs + .iter() + .enumerate() + .map(|(idx, tab)| { + let class = classes!(vec![ + Some("tab"), + (idx == self.selected_idx).then_some("selected") + ]); + let onclick = ctx.link().callback(move |_| TabListMsg::SetSelected(idx)); + html! { + +
{tab.to_string()}
+
+
+ } + }) + .collect::>(); + + let rendered_child = p + .match_fn + .emit(p.tabs.get(self.selected_idx).cloned().unwrap_or_default()); + html_template! { + +
+ {gutter_tabs} + +
{"\u{00a0}"}
+
+
+
+
+ {rendered_child} +
+ } + } +} diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style.rs index bb11444b31..9de54d9088 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style.rs @@ -15,6 +15,7 @@ mod simple; use std::sync::LazyLock; +use derivative::Derivative; use wasm_bindgen::*; use web_sys::*; use yew::prelude::*; @@ -25,7 +26,7 @@ use super::containers::radio_list_item::RadioListItem; use super::containers::select::*; use super::form::color_selector::*; use super::modal::{ModalLink, SetModalLink}; -use super::style::{LocalStyle, StyleProvider}; +use super::style::LocalStyle; use crate::components::datetime_column_style::custom::DatetimeStyleCustom; use crate::components::datetime_column_style::simple::DatetimeStyleSimple; use crate::config::*; @@ -66,20 +67,20 @@ pub enum DatetimeColumnStyleMsg { ColorChanged(String), } -#[derive(Properties)] +#[derive(Properties, Derivative)] +#[derivative(Debug)] pub struct DatetimeColumnStyleProps { pub enable_time_config: bool, - #[prop_or_default] - pub config: DatetimeColumnStyleConfig, + pub config: Option, - #[prop_or_default] pub default_config: DatetimeColumnStyleDefaultConfig, #[prop_or_default] pub on_change: Callback, #[prop_or_default] + #[derivative(Debug = "ignore")] weak_link: WeakScope, } @@ -90,8 +91,8 @@ impl ModalLink for DatetimeColumnStyleProps { } impl PartialEq for DatetimeColumnStyleProps { - fn eq(&self, other: &Self) -> bool { - self.config == other.config + fn eq(&self, _other: &Self) -> bool { + false } } @@ -101,6 +102,7 @@ impl PartialEq for DatetimeColumnStyleProps { #[derive(Debug)] pub struct DatetimeColumnStyle { config: DatetimeColumnStyleConfig, + default_config: DatetimeColumnStyleDefaultConfig, } impl DatetimeColumnStyle { @@ -117,7 +119,7 @@ impl DatetimeColumnStyle { .config .color .clone() - .unwrap_or_else(|| ctx.props().default_config.color.to_owned()); + .unwrap_or_else(|| self.default_config.color.to_owned()); let color_props = props!(ColorProps { color, on_color }); match &self.config.datetime_color_mode { @@ -145,7 +147,19 @@ impl Component for DatetimeColumnStyle { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); Self { - config: ctx.props().config.clone(), + config: ctx.props().config.clone().unwrap_or_default(), + default_config: ctx.props().default_config.clone(), + } + } + + // Always re-render when config changes. + fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { + let mut new_config = ctx.props().config.clone().unwrap_or_default(); + if self.config != new_config { + std::mem::swap(&mut self.config, &mut new_config); + true + } else { + false } } @@ -230,91 +244,89 @@ impl Component for DatetimeColumnStyle { .link() .callback(|_| DatetimeColumnStyleMsg::TimezoneEnabled); - html! { - - -
+ html_template! { + +
+
+ +
+
+ + + + class="indent" + name="color-radio-list" + disabled={ self.config.datetime_color_mode.is_none() } + selected={ selected_color_mode } + on_change={ color_mode_changed } > + + + value={ DatetimeColorMode::Foreground }> + { foreground_controls } + > + + value={ DatetimeColorMode::Background }> + { background_controls } + > + > +
+ + if ctx.props().enable_time_config {
- +
- - - class="indent" - name="color-radio-list" - disabled={ self.config.datetime_color_mode.is_none() } - selected={ selected_color_mode } - on_change={ color_mode_changed } > - - - value={ DatetimeColorMode::Foreground }> - { foreground_controls } - > - - value={ DatetimeColorMode::Background }> - { background_controls } - > - > + onchange={ on_time_zone_reset } + checked={ self.config.time_zone.is_some() } /> + + + wrapper_class="indent" + values={ ALL_TIMEZONES.iter().cloned().collect::>() } + selected={ self.config.time_zone.as_ref().unwrap_or(&*USER_TIMEZONE).clone() } + on_select={ ctx.link().callback(DatetimeColumnStyleMsg::TimezoneChanged) }> + >
+ } + + if let DatetimeFormatType::Simple(config) = &self.config._format { if ctx.props().enable_time_config { -
- -
-
- - - - wrapper_class="indent" - values={ ALL_TIMEZONES.iter().cloned().collect::>() } - selected={ self.config.time_zone.as_ref().unwrap_or(&*USER_TIMEZONE).clone() } - on_select={ ctx.link().callback(DatetimeColumnStyleMsg::TimezoneChanged) }> - > -
+ } - - if let DatetimeFormatType::Simple(config) = &self.config._format { - if ctx.props().enable_time_config { - - } - - - - } else if let DatetimeFormatType::Custom(config) = &self.config._format { - if ctx.props().enable_time_config { - - } - - - + + + } else if let DatetimeFormatType::Custom(config) = &self.config._format { + if ctx.props().enable_time_config { + } -
- + + + } + +
} } } diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs index 0adfdbac4a..1df897c78a 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs @@ -50,8 +50,8 @@ impl ModalLink for DatetimeStyleCustomProps { } impl PartialEq for DatetimeStyleCustomProps { - fn eq(&self, other: &Self) -> bool { - self.config == other.config + fn eq(&self, _other: &Self) -> bool { + false } } @@ -78,6 +78,17 @@ impl Component for DatetimeStyleCustom { } } + // Always re-render when config changes. + fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { + let mut new_config = ctx.props().config.clone(); + if self.config != new_config { + std::mem::swap(&mut self.config, &mut new_config); + true + } else { + false + } + } + // TODO could be more conservative here with re-rendering fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs index 7a2cef448e..2408dc191a 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs @@ -45,8 +45,8 @@ impl ModalLink for DatetimeStyleSimpleProps { } impl PartialEq for DatetimeStyleSimpleProps { - fn eq(&self, other: &Self) -> bool { - self.config == other.config + fn eq(&self, _other: &Self) -> bool { + false } } @@ -102,6 +102,17 @@ impl Component for DatetimeStyleSimple { } } + // Always re-render when config changes. + fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { + let mut new_config = ctx.props().config.clone(); + if self.config != new_config { + std::mem::swap(&mut self.config, &mut new_config); + true + } else { + false + } + } + fn view(&self, ctx: &Context) -> Html { let on_date_reset = ctx.link().callback(|_| DatetimeStyleSimpleMsg::DateEnabled); let on_time_reset = ctx.link().callback(|_| DatetimeStyleSimpleMsg::TimeEnabled); diff --git a/rust/perspective-viewer/src/rust/components/expression_editor.rs b/rust/perspective-viewer/src/rust/components/expression_editor.rs index 06ed049030..2237d8c11e 100644 --- a/rust/perspective-viewer/src/rust/components/expression_editor.rs +++ b/rust/perspective-viewer/src/rust/components/expression_editor.rs @@ -40,6 +40,17 @@ pub struct ExpressionEditorProps { pub alias: Option, } +pub fn get_new_column_name(session: &Session) -> String { + let mut i = 0; + loop { + i += 1; + let name = format!("New Column {i}"); + if session.metadata().get_column_table_type(&name).is_none() { + return name; + } + } +} + impl ExpressionEditorProps { fn initial_expr(&self) -> Rc { let txt = if let Some(ref alias) = self.alias { @@ -48,15 +59,7 @@ impl ExpressionEditorProps { .get_expression_by_alias(alias) .unwrap_or_default() } else { - let mut i = 1; - let mut name = "New Column 1".to_owned(); - let config = self.session.metadata(); - while config.get_column_table_type(&name).is_some() { - i += 1; - name = format!("New Column {}", i); - } - - format!("// {}\n", name) + format!("// {}\n", get_new_column_name(&self.session)) }; txt.into() diff --git a/rust/perspective-viewer/src/rust/components/expression_panel_sidebar.rs b/rust/perspective-viewer/src/rust/components/expression_panel_sidebar.rs index 9381ef45f6..0a2878696b 100644 --- a/rust/perspective-viewer/src/rust/components/expression_panel_sidebar.rs +++ b/rust/perspective-viewer/src/rust/components/expression_panel_sidebar.rs @@ -13,8 +13,8 @@ use wasm_bindgen::JsValue; use yew::{function_component, html, Callback, Html, Properties}; +use crate::components::containers::sidebar::SidebarCloseButton; use crate::components::expression_editor::ExpressionEditor; -use crate::components::viewer::SidebarCloseButton; use crate::config::ViewConfigUpdate; use crate::derive_model; use crate::model::UpdateAndRender; @@ -24,10 +24,13 @@ use crate::utils::ApiFuture; /// The state of the Expression Editor side panel #[derive(Debug, Clone, PartialEq)] -pub enum EditorState { +pub enum ExpressionPanelState { /// The expression editor is closed. Closed, + /// Not an expression column + NoExpr(String), + /// The editor is opened, and will create a new column when saved. NewExpr, @@ -46,7 +49,7 @@ pub struct ExprEditorPanelProps { /// How to render the editor, with an already existing expression, /// or a new expression. - pub editor_state: EditorState, + pub editor_state: ExpressionPanelState, /// When this callback is called, the expression editor will close. pub on_close: Callback<()>, @@ -60,9 +63,9 @@ pub fn ExprEditorPanel(p: &ExprEditorPanelProps) -> Html { let on_save = yew::use_callback( |v, p| { match &p.editor_state { - EditorState::Closed => {} - EditorState::NewExpr => save_expr(v, p), - EditorState::UpdateExpr(alias) => update_expr(alias, &v, p), + ExpressionPanelState::NewExpr => save_expr(v, p), + ExpressionPanelState::UpdateExpr(alias) => update_expr(alias, &v, p), + _ => {} } p.on_close.emit(()); @@ -79,7 +82,7 @@ pub fn ExprEditorPanel(p: &ExprEditorPanelProps) -> Html { let on_delete = yew::use_callback( |(), p| { - if let EditorState::UpdateExpr(ref s) = p.editor_state { + if let ExpressionPanelState::UpdateExpr(ref s) = p.editor_state { delete_expr(s, p); } @@ -90,8 +93,8 @@ pub fn ExprEditorPanel(p: &ExprEditorPanelProps) -> Html { let alias = yew::use_memo( |s| match s { - EditorState::NewExpr | EditorState::Closed => None, - EditorState::UpdateExpr(s) => Some(s.clone()), + ExpressionPanelState::UpdateExpr(s) => Some(s.clone()), + _ => None, }, p.editor_state.clone(), ); diff --git a/rust/perspective-viewer/src/rust/components/mod.rs b/rust/perspective-viewer/src/rust/components/mod.rs index 1a024d755a..9e65867f58 100644 --- a/rust/perspective-viewer/src/rust/components/mod.rs +++ b/rust/perspective-viewer/src/rust/components/mod.rs @@ -36,6 +36,7 @@ pub mod string_column_style; pub mod style; pub mod viewer; +pub mod column_settings_sidebar; #[cfg(test)] mod tests; diff --git a/rust/perspective-viewer/src/rust/components/number_column_style.rs b/rust/perspective-viewer/src/rust/components/number_column_style.rs index 517fc33896..fd67837ad1 100644 --- a/rust/perspective-viewer/src/rust/components/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/number_column_style.rs @@ -20,7 +20,7 @@ use super::containers::radio_list_item::RadioListItem; use super::form::color_range_selector::*; use super::form::number_input::*; use super::modal::*; -use super::style::{LocalStyle, StyleProvider}; +use super::style::LocalStyle; use crate::config::*; use crate::utils::WeakScope; use crate::*; @@ -53,10 +53,10 @@ pub enum NumberColumnStyleMsg { /// object and a default version without `Option<>` #[derive(Properties)] pub struct NumberColumnStyleProps { - #[prop_or_default] - pub config: NumberColumnStyleConfig, + #[cfg_attr(test, prop_or_default)] + pub config: Option, - #[prop_or_default] + #[cfg_attr(test, prop_or_default)] pub default_config: NumberColumnStyleDefaultConfig, #[prop_or_default] @@ -85,6 +85,7 @@ impl NumberColumnStyleProps {} /// JSON serializable config record and the defaults record). pub struct NumberColumnStyle { config: NumberColumnStyleConfig, + default_config: NumberColumnStyleDefaultConfig, fg_mode: NumberForegroundMode, bg_mode: NumberBackgroundMode, pos_fg_color: String, @@ -101,11 +102,17 @@ impl Component for NumberColumnStyle { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); - Self::reset(&ctx.props().config, &ctx.props().default_config) + Self::reset( + &ctx.props().config.clone().unwrap_or_default(), + &ctx.props().default_config.clone(), + ) } fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { - let mut new = Self::reset(&ctx.props().config, &ctx.props().default_config); + let mut new = Self::reset( + &ctx.props().config.clone().unwrap_or_default(), + &ctx.props().default_config.clone(), + ); std::mem::swap(self, &mut new); true } @@ -119,7 +126,7 @@ impl Component for NumberColumnStyle { } NumberColumnStyleMsg::FixedChanged(fixed) => { let fixed = match fixed.parse::() { - Ok(x) if x != ctx.props().default_config.fixed => Some(x), + Ok(x) if x != self.default_config.fixed => Some(x), Ok(_) => None, Err(_) if fixed.is_empty() => Some(0), Err(_) => None, @@ -234,11 +241,11 @@ impl Component for NumberColumnStyle { self.config.fg_gradient = Some(x); } (Fg, Err(_)) if gradient.is_empty() => { - self.fg_gradient = ctx.props().default_config.fg_gradient; - self.config.fg_gradient = Some(ctx.props().default_config.fg_gradient); + self.fg_gradient = self.default_config.fg_gradient; + self.config.fg_gradient = Some(self.default_config.fg_gradient); } (Fg, Err(_)) => { - self.fg_gradient = ctx.props().default_config.fg_gradient; + self.fg_gradient = self.default_config.fg_gradient; self.config.fg_gradient = None; } (Bg, Ok(x)) => { @@ -246,11 +253,11 @@ impl Component for NumberColumnStyle { self.config.bg_gradient = Some(x); } (Bg, Err(_)) if gradient.is_empty() => { - self.bg_gradient = ctx.props().default_config.bg_gradient; - self.config.bg_gradient = Some(ctx.props().default_config.bg_gradient); + self.bg_gradient = self.default_config.bg_gradient; + self.config.bg_gradient = Some(self.default_config.bg_gradient); } (Bg, Err(_)) => { - self.bg_gradient = ctx.props().default_config.bg_gradient; + self.bg_gradient = self.default_config.bg_gradient; self.config.bg_gradient = None; } }; @@ -276,7 +283,7 @@ impl Component for NumberColumnStyle { let fixed_value = self .config .fixed - .unwrap_or(ctx.props().default_config.fixed) + .unwrap_or(self.default_config.fixed) .to_string(); // Color enabled/disabled oninput callback @@ -362,82 +369,80 @@ impl Component for NumberColumnStyle { } }; - html! { - - -
-
- -
-
- - -
-
- -
-
- - - class="indent" - name="foreground-list" - disabled={ !self.config.number_fg_mode.is_enabled() } - selected={ selected_fg_mode } - on_change={ fg_mode_changed } > - - - value={ NumberForegroundMode::Color }> - { fg_color_controls } - > - - value={ NumberForegroundMode::Bar }> - { fg_bar_controls } - > - > -
-
- -
-
- - - class="indent" - name="background-list" - disabled={ self.config.number_bg_mode.is_disabled() } - selected={ selected_bg_mode } - on_change={ bg_mode_changed } > - - - value={ NumberBackgroundMode::Color }> - { bg_color_controls } - > - - value={ NumberBackgroundMode::Gradient }> - { bg_gradient_controls } - > - - value={ NumberBackgroundMode::Pulse }> - { bg_pulse_controls } - > - > -
+ html_template! { + +
+
+ +
+
+ + +
+
+ +
+
+ + + class="indent" + name="foreground-list" + disabled={ !self.config.number_fg_mode.is_enabled() } + selected={ selected_fg_mode } + on_change={ fg_mode_changed } > + + + value={ NumberForegroundMode::Color }> + { fg_color_controls } + > + + value={ NumberForegroundMode::Bar }> + { fg_bar_controls } + > + > +
+
+ +
+
+ + + class="indent" + name="background-list" + disabled={ self.config.number_bg_mode.is_disabled() } + selected={ selected_bg_mode } + on_change={ bg_mode_changed } > + + + value={ NumberBackgroundMode::Color }> + { bg_color_controls } + > + + value={ NumberBackgroundMode::Gradient }> + { bg_gradient_controls } + > + + value={ NumberBackgroundMode::Pulse }> + { bg_pulse_controls } + > + >
- +
} } } @@ -451,8 +456,8 @@ impl NumberColumnStyle { pos_fg_color: Some(pos_color), neg_fg_color: Some(neg_color), .. - } if *pos_color == ctx.props().default_config.pos_fg_color - && *neg_color == ctx.props().default_config.neg_fg_color => + } if *pos_color == self.default_config.pos_fg_color + && *neg_color == self.default_config.neg_fg_color => { config.pos_fg_color = None; config.neg_fg_color = None; @@ -465,8 +470,8 @@ impl NumberColumnStyle { pos_bg_color: Some(pos_color), neg_bg_color: Some(neg_color), .. - } if *pos_color == ctx.props().default_config.pos_bg_color - && *neg_color == ctx.props().default_config.neg_bg_color => + } if *pos_color == self.default_config.pos_bg_color + && *neg_color == self.default_config.neg_bg_color => { config.pos_bg_color = None; config.neg_bg_color = None; @@ -519,11 +524,11 @@ impl NumberColumnStyle { } /// Human readable precision hint, e.g. "Prec 0.001" for `{fixed: 3}`. - fn make_fixed_text(&self, ctx: &Context) -> String { + fn make_fixed_text(&self, _ctx: &Context) -> String { let fixed = match self.config.fixed { Some(x) if x > 0 => format!("0.{}1", "0".repeat(x as usize - 1)), - None if ctx.props().default_config.fixed > 0 => { - let n = ctx.props().default_config.fixed as usize - 1; + None if self.default_config.fixed > 0 => { + let n = self.default_config.fixed as usize - 1; format!("0.{}1", "0".repeat(n)) } Some(_) | None => "1".to_owned(), @@ -591,6 +596,7 @@ impl NumberColumnStyle { Self { config, + default_config: default_config.clone(), fg_mode, bg_mode, pos_fg_color, diff --git a/rust/perspective-viewer/src/rust/components/string_column_style.rs b/rust/perspective-viewer/src/rust/components/string_column_style.rs index 293e917ccd..80063a4c50 100644 --- a/rust/perspective-viewer/src/rust/components/string_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/string_column_style.rs @@ -19,7 +19,7 @@ use super::containers::radio_list::RadioList; use super::containers::radio_list_item::RadioListItem; use super::form::color_selector::*; use super::modal::{ModalLink, SetModalLink}; -use super::style::{LocalStyle, StyleProvider}; +use super::style::LocalStyle; use crate::config::*; use crate::utils::WeakScope; use crate::*; @@ -35,10 +35,8 @@ pub enum StringColumnStyleMsg { #[derive(Properties)] pub struct StringColumnStyleProps { - #[prop_or_default] - pub config: StringColumnStyleConfig, + pub config: Option, - #[prop_or_default] pub default_config: StringColumnStyleDefaultConfig, #[prop_or_default] @@ -55,8 +53,8 @@ impl ModalLink for StringColumnStyleProps { } impl PartialEq for StringColumnStyleProps { - fn eq(&self, other: &Self) -> bool { - self.config == other.config + fn eq(&self, _other: &Self) -> bool { + false } } @@ -65,6 +63,7 @@ impl PartialEq for StringColumnStyleProps { /// JSON serializable config record and the defaults record). pub struct StringColumnStyle { config: StringColumnStyleConfig, + default_config: StringColumnStyleDefaultConfig, } impl StringColumnStyle { @@ -81,7 +80,7 @@ impl StringColumnStyle { .config .color .clone() - .unwrap_or_else(|| ctx.props().default_config.color.to_owned()); + .unwrap_or_else(|| self.default_config.color.to_owned()); let color_props = props!(ColorProps { color, on_color }); match &self.config.string_color_mode { @@ -109,7 +108,19 @@ impl Component for StringColumnStyle { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); Self { - config: ctx.props().config.clone(), + config: ctx.props().config.clone().unwrap_or_default(), + default_config: ctx.props().default_config.clone(), + } + } + + // Always re-render when config changes. + fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { + let mut new_config = ctx.props().config.clone().unwrap_or_default(); + if self.config != new_config { + std::mem::swap(&mut self.config, &mut new_config); + true + } else { + false } } @@ -187,71 +198,69 @@ impl Component for StringColumnStyle { let background_controls = self.color_select_row(ctx, &StringColorMode::Background, "Background"); - html! { - - -
-
- -
-
- + html_template! { + +
+
+ +
+
+ - - class="indent" - disabled={ self.config.format.is_none() } - selected={ format_mode_selected } - on_change={ format_mode_changed } > + + class="indent" + disabled={ self.config.format.is_none() } + selected={ format_mode_selected } + on_change={ format_mode_changed } > - - value={ FormatMode::Bold }> - { "Bold" } - > - - value={ FormatMode::Italics }> - { "Italics" } - > - - value={ FormatMode::Link }> - { "Link" } - > - > -
-
- -
-
- + + value={ FormatMode::Bold }> + { "Bold" } + > + + value={ FormatMode::Italics }> + { "Italics" } + > + + value={ FormatMode::Link }> + { "Link" } + > + > +
+
+ +
+
+ - - class="indent" - name="color-radio-list" - disabled={ self.config.string_color_mode.is_none() } - selected={ selected_color_mode } - on_change={ color_mode_changed } > + + class="indent" + name="color-radio-list" + disabled={ self.config.string_color_mode.is_none() } + selected={ selected_color_mode } + on_change={ color_mode_changed } > - - value={ StringColorMode::Foreground }> - { foreground_controls } - > - - value={ StringColorMode::Background }> - { background_controls } - > - - value={ StringColorMode::Series }> - { series_controls } - > - > -
+ + value={ StringColorMode::Foreground }> + { foreground_controls } + > + + value={ StringColorMode::Background }> + { background_controls } + > + + value={ StringColorMode::Series }> + { series_controls } + > + >
- +
} } } diff --git a/rust/perspective-viewer/src/rust/components/tests/column_style.rs b/rust/perspective-viewer/src/rust/components/tests/column_style.rs index e84a62f352..c4150e5884 100644 --- a/rust/perspective-viewer/src/rust/components/tests/column_style.rs +++ b/rust/perspective-viewer/src/rust/components/tests/column_style.rs @@ -48,7 +48,7 @@ pub async fn test_initial_fixed() { test_html! {
- +
}; diff --git a/rust/perspective-viewer/src/rust/components/viewer.rs b/rust/perspective-viewer/src/rust/components/viewer.rs index e6cb8f5b4d..4797854b6d 100644 --- a/rust/perspective-viewer/src/rust/components/viewer.rs +++ b/rust/perspective-viewer/src/rust/components/viewer.rs @@ -18,13 +18,13 @@ use yew::prelude::*; use super::column_selector::ColumnSelector; use super::containers::split_panel::SplitPanel; -use super::expression_panel_sidebar::EditorState; use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus}; use super::plugin_selector::PluginSelector; use super::render_warning::RenderWarning; use super::status_bar::StatusBar; use super::style::{LocalStyle, StyleProvider}; -use crate::components::expression_panel_sidebar::ExprEditorPanel; +use crate::components::column_settings_sidebar::ColumnSettingsSidebar; +use crate::components::containers::sidebar::SidebarCloseButton; use crate::config::*; use crate::dragdrop::*; use crate::model::*; @@ -34,6 +34,14 @@ use crate::session::*; use crate::utils::*; use crate::*; +/// A ColumnLocator is a combination of the column's type and its name. +/// It's used to locate columns for the column_settings_sidebar. +#[derive(Clone, Debug, PartialEq)] +pub enum ColumnLocator { + Plain(String), + Expr(Option), +} + #[derive(Properties)] pub struct PerspectiveViewerProps { pub elem: web_sys::HtmlElement, @@ -67,9 +75,11 @@ pub enum PerspectiveViewerMsg { ToggleSettingsInit(Option, Option>>), ToggleSettingsComplete(SettingsUpdate, Sender<()>), PreloadFontsUpdate, - ViewConfigChanged, RenderLimits(Option<(usize, usize, Option, Option)>), - ExpressionEditor(EditorState), + /// this is really more like "open the specified column" + /// since we call ToggleColumnSettings(None, None) to clear the selected + /// column + ToggleColumnSettings(Option, Option>), } pub struct PerspectiveViewer { @@ -77,7 +87,8 @@ pub struct PerspectiveViewer { on_rendered: Option>, fonts: FontLoaderProps, settings_open: bool, - editor_state: EditorState, + /// The column which will be opened in the ColumnSettingsSidebar + selected_column: Option, on_resize: Rc>, on_dimensions_reset: Rc>, _subscriptions: [Subscription; 1], @@ -100,7 +111,7 @@ impl Component for PerspectiveViewer { } else { vec![ PerspectiveViewerMsg::RenderLimits(Some(x)), - PerspectiveViewerMsg::ViewConfigChanged, + PerspectiveViewerMsg::ToggleColumnSettings(None, None), ] } }); @@ -112,7 +123,7 @@ impl Component for PerspectiveViewer { on_rendered: None, fonts: FontLoaderProps::new(&elem, callback), settings_open: false, - editor_state: EditorState::Closed, + selected_column: None, on_resize: Default::default(), on_dimensions_reset: Default::default(), _subscriptions: [session_sub], @@ -120,7 +131,7 @@ impl Component for PerspectiveViewer { } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let needs_update = self.editor_state != EditorState::Closed; + let needs_update = self.selected_column.is_some(); match msg { PerspectiveViewerMsg::PreloadFontsUpdate => true, PerspectiveViewerMsg::Resize => { @@ -128,7 +139,7 @@ impl Component for PerspectiveViewer { false } PerspectiveViewerMsg::Reset(all, sender) => { - self.editor_state = EditorState::Closed; + self.selected_column = None; clone!( ctx.props().renderer, ctx.props().session, @@ -175,7 +186,7 @@ impl Component for PerspectiveViewer { PerspectiveViewerMsg::ToggleSettingsComplete(SettingsUpdate::SetDefault, resolve) if self.settings_open => { - self.editor_state = EditorState::Closed; + self.selected_column = None; self.settings_open = false; self.on_rendered = Some(resolve); true @@ -184,7 +195,7 @@ impl Component for PerspectiveViewer { SettingsUpdate::Update(force), resolve, ) if force != self.settings_open => { - self.editor_state = EditorState::Closed; + self.selected_column = None; self.settings_open = force; self.on_rendered = Some(resolve); true @@ -192,19 +203,15 @@ impl Component for PerspectiveViewer { PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve) if matches!(self.fonts.get_status(), FontLoaderStatus::Finished) => { - self.editor_state = EditorState::Closed; + self.selected_column = None; resolve.send(()).expect("Orphan render"); false } PerspectiveViewerMsg::ToggleSettingsComplete(_, resolve) => { - self.editor_state = EditorState::Closed; + self.selected_column = None; self.on_rendered = Some(resolve); true } - PerspectiveViewerMsg::ViewConfigChanged => { - self.editor_state = EditorState::Closed; - needs_update - } PerspectiveViewerMsg::RenderLimits(dimensions) => { if self.dimensions != dimensions { self.dimensions = dimensions; @@ -213,13 +220,30 @@ impl Component for PerspectiveViewer { false } } - PerspectiveViewerMsg::ExpressionEditor(new_state) => { - if self.editor_state == new_state { - false + PerspectiveViewerMsg::ToggleColumnSettings(selected_column, sender) => { + let (open, column_name) = if self.selected_column == selected_column { + self.selected_column = None; + (false, None) } else { - self.editor_state = new_state; - true + self.selected_column = selected_column.clone(); + + selected_column + .map(|c| match c { + ColumnLocator::Plain(s) => (true, Some(s)), + ColumnLocator::Expr(maybe_s) => (true, maybe_s), + }) + .unwrap_or_default() + }; + + ctx.props() + .presentation + .column_settings_open_changed + .emit_all((open, column_name)); + + if let Some(sender) = sender { + sender.send(()).unwrap(); } + true } } } @@ -266,12 +290,9 @@ impl Component for PerspectiveViewer { class.push("titled"); } - let on_open_expr_panel = ctx.link().callback(|s: Option| { - PerspectiveViewerMsg::ExpressionEditor( - s.map(EditorState::UpdateExpr) - .unwrap_or(EditorState::NewExpr), - ) - }); + let on_open_expr_panel = ctx + .link() + .callback(|c| PerspectiveViewerMsg::ToggleColumnSettings(Some(c), None)); let on_reset = ctx .link() .callback(|all| PerspectiveViewerMsg::Reset(all, None)); @@ -284,7 +305,8 @@ impl Component for PerspectiveViewer { id="app_panel" reverse=true on_reset={ self.on_dimensions_reset.callback() } - on_resize_finished={ ctx.props().render_callback() }> + on_resize_finished={ ctx.props().render_callback() } + >
@@ -320,13 +342,15 @@ impl Component for PerspectiveViewer {
- if !matches!(self.editor_state, EditorState::Closed) { + if let Some(selected_column) = self.selected_column.clone() { - + presentation = {&ctx.props().presentation} + {selected_column} + on_close = {ctx.link().callback(|_| PerspectiveViewerMsg::ToggleColumnSettings(None, None))} + /> <> } @@ -420,18 +444,3 @@ impl PerspectiveViewer { }; } } - -#[derive(PartialEq, Clone, Properties)] -pub struct SidebarCloseButtonProps { - pub on_close_sidebar: Callback<()>, - pub id: AttrValue, -} - -#[function_component] -pub fn SidebarCloseButton(p: &SidebarCloseButtonProps) -> Html { - let onclick = yew::use_callback(|_, cb| cb.emit(()), p.on_close_sidebar.clone()); - let id = &p.id; - html! { - - } -} diff --git a/rust/perspective-viewer/src/rust/config/column_type.rs b/rust/perspective-viewer/src/rust/config/column_type.rs index b06d86b33e..67c2784b60 100644 --- a/rust/perspective-viewer/src/rust/config/column_type.rs +++ b/rust/perspective-viewer/src/rust/config/column_type.rs @@ -47,3 +47,16 @@ impl Display for Type { }) } } +impl Type { + pub fn to_capitalized(&self) -> String { + match self { + Type::String => "String", + Type::Datetime => "Datetime", + Type::Date => "Date", + Type::Integer => "Integer", + Type::Float => "Float", + Type::Bool => "Boolean", + } + .into() + } +} diff --git a/rust/perspective-viewer/src/rust/config/datetime_column_style.rs b/rust/perspective-viewer/src/rust/config/datetime_column_style.rs index 1046368783..552e43982d 100644 --- a/rust/perspective-viewer/src/rust/config/datetime_column_style.rs +++ b/rust/perspective-viewer/src/rust/config/datetime_column_style.rs @@ -33,7 +33,6 @@ pub enum DatetimeFormatType { Simple(SimpleDatetimeStyleConfig), } - /// A model for the JSON serialized style configuration for a column of type /// `datetime`. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -67,7 +66,7 @@ impl Default for DatetimeColumnStyleConfig { derive_wasm_abi!(DatetimeColumnStyleConfig, FromWasmAbi, IntoWasmAbi); -#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize, Debug)] pub struct DatetimeColumnStyleDefaultConfig { pub color: String, } diff --git a/rust/perspective-viewer/src/rust/config/number_column_style.rs b/rust/perspective-viewer/src/rust/config/number_column_style.rs index 9d33500dc9..4d3974e5e1 100644 --- a/rust/perspective-viewer/src/rust/config/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/config/number_column_style.rs @@ -123,8 +123,7 @@ impl NumberBackgroundMode { } } -#[cfg_attr(test, derive(Debug))] -#[derive(Serialize, Deserialize, Clone, Default)] +#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)] pub struct NumberColumnStyleConfig { #[serde(default = "NumberForegroundMode::default")] #[serde(skip_serializing_if = "NumberForegroundMode::is_color")] diff --git a/rust/perspective-viewer/src/rust/config/string_column_style.rs b/rust/perspective-viewer/src/rust/config/string_column_style.rs index 29281f027e..9da41c8610 100644 --- a/rust/perspective-viewer/src/rust/config/string_column_style.rs +++ b/rust/perspective-viewer/src/rust/config/string_column_style.rs @@ -96,20 +96,22 @@ impl FromStr for FormatMode { } } -#[cfg_attr(test, derive(Debug))] -#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct StringColumnStyleConfig { #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] pub format: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] pub string_color_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] pub color: Option, } -#[derive(Clone, Default, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, Eq, PartialEq, Serialize)] pub struct StringColumnStyleDefaultConfig { pub color: String, } diff --git a/rust/perspective-viewer/src/rust/custom_elements/date_column_style.rs b/rust/perspective-viewer/src/rust/custom_elements/date_column_style.rs deleted file mode 100644 index 057bd31aef..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/date_column_style.rs +++ /dev/null @@ -1,109 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::*; - -use crate::components::datetime_column_style::*; -use crate::config::*; -use crate::custom_elements::modal::*; -use crate::utils::{CustomElementMetadata, *}; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct PerspectiveDateColumnStyleElement { - elem: HtmlElement, - modal: Option>, -} - -fn on_change(elem: &web_sys::HtmlElement, config: &DatetimeColumnStyleConfig) { - let mut event_init = web_sys::CustomEventInit::new(); - event_init.detail(&JsValue::from_serde_ext(config).unwrap()); - let event = - CustomEvent::new_with_event_init_dict("perspective-column-style-change", &event_init); - - elem.dispatch_event(&event.unwrap()).unwrap(); -} - -impl CustomElementMetadata for PerspectiveDateColumnStyleElement { - const CUSTOM_ELEMENT_NAME: &'static str = "perspective-date-column-style"; -} - -#[wasm_bindgen] -impl PerspectiveDateColumnStyleElement { - #[wasm_bindgen(constructor)] - pub fn new(elem: web_sys::HtmlElement) -> Self { - Self { elem, modal: None } - } - - /// Reset to a provided JSON config, to be used in place of `new()` when - /// re-using this component. - /// - /// # Arguments - /// * `config` - a `ColumnStyle` config in JSON form. - pub fn reset(&mut self, config: JsValue) -> ApiResult<()> { - let msg = DatetimeColumnStyleMsg::Reset(config.into_serde_ext().unwrap()); - self.modal.as_apierror()?.send_message(msg); - Ok(()) - } - - /// Dispatches to `ModalElement::open(target)` - /// - /// # Arguments - /// `target` - the relative target to pin this `ModalElement` to. - pub fn open( - &mut self, - target: web_sys::HtmlElement, - js_config: JsValue, - js_default_config: JsValue, - ) -> ApiResult<()> { - if self.modal.is_some() { - self.reset(js_config)?; - } else { - let config: DatetimeColumnStyleConfig = js_config.into_serde_ext().unwrap(); - let default_config: DatetimeColumnStyleDefaultConfig = - js_default_config.into_serde_ext().unwrap(); - - let on_change = { - clone!(self.elem); - Callback::from(move |x: DatetimeColumnStyleConfig| on_change(&elem, &x)) - }; - - let props = props!(DatetimeColumnStyleProps { - enable_time_config: false, - config, - default_config, - on_change, - }); - - self.modal = Some(ModalElement::new(self.elem.clone(), props, true, None)); - } - - ApiFuture::spawn(self.modal.as_apierror()?.clone().open(target, None)); - Ok(()) - } - - /// Remove this `ModalElement` from the DOM. - pub fn close(&mut self) -> ApiResult<()> { - self.modal.as_apierror()?.hide() - } - - pub fn destroy(self) -> ApiResult<()> { - self.modal.into_apierror()?.destroy() - } - - /// DOM lifecycle method when connected. We don't use this, as it can fire - /// during innocuous events like re-parenting. - pub fn connected_callback(&self) {} -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/datetime_column_style.rs b/rust/perspective-viewer/src/rust/custom_elements/datetime_column_style.rs deleted file mode 100644 index 3c2976a9c7..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/datetime_column_style.rs +++ /dev/null @@ -1,109 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::*; - -use crate::components::datetime_column_style::*; -use crate::config::*; -use crate::custom_elements::modal::*; -use crate::utils::{CustomElementMetadata, *}; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct PerspectiveDatetimeColumnStyleElement { - elem: HtmlElement, - modal: Option>, -} - -fn on_change(elem: &web_sys::HtmlElement, config: &DatetimeColumnStyleConfig) { - let mut event_init = web_sys::CustomEventInit::new(); - event_init.detail(&JsValue::from_serde_ext(config).unwrap()); - let event = - CustomEvent::new_with_event_init_dict("perspective-column-style-change", &event_init); - - elem.dispatch_event(&event.unwrap()).unwrap(); -} - -impl CustomElementMetadata for PerspectiveDatetimeColumnStyleElement { - const CUSTOM_ELEMENT_NAME: &'static str = "perspective-datetime-column-style"; -} - -#[wasm_bindgen] -impl PerspectiveDatetimeColumnStyleElement { - #[wasm_bindgen(constructor)] - pub fn new(elem: web_sys::HtmlElement) -> Self { - Self { elem, modal: None } - } - - /// Reset to a provided JSON config, to be used in place of `new()` when - /// re-using this component. - /// - /// # Arguments - /// * `config` - a `ColumnStyle` config in JSON form. - pub fn reset(&mut self, config: JsValue) -> ApiResult<()> { - let msg = DatetimeColumnStyleMsg::Reset(config.into_serde_ext().unwrap()); - self.modal.as_apierror()?.send_message(msg); - Ok(()) - } - - /// Dispatches to `ModalElement::open(target)` - /// - /// # Arguments - /// `target` - the relative target to pin this `ModalElement` to. - pub fn open( - &mut self, - target: web_sys::HtmlElement, - js_config: JsValue, - js_default_config: JsValue, - ) -> ApiResult<()> { - if self.modal.is_some() { - self.reset(js_config)?; - } else { - let config: DatetimeColumnStyleConfig = js_config.into_serde_ext().unwrap(); - let default_config: DatetimeColumnStyleDefaultConfig = - js_default_config.into_serde_ext().unwrap(); - - let on_change = { - clone!(self.elem); - Callback::from(move |x: DatetimeColumnStyleConfig| on_change(&elem, &x)) - }; - - let props = props!(DatetimeColumnStyleProps { - enable_time_config: true, - config, - default_config, - on_change, - }); - - self.modal = Some(ModalElement::new(self.elem.clone(), props, true, None)); - } - - ApiFuture::spawn(self.modal.as_apierror()?.clone().open(target, None)); - Ok(()) - } - - /// Remove this `ModalElement` from the DOM. - pub fn close(&mut self) -> ApiResult<()> { - self.modal.as_apierror()?.hide() - } - - pub fn destroy(self) -> ApiResult<()> { - self.modal.into_apierror()?.destroy() - } - - /// DOM lifecycle method when connected. We don't use this, as it can fire - /// during innocuous events like re-parenting. - pub fn connected_callback(&self) {} -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/mod.rs b/rust/perspective-viewer/src/rust/custom_elements/mod.rs index 046e3ec98e..ad0b20a03e 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/mod.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/mod.rs @@ -12,15 +12,11 @@ mod column_dropdown; pub mod copy_dropdown; -pub mod date_column_style; -pub mod datetime_column_style; pub mod debug_plugin; pub mod export_dropdown; mod filter_dropdown; mod function_dropdown; pub mod modal; -pub mod number_column_style; -pub mod string_column_style; pub mod viewer; pub use self::column_dropdown::*; diff --git a/rust/perspective-viewer/src/rust/custom_elements/number_column_style.rs b/rust/perspective-viewer/src/rust/custom_elements/number_column_style.rs deleted file mode 100644 index e4cd60b409..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/number_column_style.rs +++ /dev/null @@ -1,111 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::*; - -use crate::components::number_column_style::*; -use crate::config::*; -use crate::custom_elements::modal::*; -use crate::utils::{CustomElementMetadata, *}; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct PerspectiveNumberColumnStyleElement { - elem: HtmlElement, - modal: Option>, -} - -fn on_change(elem: &web_sys::HtmlElement, config: &NumberColumnStyleConfig) { - let mut event_init = web_sys::CustomEventInit::new(); - event_init.detail(&JsValue::from_serde_ext(config).unwrap()); - let event = - CustomEvent::new_with_event_init_dict("perspective-column-style-change", &event_init); - - elem.dispatch_event(&event.unwrap()).unwrap(); -} - -impl CustomElementMetadata for PerspectiveNumberColumnStyleElement { - const CUSTOM_ELEMENT_NAME: &'static str = "perspective-number-column-style"; -} - -#[wasm_bindgen] -impl PerspectiveNumberColumnStyleElement { - #[wasm_bindgen(constructor)] - pub fn new(elem: web_sys::HtmlElement) -> Self { - Self { elem, modal: None } - } - - /// Reset to a provided JSON config, to be used in place of `new()` when - /// re-using this component. - /// - /// # Arguments - /// * `config` - a `ColumnStyle` config in JSON form. - /// * `default_config` - the default `ColumnStyle` config for this column - /// type, in JSON form. - pub fn reset( - &mut self, - config: NumberColumnStyleConfig, - default_config: NumberColumnStyleDefaultConfig, - ) -> ApiResult<()> { - let msg = NumberColumnStyleMsg::Reset(config.into(), default_config.into()); - self.modal.as_apierror()?.send_message(msg); - Ok(()) - } - - /// Dispatches to `ModalElement::open(target)` after lazy initializing the - /// `ModelElement` custom element handle. - /// - /// # Arguments - /// `target` - the relative target to pin this `ModalElement` to. - pub fn open( - &mut self, - target: web_sys::HtmlElement, - config: NumberColumnStyleConfig, - default_config: NumberColumnStyleDefaultConfig, - ) -> ApiResult<()> { - if self.modal.is_some() { - self.reset(config, default_config)?; - } else { - let on_change = { - clone!(self.elem); - Callback::from(move |x: NumberColumnStyleConfig| on_change(&elem, &x)) - }; - - let props = props!(NumberColumnStyleProps { - config, - on_change, - default_config, - }); - - self.modal = Some(ModalElement::new(self.elem.clone(), props, true, None)); - } - - ApiFuture::spawn(self.modal.as_apierror()?.clone().open(target, None)); - Ok(()) - } - - /// Remove this `ModalElement` from the DOM. - pub fn close(&mut self) -> ApiResult<()> { - self.modal.as_apierror()?.hide() - } - - pub fn destroy(self) -> ApiResult<()> { - self.modal.into_apierror()?.destroy() - } - - /// DOM lifecycle method when connected. We don't use this, as it can fire - /// during innocuous events like re-parenting. - pub fn connected_callback(&self) {} -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/string_column_style.rs b/rust/perspective-viewer/src/rust/custom_elements/string_column_style.rs deleted file mode 100644 index 4b99e93b08..0000000000 --- a/rust/perspective-viewer/src/rust/custom_elements/string_column_style.rs +++ /dev/null @@ -1,108 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -use wasm_bindgen::prelude::*; -use web_sys::*; -use yew::*; - -use crate::components::string_column_style::*; -use crate::config::*; -use crate::custom_elements::modal::*; -use crate::utils::{CustomElementMetadata, *}; -use crate::*; - -#[wasm_bindgen] -#[derive(Clone)] -pub struct PerspectiveStringColumnStyleElement { - elem: HtmlElement, - modal: Option>, -} - -fn on_change(elem: &web_sys::HtmlElement, config: &StringColumnStyleConfig) { - let mut event_init = web_sys::CustomEventInit::new(); - event_init.detail(&JsValue::from_serde_ext(config).unwrap()); - let event = - CustomEvent::new_with_event_init_dict("perspective-column-style-change", &event_init); - - elem.dispatch_event(&event.unwrap()).unwrap(); -} - -impl CustomElementMetadata for PerspectiveStringColumnStyleElement { - const CUSTOM_ELEMENT_NAME: &'static str = "perspective-string-column-style"; -} - -#[wasm_bindgen] -impl PerspectiveStringColumnStyleElement { - #[wasm_bindgen(constructor)] - pub fn new(elem: web_sys::HtmlElement) -> Self { - Self { elem, modal: None } - } - - /// Reset to a provided JSON config, to be used in place of `new()` when - /// re-using this component. - /// - /// # Arguments - /// * `config` - a `ColumnStyle` config in JSON form. - pub fn reset(&mut self, config: JsValue) -> ApiResult<()> { - let msg = StringColumnStyleMsg::Reset(config.into_serde_ext().unwrap()); - self.modal.as_apierror()?.send_message(msg); - Ok(()) - } - - /// Dispatches to `ModalElement::open(target)` - /// - /// # Arguments - /// `target` - the relative target to pin this `ModalElement` to. - pub fn open( - &mut self, - target: web_sys::HtmlElement, - js_config: JsValue, - js_default_config: JsValue, - ) -> ApiResult<()> { - if self.modal.is_some() { - self.reset(js_config)?; - } else { - let config: StringColumnStyleConfig = js_config.into_serde_ext().unwrap(); - let default_config: StringColumnStyleDefaultConfig = - js_default_config.into_serde_ext().unwrap(); - - let on_change = { - clone!(self.elem); - Callback::from(move |x: StringColumnStyleConfig| on_change(&elem, &x)) - }; - - let props = props!(StringColumnStyleProps { - config, - default_config, - on_change, - }); - - self.modal = Some(ModalElement::new(self.elem.clone(), props, true, None)); - } - - ApiFuture::spawn(self.modal.as_apierror()?.clone().open(target, None)); - Ok(()) - } - - /// Remove this `ModalElement` from the DOM. - pub fn close(&mut self) -> ApiResult<()> { - self.modal.as_apierror()?.hide() - } - - pub fn destroy(self) -> ApiResult<()> { - self.modal.into_apierror()?.destroy() - } - - /// DOM lifecycle method when connected. We don't use this, as it can fire - /// during innocuous events like re-parenting. - pub fn connected_callback(&self) {} -} diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index b0454c1c66..aa71fa8f51 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -617,4 +617,22 @@ impl PerspectiveViewerElement { pub fn unsafe_get_model(&self) -> *const PerspectiveViewerElement { std::ptr::addr_of!(*self) } + + /// Asynchronously opens the column settings for a specific column. + /// When finished, the `` element will emit a + /// "perspective-toggle-column-settings" CustomEvent. + /// The event's details property has two fields: `{open: bool, column_name?: + /// string}`. The CustomEvent is also fired whenever the user toggles the + /// sidebar manually. + #[wasm_bindgen(js_name = "toggleColumnSettings")] + pub fn toggle_column_settings(&self, column_name: String) -> ApiFuture<()> { + clone!(self.session, self.root); + ApiFuture::new(async move { + let locator = session.metadata().get_column_locator(Some(column_name)); + let task = root.borrow().as_apierror()?.send_message_async(|sender| { + PerspectiveViewerMsg::ToggleColumnSettings(locator, Some(sender)) + }); + task.await.map_err(|_| ApiError::from("Cancelled")) + }) + } } diff --git a/rust/perspective-viewer/src/rust/custom_events.rs b/rust/perspective-viewer/src/rust/custom_events.rs index 8433d3aefa..1383869b4c 100644 --- a/rust/perspective-viewer/src/rust/custom_events.rs +++ b/rust/perspective-viewer/src/rust/custom_events.rs @@ -31,7 +31,7 @@ use crate::*; /// on `CustomElements`, but when it is `drop()` the Custom Element will no /// longer dispatch events such as `"perspective-config-change"`. #[derive(Clone)] -pub struct CustomEvents(Rc<(CustomEventsDataRc, [Subscription; 4])>); +pub struct CustomEvents(Rc<(CustomEventsDataRc, [Subscription; 6])>); #[derive(Clone)] struct CustomEventsDataRc(Rc); @@ -82,6 +82,22 @@ impl CustomEvents { } }); + let column_settings_sub = presentation.column_settings_open_changed.add_listener({ + clone!(data); + move |(open, column_name)| { + data.dispatch_column_settings_open_changed(open, column_name); + // column_settings is ethereal; do not change the config + } + }); + + let column_settings_updated = presentation.column_settings_updated.add_listener({ + clone!(data); + move |config: JsValue| { + data.dispatch_column_style_changed(&config); + data.clone().dispatch_config_update(); + } + }); + let plugin_sub = renderer.plugin_changed.add_listener({ clone!(data); move |plugin| { @@ -100,6 +116,8 @@ impl CustomEvents { Self(Rc::new((data, [ theme_sub, settings_sub, + column_settings_sub, + column_settings_updated, plugin_sub, view_sub, ]))) @@ -121,6 +139,29 @@ impl CustomEventsDataRc { self.elem.dispatch_event(&event.unwrap()).unwrap(); } + fn dispatch_column_style_changed(&self, config: &JsValue) { + let mut event_init = web_sys::CustomEventInit::new(); + event_init.detail(config); + let event = web_sys::CustomEvent::new_with_event_init_dict( + "perspective-column-style-change", + &event_init, + ); + self.elem.dispatch_event(&event.unwrap()).unwrap(); + } + + fn dispatch_column_settings_open_changed(&self, open: bool, column_name: Option) { + let mut event_init = web_sys::CustomEventInit::new(); + event_init.detail(&JsValue::from( + json!( {"open": open, "column_name": column_name} ), + )); + let event = web_sys::CustomEvent::new_with_event_init_dict( + "perspective-toggle-column-settings", + &event_init, + ); + + self.elem.dispatch_event(&event.unwrap()).unwrap(); + } + fn dispatch_plugin_changed(&self, plugin: &JsPerspectiveViewerPlugin) { let mut event_init = web_sys::CustomEventInit::new(); event_init.detail(plugin); diff --git a/rust/perspective-viewer/src/rust/js/plugin.rs b/rust/perspective-viewer/src/rust/js/plugin.rs index 1569b48715..ed1e4257d2 100644 --- a/rust/perspective-viewer/src/rust/js/plugin.rs +++ b/rust/perspective-viewer/src/rust/js/plugin.rs @@ -51,6 +51,9 @@ extern "C" { #[wasm_bindgen(method, getter)] pub fn config_column_names(this: &JsPerspectiveViewerPlugin) -> Option; + #[wasm_bindgen(method, getter)] + pub fn default_config(this: &JsPerspectiveViewerPlugin) -> Option; + #[wasm_bindgen(method, getter)] pub fn priority(this: &JsPerspectiveViewerPlugin) -> Option; @@ -92,6 +95,7 @@ extern "C" { #[wasm_bindgen(method, catch)] pub async fn resize(this: &JsPerspectiveViewerPlugin) -> ApiResult; + } #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)] diff --git a/rust/perspective-viewer/src/rust/lib.rs b/rust/perspective-viewer/src/rust/lib.rs index 9e27b3d0dc..0a07f17d3c 100644 --- a/rust/perspective-viewer/src/rust/lib.rs +++ b/rust/perspective-viewer/src/rust/lib.rs @@ -40,12 +40,8 @@ use utils::JsValueSerdeExt; use wasm_bindgen::prelude::*; use crate::custom_elements::copy_dropdown::CopyDropDownMenuElement; -use crate::custom_elements::date_column_style::PerspectiveDateColumnStyleElement; -use crate::custom_elements::datetime_column_style::PerspectiveDatetimeColumnStyleElement; use crate::custom_elements::debug_plugin::PerspectiveDebugPluginElement; use crate::custom_elements::export_dropdown::ExportDropDownMenuElement; -use crate::custom_elements::number_column_style::PerspectiveNumberColumnStyleElement; -use crate::custom_elements::string_column_style::PerspectiveStringColumnStyleElement; use crate::custom_elements::viewer::PerspectiveViewerElement; use crate::utils::{define_web_component, ApiResult}; @@ -90,10 +86,6 @@ pub fn bootstrap_web_components(psp: &JsValue) { define_web_component::(psp); } - define_web_component::(psp); - define_web_component::(psp); - define_web_component::(psp); - define_web_component::(psp); define_web_component::(psp); define_web_component::(psp); } diff --git a/rust/perspective-viewer/src/rust/presentation.rs b/rust/perspective-viewer/src/rust/presentation.rs index b8d019cd75..fbb8a7129d 100644 --- a/rust/perspective-viewer/src/rust/presentation.rs +++ b/rust/perspective-viewer/src/rust/presentation.rs @@ -29,6 +29,12 @@ use crate::utils::*; #[derive(Clone)] pub struct Presentation(Rc); +impl PartialEq for Presentation { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } +} + impl Deref for Presentation { type Target = PresentationHandle; @@ -46,6 +52,8 @@ pub struct PresentationHandle { is_settings_open: RefCell, is_workspace: RefCell>, pub settings_open_changed: PubSub, + pub column_settings_open_changed: PubSub<(bool, Option)>, + pub column_settings_updated: PubSub, pub theme_config_updated: PubSub<(Vec, Option)>, pub title_changed: PubSub>, } @@ -62,6 +70,8 @@ impl Presentation { name: Default::default(), theme_data: Default::default(), settings_open_changed: Default::default(), + column_settings_open_changed: Default::default(), + column_settings_updated: Default::default(), is_settings_open: Default::default(), is_workspace: Default::default(), theme_config_updated: PubSub::default(), diff --git a/rust/perspective-viewer/src/rust/session/metadata.rs b/rust/perspective-viewer/src/rust/session/metadata.rs index 6558fb125b..d62601bb05 100644 --- a/rust/perspective-viewer/src/rust/session/metadata.rs +++ b/rust/perspective-viewer/src/rust/session/metadata.rs @@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::iter::IntoIterator; use std::ops::{Deref, DerefMut}; +use crate::components::viewer::ColumnLocator; use crate::config::*; use crate::js::perspective::*; use crate::utils::*; @@ -186,6 +187,22 @@ impl SessionMetadata { is_expr.unwrap_or_default() } + /// This function will find a currently existing column. If you want to + /// create a new expression column, use ColumnLocator::Expr(None) + pub fn get_column_locator(&self, name: Option) -> Option { + name.and_then(|name| { + self.as_ref().and_then(|meta| { + if self.is_column_expression(&name) { + Some(ColumnLocator::Expr(Some(name))) + } else { + meta.column_names + .iter() + .find_map(|n| (n == &name).then_some(ColumnLocator::Plain(name.clone()))) + } + }) + }) + } + pub fn get_edit_port(&self) -> Option { self.as_ref().map(|meta| meta.edit_port) } diff --git a/rust/perspective-viewer/test/js/column_settings.spec.ts b/rust/perspective-viewer/test/js/column_settings.spec.ts new file mode 100644 index 0000000000..db93d7dbe5 --- /dev/null +++ b/rust/perspective-viewer/test/js/column_settings.spec.ts @@ -0,0 +1,151 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { test, expect } from "@playwright/test"; +import { PageView } from "@finos/perspective-test"; +import { ColumnSettingsSidebar } from "@finos/perspective-test/src/js/models/column_settings"; + +test.beforeEach(async ({ page }) => { + await page.goto("/tools/perspective-test/src/html/basic-test.html"); + await page.evaluate(async () => { + while (!window["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); +}); +export async function checkTab( + columnSettingsSidebar: ColumnSettingsSidebar, + active: boolean, + expression: boolean +) { + await columnSettingsSidebar.container.waitFor({ + state: "visible", + }); + let titles = await columnSettingsSidebar.tabTitle.all(); + if (active) { + if (expression) { + expect(titles.length).toBe(2); + expect(await titles[0].innerText()).toBe("Attributes"); + expect(await titles[1].innerText()).toBe("Style"); + } else { + expect(titles.length).toBe(1); + expect(await titles[0].innerText()).toBe("Style"); + } + } else { + if (expression) { + expect(titles.length).toBe(1); + expect(await titles[0].innerText()).toBe("Attributes"); + } else { + test.fail( + true, + "No settings exist for non-expression, inactive columns!" + ); + } + } +} + +test.describe("Plugin Styles", () => { + test("Active column edit buttons open sidebar", async ({ page }) => { + let view = new PageView(page); + let settingsPanel = await view.openSettingsPanel(); + let inactiveColumns = settingsPanel.inactiveColumns; + let activeColumns = settingsPanel.activeColumns; + + await settingsPanel.createNewExpression("expr", "true"); + await inactiveColumns.container.waitFor({ + state: "visible", + }); + let exprCol = await activeColumns.activateColumn("expr"); + let firstCol = await activeColumns.getFirstVisibleColumn(); + + firstCol.editBtn.waitFor(); + await firstCol.editBtn.click(); + await checkTab(view.columnSettingsSidebar, true, false); + + await activeColumns.scrollToBottom(); + exprCol.editBtn.waitFor(); + await exprCol.editBtn.click(); + await checkTab(view.columnSettingsSidebar, true, true); + }); + test("Inactive column edit buttons open sidebar", async ({ page }) => { + let view = new PageView(page); + let settingsPanel = await view.openSettingsPanel(); + let inactiveColumns = settingsPanel.inactiveColumns; + let activeColumns = settingsPanel.activeColumns; + + await settingsPanel.createNewExpression("expr", "true"); + let exprCol = await inactiveColumns.getColumnByName("expr"); + await activeColumns.toggleColumn("Row ID"); + let rowId = await inactiveColumns.getColumnByName("Row ID"); + expect(exprCol).toBeDefined(); + expect(rowId).toBeDefined(); + + await exprCol.editBtn.waitFor(); + await rowId.editBtn.waitFor({ state: "detached", timeout: 1000 }); + + await exprCol.editBtn.click(); + await checkTab(view.columnSettingsSidebar, false, true); + }); + test("Click to change tabs", async ({ page }) => { + let view = new PageView(page); + let settingsPanel = await view.openSettingsPanel(); + let sidebar = view.columnSettingsSidebar; + let activeColumns = settingsPanel.activeColumns; + let inactiveColumns = settingsPanel.inactiveColumns; + + await settingsPanel.createNewExpression("expr", "true"); + await activeColumns.activateColumn("expr"); + let col = activeColumns.getColumnByName("expr"); + await inactiveColumns.container.waitFor({ state: "hidden" }); + await activeColumns.scrollToBottom(); + await col.editBtn.click(); + await sidebar.container.waitFor({ state: "visible" }); + await checkTab(sidebar, true, true); + let tabs = await sidebar.tabTitle.all(); + await tabs[1].click(); + await sidebar.styleTab.container.waitFor(); + await tabs[0].click(); + await sidebar.attributesTab.container.waitFor(); + }); + test("Styles don't break on unimplemented plugins", async ({ page }) => { + let view = new PageView(page); + let settingsPanel = await view.openSettingsPanel(); + let sidebar = view.columnSettingsSidebar; + let activeColumns = settingsPanel.activeColumns; + + await settingsPanel.selectPlugin("Sunburst"); + let col = await activeColumns.getFirstVisibleColumn(); + await col.editBtn.click(); + await sidebar.container.waitFor(); + await checkTab(sidebar, true, false); + await sidebar.styleTab.container.waitFor(); + }); + test("View updates don't re-render sidebar", async ({ page }) => { + await page.evaluate(async () => { + let table = await window.__TEST_WORKER__.table({ x: [0] }); + window.__TEST_TABLE__ = table; + let viewer = document.querySelector("perspective-viewer"); + viewer?.load(table); + }); + + let view = new PageView(page); + let settingsPanel = await view.openSettingsPanel(); + let col = settingsPanel.activeColumns.getFirstVisibleColumn(); + await col.editBtn.click(); + await view.columnSettingsSidebar.container.waitFor(); + await page.evaluate(() => { + window.__TEST_TABLE__.update({ x: [1] }); + }); + await page.locator("tbody tr").nth(1).waitFor(); + await expect(view.columnSettingsSidebar.container).toBeVisible(); + }); +}); diff --git a/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts b/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts new file mode 100644 index 0000000000..0df677cc25 --- /dev/null +++ b/rust/perspective-viewer/test/js/column_settings/datagrid.spec.ts @@ -0,0 +1,267 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { + PageView as PspViewer, + compareNodes, + getEventListener, +} from "@finos/perspective-test"; +import { expect, test } from "@playwright/test"; + +let runTests = (title: string, beforeEachAndLocalTests: () => void) => { + return test.describe(title, () => { + beforeEachAndLocalTests.call(this); + + test("Clicking edit button toggles sidebar", async ({ page }) => { + let view = new PspViewer(page); + await view.openSettingsPanel(); + let editBtn = view.dataGrid.regularTable.editBtnRow.first(); + await editBtn.click(); + await view.columnSettingsSidebar.container.waitFor(); + await editBtn.click(); + await view.columnSettingsSidebar.container.waitFor({ + state: "hidden", + }); + }); + test("Toggling a column in the sidebar highlights in the plugin", async ({ + page, + }) => { + let view = new PspViewer(page); + let table = view.dataGrid.regularTable; + let activeColumns = view.settingsPanel.activeColumns; + + await view.openSettingsPanel(); + let col = await activeColumns.getFirstVisibleColumn(); + let name = await col.name.innerText(); + expect(name).toBeDefined(); + + let n = await table.getTitleIdx(name); + expect(n).toBeGreaterThan(-1); + + let nthEditBtn = table.realEditBtns.nth(n); + let selectedEditBtn = table.editBtnRow + .locator(".psp-menu-open") + .first(); + + await col.editBtn.click(); + await selectedEditBtn.waitFor(); + + expect(await compareNodes(nthEditBtn, selectedEditBtn, page)).toBe( + true + ); + + await col.editBtn.click(); + await selectedEditBtn.waitFor({ state: "hidden" }); + }); + test("Scrolling the table horizontally keeps the correct column highlighted", async ({ + page, + }) => { + let view = new PspViewer(page); + let table = view.dataGrid.regularTable; + + let thirdTitle = table.columnTitleRow.locator("th").nth(3); + let thirdEditBtn = table.editBtnRow.locator("th").nth(3); + let selectedTitle = table.columnTitleRow + .locator(".psp-menu-open") + .first(); + let selectedEditBtn = table.editBtnRow + .locator(".psp-menu-open") + .first(); + + await view.openSettingsPanel(); + await table.element.evaluate((node) => (node.scrollLeft = 0)); + await thirdEditBtn.click(); + await selectedEditBtn.waitFor(); + await selectedTitle.waitFor(); + expect( + await compareNodes(thirdEditBtn, selectedEditBtn, page) + ).toBe(true); + expect(await compareNodes(thirdTitle, selectedTitle, page)).toBe( + true + ); + + await table.element.evaluate((node) => (node.scrollLeft = 1000)); + await table.element.evaluate((node) => (node.scrollLeft = 0)); + await selectedEditBtn.waitFor(); + await selectedTitle.waitFor(); + expect( + await compareNodes(thirdEditBtn, selectedEditBtn, page) + ).toBe(true); + expect(await compareNodes(thirdTitle, selectedTitle, page)).toBe( + true + ); + }); + + // These tests only check that a connection is made between the column settings sidebar + // and the plugin itself. They do not need to check the exact contents of the plugin. + test("Numeric styling", async ({ page }) => { + let view = new PspViewer(page); + let table = view.dataGrid.regularTable; + + let col = await view.getOrCreateColumnByType("numeric"); + await col.editBtn.click(); + let name = await col.name.innerText(); + expect(name).toBeTruthy(); + let td = await table.getFirstCellByColumnName(name); + await td.waitFor(); + + // bg style + await view.columnSettingsSidebar.openTab("style"); + let contents = view.columnSettingsSidebar.styleTab.contents; + let checkbox = contents.locator("input[type=checkbox]").last(); + await checkbox.waitFor(); + + let tdStyle = await td.evaluate((node) => node.style.cssText); + let listener = await getEventListener( + page, + "perspective-column-style-change" + ); + await checkbox.click(); + expect(await listener()).toBe(true); + let newStyle = await td.evaluate((node) => node.style.cssText); + expect(tdStyle).not.toBe(newStyle); + }); + test("Calendar styling", async ({ page }) => { + let view = new PspViewer(page); + let table = view.dataGrid.regularTable; + + let col = await view.getOrCreateColumnByType("calendar"); + await col.editBtn.click(); + let name = await col.name.innerText(); + expect(name).toBeTruthy(); + let td = await table.getFirstCellByColumnName(name); + await td.waitFor(); + page.evaluate((name) => console.log(name), name); + + // text style + await view.columnSettingsSidebar.openTab("style"); + let contents = view.columnSettingsSidebar.styleTab.contents; + let checkbox = contents + .locator("input[type=checkbox]:not(:disabled)") + .first(); + let tdStyle = await td.evaluate((node) => { + console.log(node.innerHTML, node.style.cssText); + return node.style.cssText; + }); + let listener = await getEventListener( + page, + "perspective-column-style-change" + ); + await checkbox.waitFor(); + await checkbox.click({ timeout: 100 }); + expect(await listener()).toBe(true); + let newStyle = await td.evaluate((node) => { + console.log(node.innerHTML, node.style.cssText); + return node.style.cssText; + }); + expect(tdStyle).not.toBe(newStyle); + }); + test.skip("Boolean styling", async ({ page }) => { + // Boolean styling is not implemented. + }); + test("String styling", async ({ page }) => { + let view = new PspViewer(page); + let table = view.dataGrid.regularTable; + + let col = await view.getOrCreateColumnByType("string"); + await col.editBtn.click({ timeout: 100 }); + let name = await col.name.innerText(); + expect(name).toBeTruthy(); + let td = await table.getFirstCellByColumnName(name); + await td.waitFor(); + + // bg color + await view.columnSettingsSidebar.openTab("style"); + let contents = view.columnSettingsSidebar.styleTab.contents; + let checkbox = contents.locator("input[type=checkbox]").last(); + await checkbox.waitFor(); + + let tdStyle = await td.evaluate((node) => node.style.cssText); + let listener = await getEventListener( + page, + "perspective-column-style-change" + ); + await checkbox.check(); + expect(await listener()).toBe(true); + let newStyle = await td.evaluate((node) => node.style.cssText); + expect(tdStyle).not.toBe(newStyle); + }); + }); +}; + +runTests("Datagrid Column Styles", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/tools/perspective-test/src/html/basic-test.html"); + await page.evaluate(async () => { + while (!window["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); + }); + + test("Edit highlights go away when view re-draws", async ({ page }) => { + let viewer = new PspViewer(page); + await viewer.openSettingsPanel(); + let btn = await viewer.dataGrid.regularTable.getEditBtnByName("Row ID"); + await btn.click(); + await viewer.settingsPanel.groupby("Ship Mode"); + await viewer.columnSettingsSidebar.container.waitFor({ + state: "detached", + }); + await viewer.dataGrid.regularTable.openColumnEditBtn + .first() + .waitFor({ state: "detached" }); + }); +}); + +// Data grid table header rows look different when a split-by is present. + +// TODO: These tests are failing due to bunk selectors. +runTests("Datagrid Column Styles - Split-by", () => { + test.beforeEach(async ({ page }) => { + await page.goto( + "/tools/perspective-test/src/html/superstore-test.html" + ); + await page.evaluate(async () => { + while (!window["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); + }); + test("Datagrid Column Styles - Only edit buttons get styled", async ({ + page, + }) => { + await page.goto( + "/tools/perspective-test/src/html/superstore-test.html" + ); + await page.evaluate(async () => { + while (!window["__TEST_PERSPECTIVE_READY__"]) { + await new Promise((x) => setTimeout(x, 10)); + } + }); + let viewer = new PspViewer(page); + let headers = viewer.dataGrid.regularTable.table + .locator("thead") + .filter({ + hasNot: page + .locator("#psp-column-titles") + .or(page.locator("#psp-column-edit-buttons")), + has: page.locator("th.psp-menu-open"), + }); + + await viewer.openSettingsPanel(); + let btn = await viewer.dataGrid.regularTable.getEditBtnByName("Sales"); + await expect(btn).toBeVisible(); + await btn.click(); + await expect(headers).not.toBeAttached(); + }); +}); diff --git a/rust/perspective-viewer/test/js/expressions.spec.js b/rust/perspective-viewer/test/js/expressions.spec.js index 9c401d4c60..a19c050549 100644 --- a/rust/perspective-viewer/test/js/expressions.spec.js +++ b/rust/perspective-viewer/test/js/expressions.spec.js @@ -51,10 +51,8 @@ async function type_expression_test(page, expr) { const result = await page.evaluate(async () => { const elem = document .querySelector("perspective-viewer") - .shadowRoot.querySelector(".expr_editor_column"); - return ( - elem.querySelector("button").getAttribute("disabled") || "MISSING" - ); + .shadowRoot.querySelector("#editor-container"); + return elem.querySelector("button").getAttribute("disabled"); }); //await page.evaluate(() => document.activeElement.blur()); @@ -94,7 +92,7 @@ test.describe("Expressions", () => { await page.waitForFunction(() => { const root = document .querySelector("perspective-viewer") - .shadowRoot.querySelector(".expr_editor_column"); + .shadowRoot.querySelector("#editor-container"); return !!root; }); @@ -102,7 +100,7 @@ test.describe("Expressions", () => { const editor = await page.waitForFunction(async () => { const root = document .querySelector("perspective-viewer") - .shadowRoot.querySelector(".expr_editor_column"); + .shadowRoot.querySelector("#editor-container"); return root.querySelector("#content"); }); @@ -127,21 +125,20 @@ test.describe("Expressions", () => { await shadow_click(page, "perspective-viewer", "#add-expression"); - await page.waitForSelector(".expr_editor_column"); + await page.waitForSelector("#editor-container"); await page.evaluate(async () => { let root = document.querySelector("perspective-viewer").shadowRoot; - await root.querySelector("#expr_editor_close_button").click(); + await root.querySelector("#column_settings_close_button").click(); }); - await page.waitForSelector(".expr_editor_column", { + await page.waitForSelector("#editor-container", { state: "hidden", }); const contents = await page.evaluate(async () => { let root = document.querySelector("perspective-viewer").shadowRoot; return ( - root.querySelector(".expr_editor_column")?.innerHTML || - "MISSING" + root.querySelector("#editor-container")?.innerHTML || "MISSING" ); }); @@ -213,7 +210,7 @@ test.describe("Expressions", () => { await shadow_click( page, "perspective-viewer", - ".expr_editor_column", + "#editor-container", "button" ); diff --git a/tools/perspective-test/load-viewer-csv.js b/tools/perspective-test/load-viewer-csv.js index 880ce9da5f..f59d53abb3 100644 --- a/tools/perspective-test/load-viewer-csv.js +++ b/tools/perspective-test/load-viewer-csv.js @@ -19,6 +19,7 @@ async function load() { const worker = perspective.worker(); const table = worker.table(csv); await viewer.load(table); + window.__TEST_WORKER__ = worker; } await load(); diff --git a/tools/perspective-test/load-viewer-superstore.js b/tools/perspective-test/load-viewer-superstore.js new file mode 100644 index 0000000000..ce42eae8f3 --- /dev/null +++ b/tools/perspective-test/load-viewer-superstore.js @@ -0,0 +1,37 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import perspective from "/perspective.js"; + +async function load() { + let resp = await fetch("/@finos/perspective-test/assets/superstore.csv"); + let csv = await resp.text(); + const viewer = document.querySelector("perspective-viewer"); + const worker = perspective.worker(); + const table = worker.table(csv); + await viewer.load(table); + const config = { + plugin: "datagrid", + group_by: ["Region", "State"], + split_by: ["Category", "Sub-Category"], + columns: ["Sales", "Profit"], + master: false, + name: "Sales Report", + table: "superstore", + linked: false, + title: "Sales Report 2", + }; + await viewer.restore(config); +} + +await load(); +window.__TEST_PERSPECTIVE_READY__ = true; diff --git a/tools/perspective-test/results.tar.gz b/tools/perspective-test/results.tar.gz index 05b906841b..1a9d3cf1a1 100644 Binary files a/tools/perspective-test/results.tar.gz and b/tools/perspective-test/results.tar.gz differ diff --git a/tools/perspective-test/src/html/superstore-test.html b/tools/perspective-test/src/html/superstore-test.html new file mode 100644 index 0000000000..ba478794c5 --- /dev/null +++ b/tools/perspective-test/src/html/superstore-test.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tools/perspective-test/src/js/index.ts b/tools/perspective-test/src/js/index.ts index 0dfe100f22..17747d2e11 100644 --- a/tools/perspective-test/src/js/index.ts +++ b/tools/perspective-test/src/js/index.ts @@ -12,3 +12,4 @@ export * from "./utils"; export * from "./simple_viewer_tests"; +export * from "./models/page"; diff --git a/tools/perspective-test/src/js/models/column_settings.ts b/tools/perspective-test/src/js/models/column_settings.ts new file mode 100644 index 0000000000..c4f5e5a3c3 --- /dev/null +++ b/tools/perspective-test/src/js/models/column_settings.ts @@ -0,0 +1,84 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { Locator } from "@playwright/test"; +import { PageView } from "./page"; + +export class ColumnSettingsSidebar { + view: PageView; + container: Locator; + attributesTab: AttributesTab; + styleTab: StyleTab; + closeBtn: Locator; + tabTitle: Locator; + + constructor(view: PageView) { + this.view = view; + const viewer = view.container; + this.container = viewer.locator("#column_settings_sidebar"); + this.attributesTab = new AttributesTab(this.container); + this.styleTab = new StyleTab(this.container); + this.closeBtn = viewer.locator("#column_settings_close_button"); + this.tabTitle = view.container.locator( + ".tab:not(.tab-padding) .tab-title" + ); + } + + async openTab(name: string) { + let locator = this.tabTitle.filter({ hasText: name }); + await locator.click(); + await this.container + .locator(".tab.selected", { hasText: name }) + .waitFor({ timeout: 1000 }); + } +} + +export class AttributesTab { + container: Locator; + expressionEditor: ExpressionEditor; + + constructor(parent: Locator) { + this.container = parent.locator("#attributes-tab"); + this.expressionEditor = new ExpressionEditor(this.container); + } +} + +export class ExpressionEditor { + container: Locator; + content: Locator; + saveBtn: Locator; + resetBtn: Locator; + deleteBtn: Locator; + + constructor(parent: Locator) { + this.container = parent.locator("#editor-container"); + this.content = this.container.locator("#content"); + this.saveBtn = this.container.locator( + "#psp-expression-editor-button-save" + ); + this.resetBtn = this.container.locator( + "#psp-expression-editor-button-reset" + ); + this.saveBtn = this.container.locator( + "#psp-expression-editor-button-delete" + ); + } +} + +export class StyleTab { + container: Locator; + contents: Locator; + constructor(parent: Locator) { + this.container = parent.locator("#style-tab"); + this.contents = parent.locator(".style_contents"); + } +} diff --git a/tools/perspective-test/src/js/models/page.ts b/tools/perspective-test/src/js/models/page.ts new file mode 100644 index 0000000000..6eb9c07134 --- /dev/null +++ b/tools/perspective-test/src/js/models/page.ts @@ -0,0 +1,91 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { Locator, Page, expect } from "@playwright/test"; +import { ColumnSettingsSidebar } from "./column_settings"; +import { ColumnType, SettingsPanel } from "./settings_panel"; +import { DataGridPlugin } from "./plugins"; + +/** + * This class is the primary interface between Playwright tests and the items on the Perspective Viewer. + * It contains various subobjects such as the SettingsPanel and ColumnSettingsSidebar which contain their own + * functionality and locators. + */ +export class PageView { + readonly page: Page; + container: Locator; + settingsPanel: SettingsPanel; + settingsCloseButton: Locator; + /** Opens the settings panel. */ + settingsButton: Locator; + columnSettingsSidebar: ColumnSettingsSidebar; + + // plugins + dataGrid: DataGridPlugin.DataGrid; + + constructor(page: Page) { + this.page = page; + this.container = page.locator("perspective-viewer"); + this.settingsCloseButton = this.container.locator( + "#settings_close_button" + ); + this.settingsButton = this.container.locator("#settings_button"); + this.columnSettingsSidebar = new ColumnSettingsSidebar(this); + this.settingsPanel = new SettingsPanel(this); + + this.dataGrid = new DataGridPlugin.DataGrid(page); + } + + async openSettingsPanel() { + if (await this.settingsPanel.container.isVisible()) { + return this.settingsPanel; + } + await this.settingsButton.click(); + await this.settingsPanel.container.waitFor({ state: "visible" }); + return this.settingsPanel; + } + async closeSettingsPanel() { + await this.settingsCloseButton.click(); + await this.settingsPanel.container.waitFor({ state: "hidden" }); + } + + async getOrCreateColumnByType(type: ColumnType) { + let settingsPanel = this.settingsPanel; + await this.openSettingsPanel(); + let col = settingsPanel.activeColumns.getColumnByType(type); + if (await col.container.isHidden()) { + let expr: string = ""; + switch (type) { + case "string": + expr = "'foo'"; + break; + case "integer": + expr = "1"; + break; + case "float": + expr = "1.1"; + case "date": + expr = "date(0,0,0)"; + case "datetime": + expr = "now()"; + case "numeric": + expr = "1"; + case "calendar": + expr = "now()"; + } + await this.settingsPanel.createNewExpression("expr", expr); + await settingsPanel.activeColumns.activateColumn("expr"); + } + await expect(col.container).toBeVisible(); + return col; + } +} diff --git a/tools/perspective-test/src/js/models/plugins.ts b/tools/perspective-test/src/js/models/plugins.ts new file mode 100644 index 0000000000..c883ff78f0 --- /dev/null +++ b/tools/perspective-test/src/js/models/plugins.ts @@ -0,0 +1,15 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import * as DataGridPlugin from "./plugins/datagrid"; + +export { DataGridPlugin }; diff --git a/tools/perspective-test/src/js/models/plugins/datagrid.ts b/tools/perspective-test/src/js/models/plugins/datagrid.ts new file mode 100644 index 0000000000..decd29c37f --- /dev/null +++ b/tools/perspective-test/src/js/models/plugins/datagrid.ts @@ -0,0 +1,76 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { Locator, Page, expect } from "@playwright/test"; + +export class DataGrid { + element: Locator; + regularTable: RegularTable; + + constructor(page: Page) { + this.element = page.locator("perspective-viewer-datagrid"); + this.regularTable = new RegularTable(this.element); + } +} + +export class RegularTable { + element: Locator; + table: Locator; + columnTitleRow: Locator; + editBtnRow: Locator; + openColumnEditBtn: Locator; + realTitles: Locator; + realEditBtns: Locator; + + constructor(parent: Locator) { + this.element = parent.locator("regular-table"); + this.table = this.element.locator("table"); + this.columnTitleRow = this.element.locator("#psp-column-titles"); + this.editBtnRow = this.element.locator("#psp-column-edit-buttons"); + this.openColumnEditBtn = this.editBtnRow.locator(".psp-menu-open"); + this.realTitles = this.columnTitleRow.locator( + "th:not(.psp-header-group-corner)" + ); + this.realEditBtns = this.editBtnRow.locator( + "th:not(.psp-header-group-corner)" + ); + } + + async getTitleIdx(name: string) { + let ths = await this.realTitles.all(); + for (let [i, locator] of ths.entries()) { + if ((await locator.innerText()) === name) { + this.element.evaluate( + (_, i) => console.log("getTitleIdx returned:", i), + i + ); + return i; + } + } + return -1; + } + + async getEditBtnByName(name: string) { + let n = await this.getTitleIdx(name); + expect(n).not.toBe(-1); + return this.editBtnRow.locator("th").nth(n); + } + /** + * Takes the name of a column and returns a locator for the first corresponding TD in the body. + * @param name + */ + async getFirstCellByColumnName(name: string) { + let n = await this.getTitleIdx(name); + expect(n).not.toBe(-1); + return this.table.locator("tbody tr").first().locator("td").nth(n); + } +} diff --git a/tools/perspective-test/src/js/models/settings_panel.ts b/tools/perspective-test/src/js/models/settings_panel.ts new file mode 100644 index 0000000000..25c9ec2522 --- /dev/null +++ b/tools/perspective-test/src/js/models/settings_panel.ts @@ -0,0 +1,331 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { Locator, Page, expect, test } from "@playwright/test"; +import { PageView } from "./page"; + +type ViewParameter = "groupby" | "splitby" | "orderby" | "where"; + +export class SettingsPanel { + pageView: PageView; + container: Locator; + closeButton: Locator; + activeColumns: ActiveColumns; + inactiveColumns: InactiveColumns; + addExpressionButton: Locator; + pluginSelector: Locator; + groupbyInput: Locator; + splitbyInput: Locator; + orderbyInput: Locator; + whereInput: Locator; + + constructor(view: PageView) { + this.pageView = view; + const viewer = view.container; + this.container = viewer.locator("#settings_panel"); + this.closeButton = viewer.locator("#settings_close_button"); + this.activeColumns = new ActiveColumns(this.pageView); + this.inactiveColumns = new InactiveColumns(this.pageView); + this.addExpressionButton = viewer.locator("#add-expression"); + this.pluginSelector = viewer.locator("#plugin_selector_container"); + this.groupbyInput = viewer.locator("#group_by input"); + this.splitbyInput = viewer.locator("#split_by input"); + this.orderbyInput = viewer.locator("#sort input"); + this.whereInput = viewer.locator("#filter input"); + } + /** + * Creates and saves a new expression column. + * @param expr + */ + async createNewExpression(name: string, expr: string) { + await this.activeColumns.scrollToBottom(); + await this.addExpressionButton.click(); + let exprEditor = + this.pageView.columnSettingsSidebar.attributesTab.expressionEditor; + await exprEditor.container.waitFor({ + state: "visible", + }); + expect(await exprEditor.content.isVisible()).toBe(true); + // brute force clear the expression editor + for (let i = 0; i < 30; i++) { + await exprEditor.content.press("Backspace", { delay: 10 }); + } + await exprEditor.content.type(`//${name}\n${expr}`, { delay: 100 }); + await exprEditor.content.blur(); + let saveBtn = this.pageView.page.locator( + "#psp-expression-editor-button-save" + ); + expect(await saveBtn.isDisabled()).toBe(false); + await saveBtn.click(); + await this.inactiveColumns.getColumnByName(name); + } + /** + * Shorthand for setViewParamter("groupby", name) + */ + async groupby(name: string) { + return await this.setViewParameter("groupby", name); + } + /** + * Shorthand for setViewParamter("splitby", name) + */ + async splitby(name: string) { + return await this.setViewParameter("splitby", name); + } + /** + * Shorthand for setViewParamter("orderby", name) + */ + async orderby(name: string) { + return await this.setViewParameter("orderby", name); + } + /** + * Shorthand for setViewParamter("where", name) + */ + async where(name: string) { + return await this.setViewParameter("where", name); + } + /** + * Sets a view parameter ("groupby", "splitby", "orderby", or "where") to the specified column name. + * @param type + * @param name + */ + async setViewParameter(type: ViewParameter, name: string) { + let locator: Locator; + switch (type) { + case "groupby": + locator = this.groupbyInput; + break; + case "orderby": + locator = this.orderbyInput; + break; + case "splitby": + locator = this.splitbyInput; + break; + case "where": + locator = this.whereInput; + break; + default: + throw "Invalid type passed!"; + } + await locator.type(name); + await this.pageView.page + .locator("perspective-dropdown .selected") + .first() // NOTE: There probably shouldn't actually be more than one. + .waitFor(); + await locator.press("Enter"); + } + /** + * Selects a plugin by it's display name, i.e. the innerText of the .plugin-select-item + * @param name + */ + async selectPlugin(name: string) { + await this.pluginSelector.click(); + await this.pluginSelector + .locator(".plugin-select-item") + .filter({ hasText: name }) + .click(); + } +} + +export class ColumnSelector { + active: boolean; + name: Locator; + container: Locator; + editBtn: Locator; + aggSelector: Locator; + + constructor(container: Locator, active: boolean) { + this.container = container; + this.active = active; + this.name = container.locator("div .column_name"); + this.aggSelector = container.locator("select"); + this.editBtn = container.locator("div .expression-edit-button"); + } +} + +export type ColumnType = + | "integer" + | "float" + | "string" + | "date" + | "datetime" + | "numeric" + | "calendar"; + +// TODO: Consolidate this and InactiveColumns into a super class +export class ActiveColumns { + view: PageView; + page: Page; + container: Locator; + topPanel: Locator; + columnSelector: Locator; + newColumnInput: Locator; + + constructor(view: PageView) { + this.page = view.page; + this.view = view; + this.container = view.container.locator("#active-columns"); + this.topPanel = view.container.locator("#top_panel"); + this.columnSelector = view.container.locator( + "#active-columns :not(.top-panel) .column-selector-column" + ); + this.newColumnInput = view.container.locator( + ".column-selector-column .column-empty input" + ); + } + + getFirstVisibleColumn() { + return new ColumnSelector(this.columnSelector.first(), true); + } + + getColumnByName(name: string) { + let locator = this.columnSelector.filter({ + hasText: name, + }); + return new ColumnSelector(locator, true); + } + + /** + * Gets the first visible column matching the passed in type. + * @param type - A string to denote the type. Use "numeric" to mean "integer or float" and "calendar" to denote "date or datetime" + */ + getColumnByType(type: ColumnType) { + let page = this.view.page; + let has: Locator; + switch (type) { + case "numeric": + has = page.locator(".float").or(page.locator(".integer")); + break; + case "calendar": + has = page.locator(".date").or(page.locator(".datetime")); + break; + default: + has = page.locator(`.${type}`); + break; + } + return new ColumnSelector( + this.columnSelector.filter({ has }).first(), + true + ); + } + + async visibleColumns() { + let all = await this.columnSelector.all(); + let mapped = all.map((locator) => { + return new ColumnSelector(locator, true); + }); + return mapped; + } + + async scrollToTop() { + this.container.focus(); + await this.container.evaluate((node) => (node.scrollTop = 0)); + } + async scrollToBottom() { + this.container.focus(); + await this.container.evaluate( + (node) => (node.scrollTop = node.scrollHeight) + ); + } + + async toggleColumn(name: string) { + let has = this.view.page.getByText(name); + this.columnSelector + .filter({ has }) + .locator(".is_column_active") + .click(); + } + /** + * This function will use the empty input column to activate a column. + * During this process, it will scroll ActiveColumns to the bottom. + * @param name + */ + async activateColumn(name: string) { + await this.scrollToBottom(); + await this.newColumnInput.waitFor({ state: "visible" }); + await this.newColumnInput.type(name); + await this.page + .locator("perspective-dropdown .selected") + .first() // NOTE: There probably shouldn't actually be more than one. + .waitFor(); + await this.newColumnInput.press("Enter"); + let addedColumn = this.columnSelector.filter({ hasText: name }).first(); + return new ColumnSelector(addedColumn!, true); + } +} + +export class InactiveColumns { + view: PageView; + container: Locator; + columnSelector: Locator; + + constructor(view: PageView) { + this.view = view; + this.container = view.container.locator("#sub-columns"); + this.columnSelector = view.container.locator(".column-selector-column"); + } + + async getColumnByName(name: string) { + await this.container.waitFor({ state: "visible" }); + let locator = this.columnSelector.filter({ hasText: name }); + return new ColumnSelector(locator, true); + } + async visibleColumns() { + let all = await this.columnSelector.all(); + let mapped = all.map((locator) => { + return new ColumnSelector(locator, false); + }); + return mapped; + } + /** + * This function will click the toggle next to the inactive column, + * making it the only active column. + * @param name + */ + async toggleColumn(name: string) { + this.columnSelector + .filter({ has: this.container.getByText(name) }) + .locator(".is_column_active") + .click(); + } + /** + * Gets the first visible column matching the passed in type. + * @param type - A string to denote the type. Use "numeric" to mean "integer or float" and "calendar" to denote "date or datetime" + */ + getColumnByType( + type: + | "integer" + | "float" + | "string" + | "date" + | "datetime" + | "numeric" + | "calendar" + ) { + let page = this.view.page; + let has: Locator; + switch (type) { + case "numeric": + has = page.locator(".float").or(page.locator(".integer")); + break; + case "calendar": + has = page.locator(".date").or(page.locator(".datetime")); + break; + default: + has = page.locator(`.${type}`); + break; + } + return new ColumnSelector( + this.columnSelector.filter({ has }).first(), + true + ); + } +} diff --git a/tools/perspective-test/src/js/utils.ts b/tools/perspective-test/src/js/utils.ts index b1c0c7e5af..26dd801507 100644 --- a/tools/perspective-test/src/js/utils.ts +++ b/tools/perspective-test/src/js/utils.ts @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import { expect, Page } from "@playwright/test"; +import { expect, Locator, Page } from "@playwright/test"; /** * Clean a `` for serialization/comparison. @@ -113,6 +113,13 @@ export const getSvgContentString = (selector: string) => async (page: Page) => { return content; }; +/** + * Compares the content of an HTML element to a snapshot. + * To generate new snapshots, you need to delete ../tools/, ../dist/ and ../results.tar.gz + * and run the tests again. + * @param contents + * @param snapshotPath + */ export async function compareContentsToSnapshot( contents: string, snapshotPath: string[] @@ -159,6 +166,12 @@ export async function compareShadowDOMContents(page, snapshotFileName) { await compareContentsToSnapshot(contents, [snapshotFileName]); } +/** + * Clicks on an element, passing through shadow roots if necessary. + * TODO: Playwright already does this with locators. + * @param page + * @param path + */ export async function shadow_click(page, ...path): Promise { await page.evaluate( ({ path }) => { @@ -188,6 +201,14 @@ export async function shadow_click(page, ...path): Promise { ); } +/** + * Types in an element, passing through shadow roots if necessary. + * TODO: Playwright already does this with locators. + * @param page + * @param content + * @param is_incremental + * @param path + */ export async function shadow_type( page, content, @@ -246,6 +267,11 @@ export async function shadow_type( ); } +/** + * Blurs the active element on the page, passing through shadow roots. + * TODO: Playwright already does this with locators. + * @param page + */ export async function shadow_blur(page): Promise { await page.evaluate(() => { let elem = document.activeElement; @@ -256,3 +282,52 @@ export async function shadow_blur(page): Promise { } }); } + +/** + * Compares two locators to see if they match the same node in the DOM. + * @param left + * @param right + * @param page + */ +export async function compareNodes(left: Locator, right: Locator, page: Page) { + let leftEl = await left.elementHandle(); + let rightEl = await right.elementHandle(); + return await page.evaluate( + async (compare) => { + return compare.leftEl?.isEqualNode(compare.rightEl) || false; + }, + { + leftEl, + rightEl, + } + ); +} + +/** + * Adds an event listener and returns a handle which, when awaited, will check if the event has been triggered. + * @param page + * @param event + */ +export async function getEventListener(page: Page, eventName: string) { + let hasListener = await page.evaluate((eventName) => { + let viewer = document.querySelector("perspective-viewer"); + if (!viewer) { + return false; + } else { + viewer.addEventListener(eventName, async (event) => { + window.__PSP_TEST_LAST_EVENT__ = eventName; + }); + return true; + } + }, eventName); + expect(hasListener).toBe(true); + + // TODO: This should wait for the event to fire instead of just checking. + // That would require something like an abort handler and a timeout, + // or to race two promises, or something like that. I wasn't able to figure it out but this still works. + return async () => + await page.evaluate( + (eventName) => window.__PSP_TEST_LAST_EVENT__ === eventName, + eventName + ); +} diff --git a/yarn.lock b/yarn.lock index 70078d434d..c245d9265f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3989,12 +3989,14 @@ aggregate-error "^3.1.0" "@playwright/test@^1.30.0": - version "1.30.0" - resolved "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz" - integrity sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw== + version "1.37.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.37.1.tgz#e7f44ae0faf1be52d6360c6bbf689fd0057d9b6f" + integrity sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg== dependencies: "@types/node" "*" - playwright-core "1.30.0" + playwright-core "1.37.1" + optionalDependencies: + fsevents "2.3.2" "@polka/url@^1.0.0-next.20": version "1.0.0-next.21" @@ -4446,7 +4448,12 @@ resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/node@*", "@types/node@>= 8": +"@types/node@*": + version "20.5.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a" + integrity sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ== + +"@types/node@>= 8": version "14.14.21" resolved "https://registry.npmjs.org/@types/node/-/node-14.14.21.tgz" integrity sha512-cHYfKsnwllYhjOzuC5q1VpguABBeecUp24yFluHpn/BQaVxB1CuQ1FSRZCzrPxrkIfWISXV2LbeoBthLWg0+0A== @@ -5075,7 +5082,7 @@ ansi-regex@^6.0.1: ansi-sequence-parser@^1.1.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz#e0aa1cdcbc8f8bb0b5bca625aac41f5f056973cf" + resolved "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz" integrity sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg== ansi-styles@^2.2.1: @@ -8985,16 +8992,16 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fsevents@2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + fsevents@~2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz" integrity sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw== -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" @@ -10710,7 +10717,7 @@ level@^6.0.1: leveldown@^5.4.0: version "5.6.0" - resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" + resolved "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz" integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ== dependencies: abstract-leveldown "~6.2.1" @@ -11113,7 +11120,7 @@ marked@^4.0.17: marked@^4.3.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" + resolved "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== math-expression-evaluator@^1.2.14: @@ -11385,7 +11392,7 @@ minimatch@^5.1.2: minimatch@^9.0.0: version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" @@ -11547,7 +11554,7 @@ nanoid@^3.1.23, nanoid@^3.3.6: napi-macros@~2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" + resolved "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz" integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg== ncp@~2.0.0: @@ -11648,7 +11655,7 @@ node-forge@^1: node-gyp-build@~4.1.0: version "4.1.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" + resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz" integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ== node-releases@^1.1.69: @@ -12354,10 +12361,10 @@ pkginfo@0.4.1: resolved "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz" integrity sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ== -playwright-core@1.30.0: - version "1.30.0" - resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz" - integrity sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g== +playwright-core@1.37.1: + version "1.37.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126" + integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA== plist@^3.0.1: version "3.0.1" @@ -14531,7 +14538,7 @@ shelljs@^0.8.5: shiki@^0.14.1: version "0.14.3" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.3.tgz#d1a93c463942bdafb9866d74d619a4347d0bbf64" + resolved "https://registry.npmjs.org/shiki/-/shiki-0.14.3.tgz" integrity sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g== dependencies: ansi-sequence-parser "^1.1.0" @@ -15042,7 +15049,7 @@ stylehacks@^5.1.1: superstore-arrow@3.0.0, superstore-arrow@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/superstore-arrow/-/superstore-arrow-3.0.0.tgz#13666348ab28c70ce14d1984e4eb67fac99aa94e" + resolved "https://registry.npmjs.org/superstore-arrow/-/superstore-arrow-3.0.0.tgz" integrity sha512-ejqksUs3HEIcp8808T6jBvjecT3NHFoivtXheqXpSMv7GRP+G3+CReTulMfCfxuMOhI4rHag2oSyaX9OyLisFQ== supports-color@^2.0.0: @@ -15574,7 +15581,7 @@ typedarray-to-buffer@^3.1.5: typedoc-plugin-markdown@^3.15.4: version "3.15.4" - resolved "https://registry.yarnpkg.com/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.15.4.tgz#928755d9cf8e2f12c98229920a251760a82247b6" + resolved "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-3.15.4.tgz" integrity sha512-KpjFL/NDrQAbY147oIoOgob2vAdEchsMcTVd6+e6H2lC1l5xhi48bhP/fMJI7qYQ8th5nubervgqw51z7gY66A== dependencies: handlebars "^4.7.7" @@ -15586,7 +15593,7 @@ typedoc-plugin-no-inherit@^1.4.0: typedoc@^0.24.8: version "0.24.8" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.24.8.tgz#cce9f47ba6a8d52389f5e583716a2b3b4335b63e" + resolved "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz" integrity sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w== dependencies: lunr "^2.3.9" @@ -15596,7 +15603,7 @@ typedoc@^0.24.8: typescript@5.1.6: version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz" integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== typescript@~4.1.3: