From 0676046fd68f3fc7e704aa6da2186dc0bc93cc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 10 Sep 2024 18:55:35 +0200 Subject: [PATCH 1/7] Add simple nestedEditor for tabulator --- panel/models/tabulator.ts | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index aaa613fe5d..fcdde1765e 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -302,6 +302,67 @@ const datetimeEditor = function(cell: any, onRendered: any, success: any, cancel return input } +const nestedEditor = function(cell: any, onRendered: any, success: any, cancel: any, editorParams: any) { + //cell - the cell component for the editable cell + //onRendered - function to call when the editor has been rendered + //success - function to call to pass the successfully updated value to Tabulator + //cancel - function to call to abort the edit and return to a normal cell + + //create and style input + const cellValue = cell.getValue() + const row = cell.getRow().getData() + + let options = editorParams.values + for (const i of editorParams.lookup_order) { + options = options[row[i]] + if (Array.isArray(options)) { + break + } + } + + const select = document.createElement("select") + for (const option of options) { + const opt = document.createElement("option") + opt.value = option + opt.text = option + opt.selected = option === cellValue + select.appendChild(opt) + } + + select.style.padding = "4px" + select.style.width = "100%" + select.style.boxSizing = "border-box" + + select.value = cellValue + + const show = () => { + select.focus() + select.style.height = "100%" + } + onRendered(show) + + function onChange() { + success(select.value) + } + + //submit new value on blur or change + select.addEventListener("blur", onChange) + select.addEventListener("change", onChange) + + //submit new value on enter + select.addEventListener("keydown", (e) => { + if (e.key == "Enter") { + setTimeout(onChange, 100) + } + + if (e.key == "Escape") { + setTimeout(cancel, 100) + } + }) + + return select +} + function find_column(group: any, field: string): any { if (group.columns != null) { for (const col of group.columns) { @@ -955,6 +1016,8 @@ export class DataTabulatorView extends HTMLBoxView { tab_column.editor = dateEditor } else if (tab_column.editor === "datetime") { tab_column.editor = datetimeEditor + } else if (tab_column.editor === "nested") { + tab_column.editor = nestedEditor } } else if (ctype === "StringEditor") { if (editor.completions.length > 0) { From 91c25a6334e61683f5b93f7d02b4a31714722496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 11 Sep 2024 15:47:36 +0200 Subject: [PATCH 2/7] Use filtering for list instead --- panel/models/tabulator.ts | 62 ++++++--------------------------------- 1 file changed, 9 insertions(+), 53 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index fcdde1765e..6085e3ad0e 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -302,65 +302,18 @@ const datetimeEditor = function(cell: any, onRendered: any, success: any, cancel return input } -const nestedEditor = function(cell: any, onRendered: any, success: any, cancel: any, editorParams: any) { +const nestedEditor = function(cell: any, editorParams: any) { //cell - the cell component for the editable cell - //onRendered - function to call when the editor has been rendered - //success - function to call to pass the successfully updated value to Tabulator - //cancel - function to call to abort the edit and return to a normal cell - //create and style input - const cellValue = cell.getValue() const row = cell.getRow().getData() - - let options = editorParams.values + let values = editorParams.options for (const i of editorParams.lookup_order) { - options = options[row[i]] - if (Array.isArray(options)) { + values = values[row[i]] + if (Array.isArray(values)) { break } } - - const select = document.createElement("select") - for (const option of options) { - const opt = document.createElement("option") - opt.value = option - opt.text = option - opt.selected = option === cellValue - select.appendChild(opt) - } - - select.style.padding = "4px" - select.style.width = "100%" - select.style.boxSizing = "border-box" - - select.value = cellValue - - const show = () => { - select.focus() - select.style.height = "100%" - } - onRendered(show) - - function onChange() { - success(select.value) - } - - //submit new value on blur or change - select.addEventListener("blur", onChange) - select.addEventListener("change", onChange) - - //submit new value on enter - select.addEventListener("keydown", (e) => { - if (e.key == "Enter") { - setTimeout(onChange, 100) - } - - if (e.key == "Escape") { - setTimeout(cancel, 100) - } - }) - - return select + return values } function find_column(group: any, field: string): any { @@ -1017,7 +970,10 @@ export class DataTabulatorView extends HTMLBoxView { } else if (tab_column.editor === "datetime") { tab_column.editor = datetimeEditor } else if (tab_column.editor === "nested") { - tab_column.editor = nestedEditor + tab_column.editorParams.valuesLookup = (cell: any) => { + return nestedEditor(cell, tab_column.editorParams) + } + tab_column.editor = "list" } } else if (ctype === "StringEditor") { if (editor.completions.length > 0) { From 1b252247730d6cc9b12e3442ae7b73033d278f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 12 Sep 2024 09:21:11 +0200 Subject: [PATCH 3/7] Add UI test --- panel/tests/ui/widgets/test_tabulator.py | 43 ++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/panel/tests/ui/widgets/test_tabulator.py b/panel/tests/ui/widgets/test_tabulator.py index 4ba7cc8bde..bd846eecd9 100644 --- a/panel/tests/ui/widgets/test_tabulator.py +++ b/panel/tests/ui/widgets/test_tabulator.py @@ -728,6 +728,49 @@ def test_tabulator_editors_tabulator_multiselect(page, exception_handler_accumul assert not exception_handler_accumulator +@pytest.mark.parametrize("opt0", ['A', 'B']) +@pytest.mark.parametrize("opt1", ["1", "2"]) +def test_tabulator_editors_nested(page, opt0, opt1): + df = pd.DataFrame({"0": ["A"], "1": [1], "2": [None]}) + + options = { + "A": list(range(5)), + "B": { "1": list(range(5, 10)), "2": list(range(10, 15))}, + } + tabulator_editors = { + "0": {"type": "list", "values": ["A", "B"]}, + "1": {"type": "list", "values": [1, 2]}, + "2": {"type": "nested", "options": options, "lookup_order": ["0", "1"]}, + } + + widget = Tabulator(df, editors=tabulator_editors, show_index=False) + serve_component(page, widget) + + cells = page.locator('.tabulator-cell.tabulator-editable') + expect(cells).to_have_count(3) + + # Change the 0th column + cells.nth(0).click() + item = page.locator('.tabulator-edit-list-item', has_text=opt0) + expect(item).to_have_count(1) + item.click() + + # Change the 1th column + cells.nth(1).click() + item = page.locator('.tabulator-edit-list-item', has_text=opt1) + expect(item).to_have_count(1) + item.click() + + # Check the last column matches + cells.nth(2).click() + items = page.locator('.tabulator-edit-list-item') + expect(items).to_have_count(5) + + items_text = items.all_inner_texts() + expected = options[opt0][opt1] if opt0 == "B" else options[opt0] + assert items_text == list(map(str, expected)) + + @pytest.mark.parametrize('layout', Tabulator.param['layout'].objects) def test_tabulator_column_layouts(page, df_mixed, layout): widget = Tabulator(df_mixed, layout=layout) From 326594e05e976e6ace3e85050129c302ccee6052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 12 Sep 2024 09:48:52 +0200 Subject: [PATCH 4/7] Add documentation to nested editor --- examples/reference/widgets/Tabulator.ipynb | 56 ++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index 5d29793032..f372787606 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -265,6 +265,62 @@ "edit_table.on_edit(lambda e: print(e.column, e.row, e.old, e.value))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Nested editor\n", + "Suppose you want an editor to depend on values in another cell. The `nested` type can be used. The `nested` type needs two arguments, `options` and `lookup_order`; the latter describes how the `options` should be looked up. \n", + "\n", + "Let's create a simple DataFrame with three columns, the `2` column now depends on the values in the `0` and `1` column. If the `0` is `A`, the `2` column should always be between 1 and 5. If the `0` column is `B`, the `2` column will now also depend on the `1` column. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "options = {\n", + " \"A\": [1, 2, 3, 4, 5],\n", + " \"B\": {\"1\": [6, 7, 8, 9, 10], \"2\": [11, 12, 13, 14, 15]},\n", + "}\n", + "tabulator_editors = {\n", + " \"0\": {\"type\": \"list\", \"values\": [\"A\", \"B\"]},\n", + " \"1\": {\"type\": \"list\", \"values\": [1, 2]},\n", + " \"2\": {\"type\": \"nested\", \"options\": options, \"lookup_order\": [\"0\", \"1\"]},\n", + "}\n", + "\n", + "nested_df = pd.DataFrame({\"0\": [\"A\", \"B\"], \"1\": [1, 2], \"2\": [None, None]})\n", + "nested_table = pn.widgets.Tabulator(nested_df, editors=tabulator_editors, show_index=False)\n", + "nested_table" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some things to note about the `nested` editor:\n", + "- Only string keys can be used in `options` dictionary. \n", + "- Care must be taken so there is always a valid option for the `nested` editor.\n", + "- No guarantee is made that the value shown is a `nested` editor is a valid option.\n", + "\n", + "For the last point, you can use an `on_edit` callback, which either change the value or clear it. Below is an example of how to clear it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def clear_nested_column(event):\n", + " if event.column in [\"0\", \"1\"]:\n", + " nested_table.patch({\"2\": [(event.row, None)]})\n", + "\n", + "nested_table.on_edit(clear_nested_column)" + ] + }, { "cell_type": "markdown", "metadata": {}, From dd056d9d32090d52fdcc038aed989dcabd4bf019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 12 Sep 2024 09:49:54 +0200 Subject: [PATCH 5/7] Add TABULATOR_VERSION to links --- examples/reference/widgets/Tabulator.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index f372787606..24843535a3 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -185,7 +185,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The list of valid *Tabulator* formatters can be found in the [Tabulator documentation](https://tabulator.info/docs/5.5/format#format-builtin).\n", + "The list of valid *Tabulator* formatters can be found in the [Tabulator documentation](https://tabulator.info/docs/{{TABULATOR_VERSION}}/format#format-builtin).\n", "\n", "Note that the equivalent specification may also be applied for column titles using the `title_formatters` parameter (but does not support Bokeh `CellFormatter` types)." ] @@ -1369,7 +1369,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "These and other available *Tabulator* options are listed at http://tabulator.info/docs/5.4/options. \n", + "These and other available *Tabulator* options are listed at http://tabulator.info/docs/{{TABULATOR_VERSION}}/options. \n", "\n", "Obviously not all options will work though, especially any settable callbacks and options which are set by the internal Panel tabulator module (for example the `columns` option).\n", "Additionally it should be noted that the configuration parameter is not responsive so it can only be set at instantiation time." From 3b93cd2de70e2676fc7c8839d90cbb9ae6e41f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 12 Sep 2024 09:52:20 +0200 Subject: [PATCH 6/7] Remove column note in static configuration https://github.com/holoviz/panel/pull/7241 --- examples/reference/widgets/Tabulator.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/reference/widgets/Tabulator.ipynb b/examples/reference/widgets/Tabulator.ipynb index 24843535a3..a002a1b5e1 100644 --- a/examples/reference/widgets/Tabulator.ipynb +++ b/examples/reference/widgets/Tabulator.ipynb @@ -1371,7 +1371,7 @@ "source": [ "These and other available *Tabulator* options are listed at http://tabulator.info/docs/{{TABULATOR_VERSION}}/options. \n", "\n", - "Obviously not all options will work though, especially any settable callbacks and options which are set by the internal Panel tabulator module (for example the `columns` option).\n", + "Obviously not all options will work though, especially any settable callbacks and options which are set by the internal Panel tabulator module.\n", "Additionally it should be noted that the configuration parameter is not responsive so it can only be set at instantiation time." ] } From 81389c8ae3df8f03ab7f6cda85c191edc0439907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 12 Sep 2024 11:20:11 +0200 Subject: [PATCH 7/7] Handle no entry options --- panel/models/tabulator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 6085e3ad0e..97485cf6f5 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -308,12 +308,12 @@ const nestedEditor = function(cell: any, editorParams: any) { const row = cell.getRow().getData() let values = editorParams.options for (const i of editorParams.lookup_order) { - values = values[row[i]] + values = row[i] in values ? values[row[i]] : [] if (Array.isArray(values)) { break } } - return values + return values ? values : [] } function find_column(group: any, field: string): any {