From 789bb528f43f7cd28eca2e52a309eddcbb3c370b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 12 Oct 2023 11:58:25 +0100 Subject: [PATCH 01/14] Add basic inline searching and fix create first row popup --- .../components/grid/cells/HeaderCell.svelte | 164 ++++++++++++++---- .../src/components/grid/layout/NewRow.svelte | 6 +- .../src/components/grid/stores/filter.js | 40 +++++ .../src/components/grid/stores/rows.js | 6 + 4 files changed, 185 insertions(+), 31 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 5ac70c93c8..1abddfe1ff 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -3,6 +3,7 @@ import GridCell from "./GridCell.svelte" import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui" import { getColumnIcon } from "../lib/utils" + import { debounce } from "../../../utils/utils" export let column export let idx @@ -23,6 +24,8 @@ definition, datasource, schema, + focusedCellId, + filter, } = getContext("grid") const bannedDisplayColumnTypes = [ @@ -32,12 +35,15 @@ "boolean", "json", ] + const searchableTypes = ["string", "options", "number"] let anchor let open = false let editIsOpen = false let timeout let popover + let searchValue + let input $: sortedBy = column.name === $sort.column $: canMoveLeft = orderable && idx > 0 @@ -48,6 +54,9 @@ $: descendingLabel = ["number", "bigint"].includes(column.schema?.type) ? "high-low" : "Z-A" + $: searchable = searchableTypes.includes(column.schema.type) + $: searching = searchValue != null + $: debouncedUpdateFilter(searchValue) const editColumn = async () => { editIsOpen = true @@ -148,12 +157,46 @@ }) } + const startSearching = async () => { + $focusedCellId = null + searchValue = "" + await tick() + input?.focus() + } + + const onInputKeyDown = e => { + if (e.key === "Enter") { + updateFilter() + } else if (e.key === "Escape") { + input?.blur() + } + } + + const stopSearching = () => { + searchValue = null + updateFilter() + } + + const onBlurInput = () => { + if (searchValue === "") { + searchValue = null + } + updateFilter() + } + + const updateFilter = () => { + filter.actions.addInlineFilter(column, searchValue) + } + const debouncedUpdateFilter = debounce(updateFilter, 250) + onMount(() => subscribe("close-edit-column", cancelEdit))
- + {#if searching} + focusedCellId.set(null)} + on:keydown={onInputKeyDown} + /> + {/if} + +
+ +
+
+ +
+
{column.label}
- {#if sortedBy} -
- + + {#if searching} +
+ +
+ {:else} + {#if sortedBy} +
+ +
+ {/if} +
(open = true)}> +
{/if} -
(open = true)}> - -
@@ -289,6 +350,29 @@ background: var(--grid-background-alt); } + /* Icon colors */ + .header-cell :global(.spectrum-Icon) { + color: var(--spectrum-global-color-gray-600); + } + .header-cell :global(.spectrum-Icon.hoverable:hover) { + color: var(--spectrum-global-color-gray-800) !important; + cursor: pointer; + } + + /* Search icon */ + .search-icon { + display: none; + } + .header-cell.searchable:not(.open):hover .search-icon, + .header-cell.searchable.searching .search-icon { + display: block; + } + .header-cell.searchable:not(.open):hover .column-icon, + .header-cell.searchable.searching .column-icon { + display: none; + } + + /* Main center content */ .name { flex: 1 1 auto; width: 0; @@ -296,23 +380,45 @@ text-overflow: ellipsis; overflow: hidden; } + .header-cell.searching .name { + opacity: 0; + pointer-events: none; + } + input { + display: none; + font-family: var(--font-sans); + outline: none; + border: 1px solid transparent; + background: transparent; + color: var(--ink); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + padding: 0 30px; + border-radius: 2px; + } + input:focus { + border: 1px solid var(--accent-color); + } + input:not(:focus) { + background: var(--spectrum-global-color-gray-200); + } + .header-cell.searching input { + display: block; + } - .more { + /* Right icons */ + .more-icon { display: none; padding: 4px; margin: 0 -4px; } - .header-cell.open .more, - .header-cell:hover .more { + .header-cell.open .more-icon, + .header-cell:hover .more-icon { display: block; } - .more:hover { - cursor: pointer; - } - .more:hover :global(.spectrum-Icon) { - color: var(--spectrum-global-color-gray-800) !important; - } - .header-cell.open .sort-indicator, .header-cell:hover .sort-indicator { display: none; diff --git a/packages/frontend-core/src/components/grid/layout/NewRow.svelte b/packages/frontend-core/src/components/grid/layout/NewRow.svelte index aa9b6fa051..440e15ee0c 100644 --- a/packages/frontend-core/src/components/grid/layout/NewRow.svelte +++ b/packages/frontend-core/src/components/grid/layout/NewRow.svelte @@ -27,8 +27,10 @@ rowVerticalInversionIndex, columnHorizontalInversionIndex, selectedRows, - loading, + loaded, + refreshing, config, + filter, } = getContext("grid") let visible = false @@ -153,7 +155,7 @@ {#if !visible && !selectedRowCount && $config.canAddRows} diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.js index a59c98ccdd..a2de0ca2d0 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.js @@ -11,6 +11,46 @@ export const createStores = context => { } } +export const createActions = context => { + const { filter } = context + + const addInlineFilter = (column, value) => { + const filterId = `inline-${column}` + + const inlineFilter = { + field: column.name, + id: filterId, + operator: "equal", + type: "string", + valueType: "value", + value, + } + + filter.update($filter => { + // Remove any existing inline filter + if ($filter?.length) { + $filter = $filter?.filter(x => x.id !== filterId) + } + + // Add new one if a value exists + if (value) { + $filter = [...($filter || []), inlineFilter] + } + + return $filter + }) + } + + return { + filter: { + ...filter, + actions: { + addInlineFilter, + }, + }, + } +} + export const initialise = context => { const { filter, initialFilter } = context diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 49adb62936..98e64d7acb 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -8,6 +8,7 @@ export const createStores = () => { const rows = writable([]) const loading = writable(false) const loaded = writable(false) + const refreshing = writable(false) const rowChangeCache = writable({}) const inProgressChanges = writable({}) const hasNextPage = writable(false) @@ -53,6 +54,7 @@ export const createStores = () => { fetch, rowLookupMap, loaded, + refreshing, loading, rowChangeCache, inProgressChanges, @@ -82,6 +84,7 @@ export const createActions = context => { notifications, fetch, isDatasourcePlus, + refreshing, } = context const instanceLoaded = writable(false) @@ -176,6 +179,9 @@ export const createActions = context => { // Notify that we're loaded loading.set(false) } + + // Update refreshing state + refreshing.set($fetch.loading) }) fetch.set(newFetch) From 2ef2d07cab6f3f563d0d3afd62e6e7e5bf201f42 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 12 Oct 2023 14:28:05 +0100 Subject: [PATCH 02/14] Add inline searching for formula and longform columns, and improve searching operators where possible --- .../components/grid/cells/HeaderCell.svelte | 12 ++++++++-- .../src/components/grid/stores/filter.js | 22 ++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 1abddfe1ff..314db21fc5 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -35,7 +35,7 @@ "boolean", "json", ] - const searchableTypes = ["string", "options", "number"] + const searchableTypes = ["string", "options", "number", "array", "longform"] let anchor let open = false @@ -54,10 +54,18 @@ $: descendingLabel = ["number", "bigint"].includes(column.schema?.type) ? "high-low" : "Z-A" - $: searchable = searchableTypes.includes(column.schema.type) + $: searchable = isColumnSearchable(column) $: searching = searchValue != null $: debouncedUpdateFilter(searchValue) + const isColumnSearchable = col => { + const type = col.schema.type + return ( + searchableTypes.includes(type) || + (type === "formula" && col.schema.formulaType === "static") + ) + } + const editColumn = async () => { editIsOpen = true await tick() diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.js index a2de0ca2d0..25b61161fa 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.js @@ -16,27 +16,39 @@ export const createActions = context => { const addInlineFilter = (column, value) => { const filterId = `inline-${column}` - - const inlineFilter = { + let inlineFilter = { field: column.name, id: filterId, operator: "equal", - type: "string", + type: column.schema.type, valueType: "value", value, } + // Add overrides specific so the certain column type + switch (column.schema.type) { + case "string": + case "formula": + case "longform": + inlineFilter.operator = "string" + break + case "number": + inlineFilter.value = parseFloat(value) + break + case "array": + inlineFilter.operator = "contains" + } + + // Add this filter filter.update($filter => { // Remove any existing inline filter if ($filter?.length) { $filter = $filter?.filter(x => x.id !== filterId) } - // Add new one if a value exists if (value) { $filter = [...($filter || []), inlineFilter] } - return $filter }) } From cfdaa3564c9b4e507a79fa75e85eb22778628626 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 12 Oct 2023 14:30:41 +0100 Subject: [PATCH 03/14] Improve options inline searching --- .../src/components/grid/stores/filter.js | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.js index 25b61161fa..7e8cb364a8 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.js @@ -16,27 +16,22 @@ export const createActions = context => { const addInlineFilter = (column, value) => { const filterId = `inline-${column}` + const type = column.schema.type let inlineFilter = { field: column.name, id: filterId, - operator: "equal", - type: column.schema.type, + operator: "string", valueType: "value", + type, value, } // Add overrides specific so the certain column type - switch (column.schema.type) { - case "string": - case "formula": - case "longform": - inlineFilter.operator = "string" - break - case "number": - inlineFilter.value = parseFloat(value) - break - case "array": - inlineFilter.operator = "contains" + if (type === "number") { + inlineFilter.value = parseFloat(value) + inlineFilter.operator = "equal" + } else if (type === "array") { + inlineFilter.operator = "contains" } // Add this filter From c906efb972b211ba02dbd2e1f64c6e9a8002ec61 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 12 Oct 2023 14:37:13 +0100 Subject: [PATCH 04/14] Fix text colour for inline searching in grid block --- .../frontend-core/src/components/grid/cells/HeaderCell.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 573030b7b4..cdd8afb57e 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -398,7 +398,7 @@ outline: none; border: 1px solid transparent; background: transparent; - color: var(--ink); + color: var(--spectrum-global-color-gray-800); position: absolute; top: 0; left: 0; From 6dfe2c22af340af73429121cf35c86874c60c3cc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 12 Oct 2023 15:46:18 +0100 Subject: [PATCH 05/14] Fix issue with multiple filters at the same time and remove unused variable --- .../src/components/grid/cells/HeaderCell.svelte | 7 ------- .../frontend-core/src/components/grid/stores/filter.js | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index cdd8afb57e..7d2b5d5941 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -29,13 +29,6 @@ filter, } = getContext("grid") - const bannedDisplayColumnTypes = [ - "link", - "array", - "attachment", - "boolean", - "json", - ] const searchableTypes = ["string", "options", "number", "array", "longform"] let anchor diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.js index 7e8cb364a8..984c2115ee 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.js @@ -15,7 +15,7 @@ export const createActions = context => { const { filter } = context const addInlineFilter = (column, value) => { - const filterId = `inline-${column}` + const filterId = `inline-${column.name}` const type = column.schema.type let inlineFilter = { field: column.name, From 804aab3e43a0f9a4bb154d54925bdfa29ea37d41 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 13 Oct 2023 09:36:50 +0100 Subject: [PATCH 06/14] Refactor to use types for fields and add support for searching bigint columns --- .../components/grid/cells/HeaderCell.svelte | 25 ++++++++++++------- .../src/components/grid/stores/filter.js | 7 ++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 7d2b5d5941..d4ed41efd3 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -5,6 +5,7 @@ import GridCell from "./GridCell.svelte" import { getColumnIcon } from "../lib/utils" import { debounce } from "../../../utils/utils" + import { FieldType, FormulaTypes } from "@budibase/types" export let column export let idx @@ -29,7 +30,14 @@ filter, } = getContext("grid") - const searchableTypes = ["string", "options", "number", "array", "longform"] + const searchableTypes = [ + FieldType.STRING, + FieldType.OPTIONS, + FieldType.NUMBER, + FieldType.BIGINT, + FieldType.ARRAY, + FieldType.LONGFORM, + ] let anchor let open = false @@ -42,21 +50,20 @@ $: sortedBy = column.name === $sort.column $: canMoveLeft = orderable && idx > 0 $: canMoveRight = orderable && idx < $renderedColumns.length - 1 - $: ascendingLabel = ["number", "bigint"].includes(column.schema?.type) - ? "low-high" - : "A-Z" - $: descendingLabel = ["number", "bigint"].includes(column.schema?.type) - ? "high-low" - : "Z-A" + $: numericType = [FieldType.NUMBER, FieldType.BIGINT].includes( + column.schema?.type + ) + $: ascendingLabel = numericType ? "low-high" : "A-Z" + $: descendingLabel = numericType ? "high-low" : "Z-A" $: searchable = isColumnSearchable(column) $: searching = searchValue != null $: debouncedUpdateFilter(searchValue) const isColumnSearchable = col => { - const type = col.schema.type + const { type, formulaType } = col.schema return ( searchableTypes.includes(type) || - (type === "formula" && col.schema.formulaType === "static") + (type === FieldType.FORMULA && formulaType === FormulaTypes.STATIC) ) } diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.js index 984c2115ee..76c8c5d3ec 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.js @@ -1,4 +1,5 @@ import { writable, get } from "svelte/store" +import { FieldType } from "@budibase/types" export const createStores = context => { const { props } = context @@ -27,10 +28,12 @@ export const createActions = context => { } // Add overrides specific so the certain column type - if (type === "number") { + if (type === FieldType.NUMBER) { inlineFilter.value = parseFloat(value) inlineFilter.operator = "equal" - } else if (type === "array") { + } else if (type === FieldType.BIGINT) { + inlineFilter.operator = "equal" + } else if (type === FieldType.ARRAY) { inlineFilter.operator = "contains" } From b337bd7435d9263fd7750009a22e653ab3513dac Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 13 Oct 2023 17:03:19 +0100 Subject: [PATCH 07/14] Fix bug in SortableFieldSelect which results in options being available for sort fields --- .../design/settings/controls/SortableFieldSelect.svelte | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/builder/src/components/design/settings/controls/SortableFieldSelect.svelte b/packages/builder/src/components/design/settings/controls/SortableFieldSelect.svelte index 74b044e75e..21ed68ce68 100644 --- a/packages/builder/src/components/design/settings/controls/SortableFieldSelect.svelte +++ b/packages/builder/src/components/design/settings/controls/SortableFieldSelect.svelte @@ -20,9 +20,7 @@ const getSortableFields = schema => { return Object.entries(schema || {}) - .filter( - entry => !UNSORTABLE_TYPES.includes(entry[1].type) && entry[1].sortable - ) + .filter(entry => !UNSORTABLE_TYPES.includes(entry[1].type)) .map(entry => entry[0]) } From e3d6a68ea14263bd1bcf4083798a0ea8e2f95da8 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 13 Oct 2023 19:06:53 +0100 Subject: [PATCH 08/14] Refactor inline searching to be a separate concept from normal filters, and optimise API usage across all datasources --- .../src/components/grid/stores/datasource.js | 5 +- .../grid/stores/datasources/nonPlus.js | 7 ++- .../grid/stores/datasources/table.js | 7 ++- .../grid/stores/datasources/viewV2.js | 52 +++++++++++-------- .../src/components/grid/stores/filter.js | 34 ++++++++---- .../src/components/grid/stores/rows.js | 6 +-- .../frontend-core/src/fetch/ViewV2Fetch.js | 29 +++++++++-- 7 files changed, 96 insertions(+), 44 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 0b62194f73..1be5ae7878 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -1,8 +1,9 @@ -import { derived, get, writable } from "svelte/store" +import { derived, get } from "svelte/store" import { getDatasourceDefinition } from "../../../fetch" +import { memo } from "../../../utils" export const createStores = () => { - const definition = writable(null) + const definition = memo(null) return { definition, diff --git a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js index a05e1f7d37..017c16a03c 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/nonPlus.js @@ -66,6 +66,8 @@ export const initialise = context => { datasource, sort, filter, + inlineFilters, + allFilters, nonPlus, initialFilter, initialSortColumn, @@ -87,6 +89,7 @@ export const initialise = context => { // Wipe state filter.set(get(initialFilter)) + inlineFilters.set([]) sort.set({ column: get(initialSortColumn), order: get(initialSortOrder) || "ascending", @@ -94,14 +97,14 @@ export const initialise = context => { // Update fetch when filter changes unsubscribers.push( - filter.subscribe($filter => { + allFilters.subscribe($allFilters => { // Ensure we're updating the correct fetch const $fetch = get(fetch) if (!isSameDatasource($fetch?.options?.datasource, $datasource)) { return } $fetch.update({ - filter: $filter, + filter: $allFilters, }) }) ) diff --git a/packages/frontend-core/src/components/grid/stores/datasources/table.js b/packages/frontend-core/src/components/grid/stores/datasources/table.js index 9ced1530ba..2f49ab1d38 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/table.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/table.js @@ -71,6 +71,8 @@ export const initialise = context => { datasource, fetch, filter, + inlineFilters, + allFilters, sort, table, initialFilter, @@ -93,6 +95,7 @@ export const initialise = context => { // Wipe state filter.set(get(initialFilter)) + inlineFilters.set([]) sort.set({ column: get(initialSortColumn), order: get(initialSortOrder) || "ascending", @@ -100,14 +103,14 @@ export const initialise = context => { // Update fetch when filter changes unsubscribers.push( - filter.subscribe($filter => { + allFilters.subscribe($allFilters => { // Ensure we're updating the correct fetch const $fetch = get(fetch) if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { return } $fetch.update({ - filter: $filter, + filter: $allFilters, }) }) ) diff --git a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js index f0572003c2..35f57a5fc4 100644 --- a/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js +++ b/packages/frontend-core/src/components/grid/stores/datasources/viewV2.js @@ -73,6 +73,8 @@ export const initialise = context => { sort, rows, filter, + inlineFilters, + allFilters, subscribe, viewV2, initialFilter, @@ -97,6 +99,7 @@ export const initialise = context => { // Reset state for new view filter.set(get(initialFilter)) + inlineFilters.set([]) sort.set({ column: get(initialSortColumn), order: get(initialSortOrder) || "ascending", @@ -143,21 +146,19 @@ export const initialise = context => { order: $sort.order || "ascending", }, }) - await rows.actions.refreshData() } } - // Otherwise just update the fetch - else { - // Ensure we're updating the correct fetch - const $fetch = get(fetch) - if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { - return - } - $fetch.update({ - sortOrder: $sort.order || "ascending", - sortColumn: $sort.column, - }) + + // Also update the fetch to ensure the new sort is respected. + // Ensure we're updating the correct fetch. + const $fetch = get(fetch) + if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { + return } + $fetch.update({ + sortOrder: $sort.order, + sortColumn: $sort.column, + }) }) ) @@ -176,20 +177,25 @@ export const initialise = context => { ...$view, query: $filter, }) - await rows.actions.refreshData() } } - // Otherwise just update the fetch - else { - // Ensure we're updating the correct fetch - const $fetch = get(fetch) - if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { - return - } - $fetch.update({ - filter: $filter, - }) + }) + ) + + // Keep fetch up to date with filters. + // If we're able to save filters against the view then we only need to apply + // inline filters to the fetch, as saved filters are applied server side. + // If we can't save filters, then all filters must be applied to the fetch. + unsubscribers.push( + allFilters.subscribe($allFilters => { + // Ensure we're updating the correct fetch + const $fetch = get(fetch) + if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) { + return } + $fetch.update({ + filter: $allFilters, + }) }) ) diff --git a/packages/frontend-core/src/components/grid/stores/filter.js b/packages/frontend-core/src/components/grid/stores/filter.js index 76c8c5d3ec..a16b101bbb 100644 --- a/packages/frontend-core/src/components/grid/stores/filter.js +++ b/packages/frontend-core/src/components/grid/stores/filter.js @@ -1,4 +1,4 @@ -import { writable, get } from "svelte/store" +import { writable, get, derived } from "svelte/store" import { FieldType } from "@budibase/types" export const createStores = context => { @@ -6,14 +6,31 @@ export const createStores = context => { // Initialise to default props const filter = writable(get(props).initialFilter) + const inlineFilters = writable([]) return { filter, + inlineFilters, + } +} + +export const deriveStores = context => { + const { filter, inlineFilters } = context + + const allFilters = derived( + [filter, inlineFilters], + ([$filter, $inlineFilters]) => { + return [...($filter || []), ...$inlineFilters] + } + ) + + return { + allFilters, } } export const createActions = context => { - const { filter } = context + const { filter, inlineFilters } = context const addInlineFilter = (column, value) => { const filterId = `inline-${column.name}` @@ -38,16 +55,15 @@ export const createActions = context => { } // Add this filter - filter.update($filter => { - // Remove any existing inline filter - if ($filter?.length) { - $filter = $filter?.filter(x => x.id !== filterId) - } + inlineFilters.update($inlineFilters => { + // Remove any existing inline filter for this column + $inlineFilters = $inlineFilters?.filter(x => x.id !== filterId) + // Add new one if a value exists if (value) { - $filter = [...($filter || []), inlineFilter] + $inlineFilters.push(inlineFilter) } - return $filter + return $inlineFilters }) } diff --git a/packages/frontend-core/src/components/grid/stores/rows.js b/packages/frontend-core/src/components/grid/stores/rows.js index 98e64d7acb..51c46f8263 100644 --- a/packages/frontend-core/src/components/grid/stores/rows.js +++ b/packages/frontend-core/src/components/grid/stores/rows.js @@ -68,7 +68,7 @@ export const createActions = context => { rows, rowLookupMap, definition, - filter, + allFilters, loading, sort, datasource, @@ -111,7 +111,7 @@ export const createActions = context => { // Tick to allow other reactive logic to update stores when datasource changes // before proceeding. This allows us to wipe filters etc if needed. await tick() - const $filter = get(filter) + const $allFilters = get(allFilters) const $sort = get(sort) // Determine how many rows to fetch per page @@ -123,7 +123,7 @@ export const createActions = context => { API, datasource: $datasource, options: { - filter: $filter, + filter: $allFilters, sortColumn: $sort.column, sortOrder: $sort.order, limit, diff --git a/packages/frontend-core/src/fetch/ViewV2Fetch.js b/packages/frontend-core/src/fetch/ViewV2Fetch.js index b9eaf4bdf7..464a85464e 100644 --- a/packages/frontend-core/src/fetch/ViewV2Fetch.js +++ b/packages/frontend-core/src/fetch/ViewV2Fetch.js @@ -35,9 +35,32 @@ export default class ViewV2Fetch extends DataFetch { } async getData() { - const { datasource, limit, sortColumn, sortOrder, sortType, paginate } = - this.options - const { cursor, query } = get(this.store) + const { + datasource, + limit, + sortColumn, + sortOrder, + sortType, + paginate, + filter, + } = this.options + const { cursor, query, definition } = get(this.store) + + // If sort params are not defined, update options to store the sorting + // params built in to this view. This ensures that we can accurately + // compare old and new sorting params and skip a redundant API call. + if (!sortColumn && definition.sort?.field) { + this.options.sortColumn = definition.sort.field + this.options.sortOrder = definition.sort.order + } + + // If sort params are not defined, update options to store the sorting + // params built in to this view. This ensures that we can accurately + // compare old and new sorting params and skip a redundant API call. + if (!filter?.length && definition.query?.length) { + this.options.filter = definition.query + } + try { const res = await this.API.viewV2.fetch({ viewId: datasource.id, From 27373a9648585baf8c5f812dfcd29f63c1c19aeb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 13 Oct 2023 19:17:49 +0100 Subject: [PATCH 09/14] Ensure header cells properly update when reordered while a search value is applied --- .../src/components/grid/cells/HeaderCell.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index d4ed41efd3..a053b2a6f0 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -28,6 +28,7 @@ schema, focusedCellId, filter, + inlineFilters, } = getContext("grid") const searchableTypes = [ @@ -56,9 +57,14 @@ $: ascendingLabel = numericType ? "low-high" : "A-Z" $: descendingLabel = numericType ? "high-low" : "Z-A" $: searchable = isColumnSearchable(column) + $: resetSearchValue(column.name) $: searching = searchValue != null $: debouncedUpdateFilter(searchValue) + const resetSearchValue = name => { + searchValue = $inlineFilters?.find(x => x.id === `inline-${name}`)?.value + } + const isColumnSearchable = col => { const { type, formulaType } = col.schema return ( From a857eb266ca1a22666d5f9ae0258d50988685923 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 16 Oct 2023 17:12:25 +0100 Subject: [PATCH 10/14] Ensure keyboard events while inline searching are not captured by the main grid keyboard manager --- .../frontend-core/src/components/grid/cells/HeaderCell.svelte | 1 + .../src/components/grid/overlays/KeyboardManager.svelte | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index a053b2a6f0..6648ba1a69 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -234,6 +234,7 @@ on:blur={onBlurInput} on:click={() => focusedCellId.set(null)} on:keydown={onInputKeyDown} + data-grid-ignore /> {/if} diff --git a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte index cd23f154b5..8b0a0f0942 100644 --- a/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte +++ b/packages/frontend-core/src/components/grid/overlays/KeyboardManager.svelte @@ -21,6 +21,7 @@ const ignoredOriginSelectors = [ ".spectrum-Modal", "#builder-side-panel-container", + "[data-grid-ignore]", ] // Global key listener which intercepts all key events From 74cab111917c91c64b39f8d913be228259d42982 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 16 Oct 2023 17:17:14 +0100 Subject: [PATCH 11/14] Improve grid sorting labels to account for date types and provide better labels --- .../components/grid/cells/HeaderCell.svelte | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte index 6648ba1a69..f367e3427f 100644 --- a/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte +++ b/packages/frontend-core/src/components/grid/cells/HeaderCell.svelte @@ -51,16 +51,33 @@ $: sortedBy = column.name === $sort.column $: canMoveLeft = orderable && idx > 0 $: canMoveRight = orderable && idx < $renderedColumns.length - 1 - $: numericType = [FieldType.NUMBER, FieldType.BIGINT].includes( - column.schema?.type - ) - $: ascendingLabel = numericType ? "low-high" : "A-Z" - $: descendingLabel = numericType ? "high-low" : "Z-A" + $: sortingLabels = getSortingLabels(column.schema?.type) $: searchable = isColumnSearchable(column) $: resetSearchValue(column.name) $: searching = searchValue != null $: debouncedUpdateFilter(searchValue) + const getSortingLabels = type => { + switch (type) { + case FieldType.NUMBER: + case FieldType.BIGINT: + return { + ascending: "low-high", + descending: "high-low", + } + case FieldType.DATETIME: + return { + ascending: "old-new", + descending: "new-old", + } + default: + return { + ascending: "A-Z", + descending: "Z-A", + } + } + } + const resetSearchValue = name => { searchValue = $inlineFilters?.find(x => x.id === `inline-${name}`)?.value } @@ -318,14 +335,14 @@ on:click={sortAscending} disabled={column.name === $sort.column && $sort.order === "ascending"} > - Sort {ascendingLabel} + Sort {sortingLabels.ascending} - Sort {descendingLabel} + Sort {sortingLabels.descending} Move left From 9a72c418c9c1b92c1496459d2127e8ca11ff62ee Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 20 Oct 2023 14:13:30 +0100 Subject: [PATCH 12/14] Remove duplicate comment --- packages/frontend-core/src/fetch/ViewV2Fetch.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/frontend-core/src/fetch/ViewV2Fetch.js b/packages/frontend-core/src/fetch/ViewV2Fetch.js index 464a85464e..9d2f8c103a 100644 --- a/packages/frontend-core/src/fetch/ViewV2Fetch.js +++ b/packages/frontend-core/src/fetch/ViewV2Fetch.js @@ -46,17 +46,13 @@ export default class ViewV2Fetch extends DataFetch { } = this.options const { cursor, query, definition } = get(this.store) - // If sort params are not defined, update options to store the sorting + // If sort/filter params are not defined, update options to store the // params built in to this view. This ensures that we can accurately - // compare old and new sorting params and skip a redundant API call. + // compare old and new params and skip a redundant API call. if (!sortColumn && definition.sort?.field) { this.options.sortColumn = definition.sort.field this.options.sortOrder = definition.sort.order } - - // If sort params are not defined, update options to store the sorting - // params built in to this view. This ensures that we can accurately - // compare old and new sorting params and skip a redundant API call. if (!filter?.length && definition.query?.length) { this.options.filter = definition.query } From f355933bf306ef427d42e4d73b31220ffaba44be Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 24 Oct 2023 08:26:57 +0100 Subject: [PATCH 13/14] Add grid support for old views with groupBy specified, and any other datasource that has a custom means of determining a datasource schema --- .../src/components/grid/stores/datasource.js | 10 +++++++--- packages/frontend-core/src/fetch/index.js | 20 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index 1be5ae7878..d49938ecc8 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -1,5 +1,5 @@ import { derived, get } from "svelte/store" -import { getDatasourceDefinition } from "../../../fetch" +import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch" import { memo } from "../../../utils" export const createStores = () => { @@ -11,10 +11,14 @@ export const createStores = () => { } export const deriveStores = context => { - const { definition, schemaOverrides, columnWhitelist, datasource } = context + const { API, definition, schemaOverrides, columnWhitelist, datasource } = context const schema = derived(definition, $definition => { - let schema = $definition?.schema + let schema = getDatasourceSchema({ + API, + datasource: get(datasource), + definition: $definition + }) if (!schema) { return null } diff --git a/packages/frontend-core/src/fetch/index.js b/packages/frontend-core/src/fetch/index.js index d133942bb7..a41a859351 100644 --- a/packages/frontend-core/src/fetch/index.js +++ b/packages/frontend-core/src/fetch/index.js @@ -32,12 +32,24 @@ export const fetchData = ({ API, datasource, options }) => { return new Fetch({ API, datasource, ...options }) } -// Fetches the definition of any type of datasource -export const getDatasourceDefinition = async ({ API, datasource }) => { +// Creates an empty fetch instance with no datasource configured, so no data +// will initially be loaded +const createEmptyFetchInstance = ({ API, datasource }) => { const handler = DataFetchMap[datasource?.type] if (!handler) { return null } - const instance = new handler({ API }) - return await instance.getDefinition(datasource) + return new handler({ API }) +} + +// Fetches the definition of any type of datasource +export const getDatasourceDefinition = async ({ API, datasource }) => { + const instance = createEmptyFetchInstance({ API, datasource }) + return await instance?.getDefinition(datasource) +} + +// Fetches the schema of any type of datasource +export const getDatasourceSchema = ({ API, datasource, definition }) => { + const instance = createEmptyFetchInstance({ API, datasource }) + return instance?.getSchema(datasource, definition) } From c5537dd58c9df1e5998e62ecf71439e9e5df5413 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 24 Oct 2023 08:46:55 +0100 Subject: [PATCH 14/14] Prettier --- .../frontend-core/src/components/grid/stores/datasource.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/frontend-core/src/components/grid/stores/datasource.js b/packages/frontend-core/src/components/grid/stores/datasource.js index d49938ecc8..958f4541bd 100644 --- a/packages/frontend-core/src/components/grid/stores/datasource.js +++ b/packages/frontend-core/src/components/grid/stores/datasource.js @@ -11,13 +11,14 @@ export const createStores = () => { } export const deriveStores = context => { - const { API, definition, schemaOverrides, columnWhitelist, datasource } = context + const { API, definition, schemaOverrides, columnWhitelist, datasource } = + context const schema = derived(definition, $definition => { let schema = getDatasourceSchema({ API, datasource: get(datasource), - definition: $definition + definition: $definition, }) if (!schema) { return null