Merge branch 'master' into feature/change-global-admin-to-creator
This commit is contained in:
commit
3473aa61ff
|
@ -51,7 +51,7 @@ http {
|
|||
proxy_buffering off;
|
||||
|
||||
set $csp_default "default-src 'self'";
|
||||
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io";
|
||||
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net";
|
||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||
set $csp_object "object-src 'none'";
|
||||
set $csp_base_uri "base-uri 'self'";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.11.44",
|
||||
"version": "2.11.45",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -777,7 +777,8 @@
|
|||
disabled={deleteColName !== originalName}
|
||||
>
|
||||
<p>
|
||||
Are you sure you wish to delete the column <b>{originalName}?</b>
|
||||
Are you sure you wish to delete the column
|
||||
<b on:click={() => (deleteColName = originalName)}>{originalName}?</b>
|
||||
Your data will be deleted and this action cannot be undone - enter the column
|
||||
name to confirm.
|
||||
</p>
|
||||
|
@ -810,4 +811,11 @@
|
|||
gap: 8px;
|
||||
display: flex;
|
||||
}
|
||||
b {
|
||||
transition: color 130ms ease-out;
|
||||
}
|
||||
b:hover {
|
||||
cursor: pointer;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
}
|
||||
.alert-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
margin: -28px -40px 14px -40px;
|
||||
}
|
||||
.alert-wrap :global(> *) {
|
||||
flex: 1;
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
{#if section.visible}
|
||||
<DetailSummary
|
||||
name={showSectionTitle ? section.name : ""}
|
||||
collapsible={false}
|
||||
show={section.collapsed !== true}
|
||||
>
|
||||
{#if section.info}
|
||||
<div class="section-info">
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let invertX = false
|
||||
export let invertY = false
|
||||
export let contentLines = 1
|
||||
export let hidden = false
|
||||
|
||||
const emptyError = writable(null)
|
||||
|
||||
|
@ -78,6 +79,7 @@
|
|||
{focused}
|
||||
{selectedUser}
|
||||
{readonly}
|
||||
{hidden}
|
||||
error={$error}
|
||||
on:click={() => focusedCellId.set(cellId)}
|
||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
export let defaultHeight = false
|
||||
export let center = false
|
||||
export let readonly = false
|
||||
export let hidden = false
|
||||
|
||||
$: style = getStyle(width, selectedUser)
|
||||
|
||||
|
@ -30,6 +31,7 @@
|
|||
class:error
|
||||
class:center
|
||||
class:readonly
|
||||
class:hidden
|
||||
class:default-height={defaultHeight}
|
||||
class:selected-other={selectedUser != null}
|
||||
class:alt={rowIdx % 2 === 1}
|
||||
|
@ -81,6 +83,9 @@
|
|||
.cell.center {
|
||||
align-items: center;
|
||||
}
|
||||
.cell.hidden {
|
||||
content-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Cell border */
|
||||
.cell.focused:after,
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui"
|
||||
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
|
||||
|
@ -15,7 +17,7 @@
|
|||
isResizing,
|
||||
rand,
|
||||
sort,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
dispatch,
|
||||
subscribe,
|
||||
config,
|
||||
|
@ -24,23 +26,69 @@
|
|||
definition,
|
||||
datasource,
|
||||
schema,
|
||||
focusedCellId,
|
||||
filter,
|
||||
inlineFilters,
|
||||
} = getContext("grid")
|
||||
|
||||
const searchableTypes = [
|
||||
FieldType.STRING,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.NUMBER,
|
||||
FieldType.BIGINT,
|
||||
FieldType.ARRAY,
|
||||
FieldType.LONGFORM,
|
||||
]
|
||||
|
||||
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
|
||||
$: 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"
|
||||
$: canMoveRight = orderable && idx < $visibleColumns.length - 1
|
||||
$: 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
|
||||
}
|
||||
|
||||
const isColumnSearchable = col => {
|
||||
const { type, formulaType } = col.schema
|
||||
return (
|
||||
searchableTypes.includes(type) ||
|
||||
(type === FieldType.FORMULA && formulaType === FormulaTypes.STATIC)
|
||||
)
|
||||
}
|
||||
|
||||
const editColumn = async () => {
|
||||
editIsOpen = true
|
||||
|
@ -141,12 +189,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))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="header-cell"
|
||||
class:open
|
||||
class:searchable
|
||||
class:searching
|
||||
style="flex: 0 0 {column.width}px;"
|
||||
bind:this={anchor}
|
||||
class:disabled={$isReordering || $isResizing}
|
||||
|
@ -161,30 +243,49 @@
|
|||
defaultHeight
|
||||
center
|
||||
>
|
||||
<Icon
|
||||
size="S"
|
||||
name={getColumnIcon(column)}
|
||||
color={`var(--spectrum-global-color-gray-600)`}
|
||||
/>
|
||||
{#if searching}
|
||||
<input
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value={searchValue}
|
||||
on:blur={onBlurInput}
|
||||
on:click={() => focusedCellId.set(null)}
|
||||
on:keydown={onInputKeyDown}
|
||||
data-grid-ignore
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="column-icon">
|
||||
<Icon size="S" name={getColumnIcon(column)} />
|
||||
</div>
|
||||
<div class="search-icon" on:click={startSearching}>
|
||||
<Icon hoverable size="S" name="Search" />
|
||||
</div>
|
||||
|
||||
<div class="name">
|
||||
{column.label}
|
||||
</div>
|
||||
{#if sortedBy}
|
||||
<div class="sort-indicator">
|
||||
<Icon
|
||||
size="S"
|
||||
name={$sort.order === "descending" ? "SortOrderDown" : "SortOrderUp"}
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
|
||||
{#if searching}
|
||||
<div class="clear-icon" on:click={stopSearching}>
|
||||
<Icon hoverable size="S" name="Close" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if sortedBy}
|
||||
<div class="sort-indicator">
|
||||
<Icon
|
||||
hoverable
|
||||
size="S"
|
||||
name={$sort.order === "descending"
|
||||
? "SortOrderDown"
|
||||
: "SortOrderUp"}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="more-icon" on:click={() => (open = true)}>
|
||||
<Icon hoverable size="S" name="MoreVertical" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="more" on:click={() => (open = true)}>
|
||||
<Icon
|
||||
size="S"
|
||||
name="MoreVertical"
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
</div>
|
||||
</GridCell>
|
||||
</div>
|
||||
|
||||
|
@ -235,7 +336,7 @@
|
|||
disabled={!canBeSortColumn(column.schema.type) ||
|
||||
(column.name === $sort.column && $sort.order === "ascending")}
|
||||
>
|
||||
Sort {ascendingLabel}
|
||||
Sort {sortingLabels.ascending}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="SortOrderDown"
|
||||
|
@ -243,7 +344,7 @@
|
|||
disabled={!canBeSortColumn(column.schema.type) ||
|
||||
(column.name === $sort.column && $sort.order === "descending")}
|
||||
>
|
||||
Sort {descendingLabel}
|
||||
Sort {sortingLabels.descending}
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
|
||||
Move left
|
||||
|
@ -283,6 +384,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;
|
||||
|
@ -290,23 +414,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(--spectrum-global-color-gray-800);
|
||||
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;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
const {
|
||||
bounds,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
rowVerticalInversionIndex,
|
||||
hoveredRowId,
|
||||
dispatch,
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
let body
|
||||
|
||||
$: renderColumnsWidth = $renderedColumns.reduce(
|
||||
$: columnsWidth = $visibleColumns.reduce(
|
||||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
|
@ -47,7 +47,7 @@
|
|||
<div
|
||||
class="blank"
|
||||
class:highlighted={$hoveredRowId === BlankRowID}
|
||||
style="width:{renderColumnsWidth}px"
|
||||
style="width:{columnsWidth}px"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
focusedCellId,
|
||||
reorder,
|
||||
selectedRows,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
hoveredRowId,
|
||||
selectedCellMap,
|
||||
focusedRow,
|
||||
|
@ -19,6 +19,7 @@
|
|||
isDragging,
|
||||
dispatch,
|
||||
rows,
|
||||
columnRenderMap,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
|
@ -34,7 +35,7 @@
|
|||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||
>
|
||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
||||
{#each $visibleColumns as column, columnIdx}
|
||||
{@const cellId = `${row._id}-${column.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
|
@ -51,6 +52,7 @@
|
|||
selectedUser={$selectedCellMap[cellId]}
|
||||
width={column.width}
|
||||
contentLines={$contentLines}
|
||||
hidden={!$columnRenderMap[column.name]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
maxScrollLeft,
|
||||
bounds,
|
||||
hoveredRowId,
|
||||
hiddenColumnsWidth,
|
||||
menu,
|
||||
} = getContext("grid")
|
||||
|
||||
|
@ -23,10 +22,10 @@
|
|||
let initialTouchX
|
||||
let initialTouchY
|
||||
|
||||
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
||||
$: style = generateStyle($scroll, $rowHeight)
|
||||
|
||||
const generateStyle = (scroll, rowHeight, hiddenWidths) => {
|
||||
const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0
|
||||
const generateStyle = (scroll, rowHeight) => {
|
||||
const offsetX = scrollHorizontally ? -1 * scroll.left : 0
|
||||
const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
|
||||
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
||||
}
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||
import { TempTooltip, TooltipType } from "@budibase/bbui"
|
||||
|
||||
const { renderedColumns, config, hasNonAutoColumn, datasource, loading } =
|
||||
const { visibleColumns, config, hasNonAutoColumn, datasource, loading } =
|
||||
getContext("grid")
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<GridScrollWrapper scrollHorizontally>
|
||||
<div class="row">
|
||||
{#each $renderedColumns as column, idx}
|
||||
{#each $visibleColumns as column, idx}
|
||||
<HeaderCell {column} {idx}>
|
||||
<slot name="edit-column" />
|
||||
</HeaderCell>
|
||||
|
|
|
@ -2,17 +2,16 @@
|
|||
import { getContext, onMount } from "svelte"
|
||||
import { Icon, Popover, clickOutside } from "@budibase/bbui"
|
||||
|
||||
const { renderedColumns, scroll, hiddenColumnsWidth, width, subscribe } =
|
||||
getContext("grid")
|
||||
const { visibleColumns, scroll, width, subscribe } = getContext("grid")
|
||||
|
||||
let anchor
|
||||
let open = false
|
||||
|
||||
$: columnsWidth = $renderedColumns.reduce(
|
||||
$: columnsWidth = $visibleColumns.reduce(
|
||||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
|
||||
$: end = columnsWidth - 1 - $scroll.left
|
||||
$: left = Math.min($width - 40, end)
|
||||
|
||||
const close = () => {
|
||||
|
@ -34,7 +33,7 @@
|
|||
<Popover
|
||||
bind:open
|
||||
{anchor}
|
||||
align={$renderedColumns.length ? "right" : "left"}
|
||||
align={$visibleColumns.length ? "right" : "left"}
|
||||
offset={0}
|
||||
popoverTarget={document.getElementById(`add-column-button`)}
|
||||
customZindex={100}
|
||||
|
|
|
@ -20,15 +20,18 @@
|
|||
datasource,
|
||||
subscribe,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
rowHeight,
|
||||
hasNextPage,
|
||||
maxScrollTop,
|
||||
rowVerticalInversionIndex,
|
||||
columnHorizontalInversionIndex,
|
||||
selectedRows,
|
||||
loading,
|
||||
loaded,
|
||||
refreshing,
|
||||
config,
|
||||
filter,
|
||||
columnRenderMap,
|
||||
} = getContext("grid")
|
||||
|
||||
let visible = false
|
||||
|
@ -36,7 +39,7 @@
|
|||
let newRow
|
||||
let offset = 0
|
||||
|
||||
$: firstColumn = $stickyColumn || $renderedColumns[0]
|
||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
$: $datasource, (visible = false)
|
||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
||||
|
@ -153,7 +156,7 @@
|
|||
<!-- New row FAB -->
|
||||
<TempTooltip
|
||||
text="Click here to create your first row"
|
||||
condition={hasNoRows && !$loading}
|
||||
condition={hasNoRows && $loaded && !$filter?.length && !$refreshing}
|
||||
type={TooltipType.Info}
|
||||
>
|
||||
{#if !visible && !selectedRowCount && $config.canAddRows}
|
||||
|
@ -209,29 +212,28 @@
|
|||
<div class="normal-columns" transition:fade|local={{ duration: 130 }}>
|
||||
<GridScrollWrapper scrollHorizontally attachHandlers>
|
||||
<div class="row">
|
||||
{#each $renderedColumns as column, columnIdx}
|
||||
{#each $visibleColumns as column, columnIdx}
|
||||
{@const cellId = `new-${column.name}`}
|
||||
{#key cellId}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
{updateValue}
|
||||
rowFocused
|
||||
row={newRow}
|
||||
focused={$focusedCellId === cellId}
|
||||
width={column.width}
|
||||
topRow={offset === 0}
|
||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
||||
{invertY}
|
||||
>
|
||||
{#if column?.schema?.autocolumn}
|
||||
<div class="readonly-overlay">Can't edit auto column</div>
|
||||
{/if}
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</DataCell>
|
||||
{/key}
|
||||
<DataCell
|
||||
{cellId}
|
||||
{column}
|
||||
{updateValue}
|
||||
rowFocused
|
||||
row={newRow}
|
||||
focused={$focusedCellId === cellId}
|
||||
width={column.width}
|
||||
topRow={offset === 0}
|
||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
||||
{invertY}
|
||||
hidden={!$columnRenderMap[column.name]}
|
||||
>
|
||||
{#if column?.schema?.autocolumn}
|
||||
<div class="readonly-overlay">Can't edit auto column</div>
|
||||
{/if}
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
{/if}
|
||||
</DataCell>
|
||||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
const ignoredOriginSelectors = [
|
||||
".spectrum-Modal",
|
||||
"#builder-side-panel-container",
|
||||
"[data-grid-ignore]",
|
||||
]
|
||||
|
||||
// Global key listener which intercepts all key events
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { GutterWidth } from "../lib/constants"
|
||||
|
||||
const { resize, renderedColumns, stickyColumn, isReordering, scrollLeft } =
|
||||
const { resize, visibleColumns, stickyColumn, isReordering, scrollLeft } =
|
||||
getContext("grid")
|
||||
|
||||
$: offset = GutterWidth + ($stickyColumn?.width || 0)
|
||||
|
@ -26,7 +26,7 @@
|
|||
<div class="resize-indicator" />
|
||||
</div>
|
||||
{/if}
|
||||
{#each $renderedColumns as column}
|
||||
{#each $visibleColumns as column}
|
||||
<div
|
||||
class="resize-slider"
|
||||
class:visible={activeColumn === column.name}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
import { getDatasourceDefinition } from "../../../fetch"
|
||||
import { derived, get } from "svelte/store"
|
||||
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
|
||||
import { memo } from "../../../utils"
|
||||
|
||||
export const createStores = () => {
|
||||
const definition = writable(null)
|
||||
const definition = memo(null)
|
||||
|
||||
return {
|
||||
definition,
|
||||
|
@ -10,10 +11,15 @@ 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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -1,13 +1,79 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { writable, get, derived } from "svelte/store"
|
||||
import { FieldType } from "@budibase/types"
|
||||
|
||||
export const createStores = context => {
|
||||
const { props } = 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, inlineFilters } = context
|
||||
|
||||
const addInlineFilter = (column, value) => {
|
||||
const filterId = `inline-${column.name}`
|
||||
const type = column.schema.type
|
||||
let inlineFilter = {
|
||||
field: column.name,
|
||||
id: filterId,
|
||||
operator: "string",
|
||||
valueType: "value",
|
||||
type,
|
||||
value,
|
||||
}
|
||||
|
||||
// Add overrides specific so the certain column type
|
||||
if (type === FieldType.NUMBER) {
|
||||
inlineFilter.value = parseFloat(value)
|
||||
inlineFilter.operator = "equal"
|
||||
} else if (type === FieldType.BIGINT) {
|
||||
inlineFilter.operator = "equal"
|
||||
} else if (type === FieldType.ARRAY) {
|
||||
inlineFilter.operator = "contains"
|
||||
}
|
||||
|
||||
// Add this filter
|
||||
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) {
|
||||
$inlineFilters.push(inlineFilter)
|
||||
}
|
||||
return $inlineFilters
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
filter: {
|
||||
...filter,
|
||||
actions: {
|
||||
addInlineFilter,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -66,7 +68,7 @@ export const createActions = context => {
|
|||
rows,
|
||||
rowLookupMap,
|
||||
definition,
|
||||
filter,
|
||||
allFilters,
|
||||
loading,
|
||||
sort,
|
||||
datasource,
|
||||
|
@ -82,6 +84,7 @@ export const createActions = context => {
|
|||
notifications,
|
||||
fetch,
|
||||
isDatasourcePlus,
|
||||
refreshing,
|
||||
} = context
|
||||
const instanceLoaded = writable(false)
|
||||
|
||||
|
@ -108,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
|
||||
|
@ -120,7 +123,7 @@ export const createActions = context => {
|
|||
API,
|
||||
datasource: $datasource,
|
||||
options: {
|
||||
filter: $filter,
|
||||
filter: $allFilters,
|
||||
sortColumn: $sort.column,
|
||||
sortOrder: $sort.order,
|
||||
limit,
|
||||
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { derived, get } from "svelte/store"
|
||||
import { derived } from "svelte/store"
|
||||
import {
|
||||
MaxCellRenderHeight,
|
||||
MaxCellRenderWidthOverflow,
|
||||
|
@ -50,12 +50,11 @@ export const deriveStores = context => {
|
|||
const interval = MinColumnWidth
|
||||
return Math.round($scrollLeft / interval) * interval
|
||||
})
|
||||
const renderedColumns = derived(
|
||||
const columnRenderMap = derived(
|
||||
[visibleColumns, scrollLeftRounded, width],
|
||||
([$visibleColumns, $scrollLeft, $width], set) => {
|
||||
([$visibleColumns, $scrollLeft, $width]) => {
|
||||
if (!$visibleColumns.length) {
|
||||
set([])
|
||||
return
|
||||
return {}
|
||||
}
|
||||
let startColIdx = 0
|
||||
let rightEdge = $visibleColumns[0].width
|
||||
|
@ -75,34 +74,16 @@ export const deriveStores = context => {
|
|||
leftEdge += $visibleColumns[endColIdx].width
|
||||
endColIdx++
|
||||
}
|
||||
// Render an additional column on either side to account for
|
||||
// debounce column updates based on scroll position
|
||||
const next = $visibleColumns.slice(
|
||||
Math.max(0, startColIdx - 1),
|
||||
endColIdx + 1
|
||||
)
|
||||
const current = get(renderedColumns)
|
||||
if (JSON.stringify(next) !== JSON.stringify(current)) {
|
||||
set(next)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const hiddenColumnsWidth = derived(
|
||||
[renderedColumns, visibleColumns],
|
||||
([$renderedColumns, $visibleColumns]) => {
|
||||
const idx = $visibleColumns.findIndex(
|
||||
col => col.name === $renderedColumns[0]?.name
|
||||
)
|
||||
let width = 0
|
||||
if (idx > 0) {
|
||||
for (let i = 0; i < idx; i++) {
|
||||
width += $visibleColumns[i].width
|
||||
}
|
||||
}
|
||||
return width
|
||||
},
|
||||
0
|
||||
// Only update the store if different
|
||||
let next = {}
|
||||
$visibleColumns
|
||||
.slice(Math.max(0, startColIdx), endColIdx)
|
||||
.forEach(col => {
|
||||
next[col.name] = true
|
||||
})
|
||||
return next
|
||||
}
|
||||
)
|
||||
|
||||
// Determine the row index at which we should start vertically inverting cell
|
||||
|
@ -130,12 +111,12 @@ export const deriveStores = context => {
|
|||
// Determine the column index at which we should start horizontally inverting
|
||||
// cell dropdowns
|
||||
const columnHorizontalInversionIndex = derived(
|
||||
[renderedColumns, scrollLeft, width],
|
||||
([$renderedColumns, $scrollLeft, $width]) => {
|
||||
[visibleColumns, scrollLeft, width],
|
||||
([$visibleColumns, $scrollLeft, $width]) => {
|
||||
const cutoff = $width + $scrollLeft - ScrollBarSize * 3
|
||||
let inversionIdx = $renderedColumns.length
|
||||
for (let i = $renderedColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
||||
const rightEdge = $renderedColumns[i].left + $renderedColumns[i].width
|
||||
let inversionIdx = $visibleColumns.length
|
||||
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
|
||||
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
|
||||
if (rightEdge + MaxCellRenderWidthOverflow <= cutoff) {
|
||||
break
|
||||
}
|
||||
|
@ -148,8 +129,7 @@ export const deriveStores = context => {
|
|||
scrolledRowCount,
|
||||
visualRowCapacity,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
hiddenColumnsWidth,
|
||||
columnRenderMap,
|
||||
rowVerticalInversionIndex,
|
||||
columnHorizontalInversionIndex,
|
||||
}
|
||||
|
|
|
@ -35,9 +35,28 @@ 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/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 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 (!filter?.length && definition.query?.length) {
|
||||
this.options.filter = definition.query
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.API.viewV2.fetch({
|
||||
viewId: datasource.id,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue