1
0
Fork 0
mirror of synced 2024-07-04 22:11:23 +12:00

Reworked Column Configuration in the data section (#11379)

* base work for using popover to create and edit columns

* more work to enable editing column in popover

* update styling of column type configs

* add new option picker component

* some updates to how the popover is opened and the new picker

* more updates to support the popover handling correctly

* update the popover to support a custom z index

* some styling around the colour picker

* update naming

* fix lint errors

* fix lint

* update filename

* incremental column numbers based on existing schema

* move func declaration

* add option color object to schema not constraints

* ux / pr comment updates

* undefined var

* fix issue with deleting option

* change background color

* update popove z-index
This commit is contained in:
Peter Clement 2023-07-31 15:28:11 +01:00 committed by GitHub
parent 9314a85220
commit 2ee7cb008b
16 changed files with 643 additions and 226 deletions

View file

@ -85,7 +85,8 @@
"dayjs": "^1.10.4",
"easymde": "^2.16.1",
"svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0"
"svelte-portal": "^1.0.0",
"svelte-dnd-action": "^0.9.8"
},
"resolutions": {
"loader-utils": "1.4.1"

View file

@ -1,5 +1,4 @@
<script>
//import { createEventDispatcher } from "svelte"
import "@spectrum-css/popover/dist/index-vars.css"
import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"

View file

@ -0,0 +1,252 @@
<script>
import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action"
import Icon from "../Icon/Icon.svelte"
import Popover from "../Popover/Popover.svelte"
import { onMount } from "svelte"
const flipDurationMs = 150
export let constraints
export let optionColors = {}
let options = []
let colorPopovers = []
let anchors = []
let colorsArray = [
"hsla(0, 90%, 75%, 0.3)",
"hsla(50, 80%, 75%, 0.3)",
"hsla(120, 90%, 75%, 0.3)",
"hsla(200, 90%, 75%, 0.3)",
"hsla(240, 90%, 75%, 0.3)",
"hsla(320, 90%, 75%, 0.3)",
]
$: {
if (constraints.inclusion.length) {
options = constraints.inclusion.map(value => ({
name: value,
id: Math.random(),
}))
}
}
const removeInput = idx => {
delete optionColors[options[idx].name]
constraints.inclusion = constraints.inclusion.filter((e, i) => i !== idx)
options = options.filter((e, i) => i !== idx)
colorPopovers.pop(undefined)
anchors.pop(undefined)
}
const addNewInput = () => {
options = [
...options,
{ name: `Option ${constraints.inclusion.length + 1}`, id: Math.random() },
]
constraints.inclusion = [
...constraints.inclusion,
`Option ${constraints.inclusion.length + 1}`,
]
colorPopovers.push(undefined)
anchors.push(undefined)
}
const handleDndConsider = e => {
options = e.detail.items
}
const handleDndFinalize = e => {
options = e.detail.items
constraints.inclusion = options.map(option => option.name)
}
const handleColorChange = (optionName, color, idx) => {
optionColors[optionName] = color
colorPopovers[idx].hide()
}
const handleNameChange = (optionName, idx, value) => {
constraints.inclusion[idx] = value
options[idx].name = value
optionColors[value] = optionColors[optionName]
delete optionColors[optionName]
}
const openColorPickerPopover = (optionIdx, target) => {
colorPopovers[optionIdx].show()
anchors[optionIdx] = target
}
onMount(() => {
// Initialize anchor arrays on mount, assuming 'options' is already populated
colorPopovers = constraints.inclusion.map(() => undefined)
anchors = constraints.inclusion.map(() => undefined)
})
</script>
<div>
<div
class="actions"
use:dndzone={{
items: options,
flipDurationMs,
dropTargetStyle: { outline: "none" },
}}
on:consider={handleDndConsider}
on:finalize={handleDndFinalize}
>
{#each options as option, idx (option.id)}
<div
class="no-border action-container"
animate:flip={{ duration: flipDurationMs }}
>
<div class="child drag-handle-spacing">
<Icon name="DragHandle" size="L" />
</div>
<div class="child color-picker">
<div
id="color-picker"
bind:this={anchors[idx]}
style="--color:{optionColors?.[option.name] ||
'hsla(0, 1%, 50%, 0.3)'}"
class="circle"
on:click={e => openColorPickerPopover(idx, e.target)}
>
<Popover
bind:this={colorPopovers[idx]}
anchor={anchors[idx]}
align="left"
offset={0}
style=""
popoverTarget={document.getElementById(`color-picker`)}
animate={false}
>
<div class="colors">
{#each colorsArray as color}
<div
on:click={() => handleColorChange(option.name, color, idx)}
style="--color:{color};"
class="circle circle-hover"
/>
{/each}
</div>
</Popover>
</div>
</div>
<div class="child">
<input
class="input-field"
type="text"
on:change={e => handleNameChange(option.name, idx, e.target.value)}
value={option.name}
placeholder="Option name"
/>
</div>
<div class="child">
<Icon name="Close" hoverable size="S" on:click={removeInput(idx)} />
</div>
</div>
{/each}
</div>
<div on:click={addNewInput} class="add-option">
<Icon hoverable name="Add" />
<div>Add option</div>
</div>
</div>
<style>
.action-container {
background-color: var(--spectrum-alias-background-color-primary);
border-radius: 0px;
border: 1px solid var(--spectrum-global-color-gray-300);
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
border-color 130ms ease-in-out;
display: flex;
flex-direction: row;
align-items: center;
}
.no-border {
border-bottom: none;
}
.action-container:last-child {
border-bottom: 1px solid var(--spectrum-global-color-gray-300) !important;
}
.child {
height: 30px;
}
.child:hover,
.child:focus {
background: var(--spectrum-global-color-gray-200);
}
.add-option {
display: flex;
flex-direction: row;
align-items: center;
padding: var(--spacing-m);
gap: var(--spacing-m);
cursor: pointer;
}
.input-field {
border: none;
outline: none;
background-color: transparent;
width: 100%;
color: var(--text);
}
.child input[type="text"] {
padding-left: 10px;
}
.input-field:hover,
.input-field:focus {
background: var(--spectrum-global-color-gray-200);
}
.action-container > :nth-child(1) {
flex-grow: 1;
justify-content: center;
display: flex;
}
.action-container > :nth-child(2) {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
}
.action-container > :nth-child(3) {
flex-grow: 4;
display: flex;
}
.action-container > :nth-child(4) {
flex-grow: 1;
justify-content: center;
display: flex;
}
.circle {
height: 20px;
width: 20px;
background-color: var(--color);
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
}
.circle-hover:hover {
border: 1px solid var(--spectrum-global-color-blue-400);
cursor: pointer;
}
.colors {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: var(--spacing-xl);
justify-items: center;
margin: var(--spacing-m);
}
</style>

View file

@ -21,6 +21,7 @@
export let offset = 5
export let customHeight
export let animate = true
export let customZindex
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
@ -77,8 +78,9 @@
}}
on:keydown={handleEscape}
class="spectrum-Popover is-open"
class:customZindex
role="presentation"
style="height: {customHeight}"
style="height: {customHeight}; --customZindex: {customZindex};"
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
>
<slot />
@ -92,4 +94,8 @@
border-color: var(--spectrum-global-color-gray-300);
overflow: auto;
}
.customZindex {
z-index: var(--customZindex) !important;
}
</style>

View file

@ -84,7 +84,7 @@ export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte
export { default as Slider } from "./Form/Slider.svelte"
export { default as Accordion } from "./Accordion/Accordion.svelte"
export { default as File } from "./Form/File.svelte"
export { default as OptionSelectDnD } from "./OptionSelectDnD/OptionSelectDnD.svelte"
// Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"

View file

@ -64,6 +64,13 @@
<svelte:fragment slot="filter">
<GridFilterButton />
</svelte:fragment>
<svelte:fragment slot="edit-column">
<GridEditColumnModal />
</svelte:fragment>
<svelte:fragment slot="add-column">
<GridAddColumnModal />
</svelte:fragment>
<svelte:fragment slot="controls">
{#if isInternal}
<GridCreateViewButton />
@ -77,9 +84,8 @@
{:else}
<GridImportButton />
{/if}
<GridExportButton />
<GridAddColumnModal />
<GridEditColumnModal />
{#if isUsersTable}
<GridEditUserModal />
{:else}

View file

@ -7,12 +7,12 @@
Toggle,
RadioGroup,
DatePicker,
ModalContent,
Context,
Modal,
notifications,
OptionSelectDnD,
Layout,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { createEventDispatcher, getContext } from "svelte"
import { cloneDeep } from "lodash/fp"
import { tables, datasources } from "stores/backend"
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
@ -26,12 +26,10 @@
SWITCHABLE_TYPES,
} from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import ValuesList from "components/common/ValuesList.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { truncate } from "lodash"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
import { getBindings } from "components/backend/DataTable/formula"
import { getContext } from "svelte"
import JSONSchemaModal from "./JSONSchemaModal.svelte"
import { ValidColumnNameRegex } from "@budibase/shared-core"
@ -45,11 +43,11 @@
const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
const { hide } = getContext(Context.Modal)
let fieldDefinitions = cloneDeep(FIELDS)
const { dispatch: gridDispatch } = getContext("grid")
export let field
let fieldDefinitions = cloneDeep(FIELDS)
let originalName
let linkEditDisabled
let primaryDisplay
@ -61,11 +59,10 @@
let savingColumn
let deleteColName
let jsonSchemaModal
let allowedTypes = []
let editableColumn = {
type: "string",
constraints: fieldDefinitions.STRING.constraints,
// Initial value for column name in other table for linked records
fieldName: $tables.selected.name,
}
@ -83,7 +80,23 @@
primaryDisplay =
$tables.selected.primaryDisplay == null ||
$tables.selected.primaryDisplay === editableColumn.name
} else if (!savingColumn) {
let highestNumber = 0
Object.keys(table.schema).forEach(columnName => {
const columnNumber = extractColumnNumber(columnName)
if (columnNumber > highestNumber) {
highestNumber = columnNumber
}
return highestNumber
})
if (highestNumber >= 1) {
editableColumn.name = `Column 0${highestNumber + 1}`
} else {
editableColumn.name = "Column 01"
}
}
allowedTypes = getAllowedTypes()
}
$: initialiseField(field, savingColumn)
@ -182,6 +195,8 @@
indexes,
})
dispatch("updatecolumns")
gridDispatch("close-edit-column")
if (
saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
@ -203,6 +218,7 @@
function cancelEdit() {
editableColumn.name = originalName
gridDispatch("close-edit-column")
}
async function deleteColumn() {
@ -214,8 +230,8 @@
await tables.deleteField(editableColumn)
notifications.success(`Column ${editableColumn.name} deleted`)
confirmDeleteDialog.hide()
hide()
dispatch("updatecolumns")
gridDispatch("close-edit-column")
}
} catch (error) {
notifications.error(`Error deleting column: ${error.message}`)
@ -251,14 +267,6 @@
required = req
}
function onChangePrimaryDisplay(e) {
const isPrimary = e.detail
// primary display is always required
if (isPrimary) {
editableColumn.constraints.presence = { allowEmpty: false }
}
}
function openJsonSchemaEditor() {
jsonSchemaModal.show()
}
@ -272,6 +280,11 @@
deleteColName = ""
}
function extractColumnNumber(columnName) {
const match = columnName.match(/Column (\d+)/)
return match ? parseInt(match[1]) : 0
}
function getRelationshipOptions(field) {
if (!field || !field.tableId) {
return null
@ -402,15 +415,8 @@
}
</script>
<ModalContent
title={originalName ? "Edit Column" : "Create Column"}
confirmText="Save Column"
onConfirm={saveColumn}
onCancel={cancelEdit}
disabled={invalid}
>
<Layout noPadding gap="S">
<Input
label="Name"
bind:value={editableColumn.name}
disabled={uneditable ||
(linkEditDisabled && editableColumn.type === LINK_TYPE)}
@ -419,12 +425,12 @@
<Select
disabled={!typeEnabled}
label="Type"
bind:value={editableColumn.type}
on:change={handleTypeChange}
options={getAllowedTypes()}
options={allowedTypes}
getOptionLabel={field => field.name}
getOptionValue={field => field.type}
getOptionIcon={field => field.icon}
isOptionEnabled={option => {
if (option.type == AUTO_TYPE) {
return availableAutoColumnKeys?.length > 0
@ -433,28 +439,6 @@
}}
/>
{#if canBeRequired || canBeDisplay}
<div>
{#if canBeRequired}
<Toggle
value={required}
on:change={onChangeRequired}
disabled={primaryDisplay}
thin
text="Required"
/>
{/if}
{#if canBeDisplay}
<Toggle
bind:value={primaryDisplay}
on:change={onChangePrimaryDisplay}
thin
text="Use as table display column"
/>
{/if}
</div>
{/if}
{#if editableColumn.type === "string"}
<Input
type="number"
@ -462,9 +446,9 @@
bind:value={editableColumn.constraints.length.maximum}
/>
{:else if editableColumn.type === "options"}
<ValuesList
label="Options (one per line)"
bind:values={editableColumn.constraints.inclusion}
<OptionSelectDnD
bind:constraints={editableColumn.constraints}
bind:optionColors={editableColumn.optionColors}
/>
{:else if editableColumn.type === "longform"}
<div>
@ -480,19 +464,28 @@
/>
</div>
{:else if editableColumn.type === "array"}
<ValuesList
label="Options (one per line)"
bind:values={editableColumn.constraints.inclusion}
<OptionSelectDnD
bind:constraints={editableColumn.constraints}
bind:optionColors={editableColumn.optionColors}
/>
{:else if editableColumn.type === "datetime" && !editableColumn.autocolumn}
<DatePicker
label="Earliest"
bind:value={editableColumn.constraints.datetime.earliest}
/>
<DatePicker
label="Latest"
bind:value={editableColumn.constraints.datetime.latest}
/>
<div class="split-label">
<div class="label-length">
<Label size="M">Earliest</Label>
</div>
<div class="input-length">
<DatePicker bind:value={editableColumn.constraints.datetime.earliest} />
</div>
</div>
<div class="split-label">
<div class="label-length">
<Label size="M">Latest</Label>
</div>
<div class="input-length">
<DatePicker bind:value={editableColumn.constraints.datetime.latest} />
</div>
</div>
{#if datasource?.source !== "ORACLE" && datasource?.source !== "SQL_SERVER"}
<div>
<Label
@ -509,16 +502,30 @@
</div>
{/if}
{:else if editableColumn.type === "number" && !editableColumn.autocolumn}
<Input
type="number"
label="Min Value"
bind:value={editableColumn.constraints.numericality.greaterThanOrEqualTo}
/>
<Input
type="number"
label="Max Value"
bind:value={editableColumn.constraints.numericality.lessThanOrEqualTo}
/>
<div class="split-label">
<div class="label-length">
<Label size="M">Max Value</Label>
</div>
<div class="input-length">
<Input
type="number"
bind:value={editableColumn.constraints.numericality
.greaterThanOrEqualTo}
/>
</div>
</div>
<div class="split-label">
<div class="label-length">
<Label size="M">Max Value</Label>
</div>
<div class="input-length">
<Input
type="number"
bind:value={editableColumn.constraints.numericality.lessThanOrEqualTo}
/>
</div>
</div>
{:else if editableColumn.type === "link"}
<Select
label="Table"
@ -547,32 +554,44 @@
/>
{:else if editableColumn.type === FORMULA_TYPE}
{#if !table.sql}
<Select
label="Formula type"
bind:value={editableColumn.formulaType}
options={[
{ label: "Dynamic", value: "dynamic" },
{ label: "Static", value: "static" },
]}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered or sorted by,
<div class="split-label">
<div class="label-length">
<Label size="M">Formula Type</Label>
</div>
<div class="input-length">
<Select
bind:value={editableColumn.formulaType}
options={[
{ label: "Dynamic", value: "dynamic" },
{ label: "Static", value: "static" },
]}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered or sorted by,
while static formula are calculated when the row is saved."
/>
/>
</div>
</div>
{/if}
<ModalBindableInput
title="Formula"
label="Formula"
value={editableColumn.formula}
on:change={e => {
editableColumn = {
...editableColumn,
formula: e.detail,
}
}}
bindings={getBindings({ table })}
allowJS
/>
<div class="split-label">
<div class="label-length">
<Label size="M">Formula</Label>
</div>
<div class="input-length">
<ModalBindableInput
title="Formula"
value={editableColumn.formula}
on:change={e => {
editableColumn = {
...editableColumn,
formula: e.detail,
}
}}
bindings={getBindings({ table })}
allowJS
/>
</div>
</div>
{:else if editableColumn.type === JSON_TYPE}
<Button primary text on:click={openJsonSchemaEditor}
>Open schema editor</Button
@ -591,12 +610,28 @@
/>
{/if}
<div slot="footer">
{#if !uneditable && originalName != null}
<Button warning text on:click={confirmDelete}>Delete</Button>
{/if}
</div>
</ModalContent>
{#if canBeRequired || canBeDisplay}
<div>
{#if canBeRequired}
<Toggle
value={required}
on:change={onChangeRequired}
disabled={primaryDisplay}
thin
text="Required"
/>
{/if}
</div>
{/if}
</Layout>
<div class="action-buttons">
{#if !uneditable && originalName != null}
<Button quiet warning text on:click={confirmDelete}>Delete</Button>
{/if}
<Button secondary newStyles on:click={cancelEdit}>Cancel</Button>
<Button disabled={invalid} newStyles cta on:click={saveColumn}>Save</Button>
</div>
<Modal bind:this={jsonSchemaModal}>
<JSONSchemaModal
schema={editableColumn.schema}
@ -607,6 +642,7 @@
}}
/>
</Modal>
<ConfirmDialog
bind:this={confirmDeleteDialog}
okText="Delete Column"
@ -622,3 +658,24 @@
</p>
<Input bind:value={deleteColName} placeholder={originalName} />
</ConfirmDialog>
<style>
.action-buttons {
display: flex;
justify-content: flex-end;
margin-top: var(--spacing-s);
gap: var(--spacing-l);
}
.split-label {
display: flex;
align-items: center;
}
.label-length {
flex-basis: 40%;
}
.input-length {
flex-grow: 1;
}
</style>

View file

@ -1,15 +1,8 @@
<script>
import { getContext, onMount } from "svelte"
import { Modal } from "@budibase/bbui"
import { getContext } from "svelte"
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
const { rows, subscribe } = getContext("grid")
let modal
onMount(() => subscribe("add-column", modal.show))
const { rows } = getContext("grid")
</script>
<Modal bind:this={modal}>
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />
</Modal>
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />

View file

@ -1,24 +1,19 @@
<script>
import { getContext, onMount } from "svelte"
import { Modal } from "@budibase/bbui"
import CreateEditColumn from "../CreateEditColumn.svelte"
const { rows, subscribe } = getContext("grid")
let editableColumn
let editColumnModal
const editColumn = column => {
editableColumn = column
editColumnModal.show()
}
onMount(() => subscribe("edit-column", editColumn))
</script>
<Modal bind:this={editColumnModal}>
<CreateEditColumn
field={editableColumn}
on:updatecolumns={rows.actions.refreshData}
/>
</Modal>
<CreateEditColumn
field={editableColumn}
on:updatecolumns={rows.actions.refreshData}
/>

View file

@ -2,6 +2,7 @@ export const FIELDS = {
STRING: {
name: "Text",
type: "string",
icon: "Text",
constraints: {
type: "string",
length: {},
@ -11,6 +12,7 @@ export const FIELDS = {
BARCODEQR: {
name: "Barcode/QR",
type: "barcodeqr",
icon: "Camera",
constraints: {
type: "string",
length: {},
@ -20,6 +22,7 @@ export const FIELDS = {
LONGFORM: {
name: "Long Form Text",
type: "longform",
icon: "TextAlignLeft",
constraints: {
type: "string",
length: {},
@ -29,6 +32,7 @@ export const FIELDS = {
OPTIONS: {
name: "Options",
type: "options",
icon: "Dropdown",
constraints: {
type: "string",
presence: false,
@ -38,6 +42,7 @@ export const FIELDS = {
ARRAY: {
name: "Multi-select",
type: "array",
icon: "Duplicate",
constraints: {
type: "array",
presence: false,
@ -47,6 +52,7 @@ export const FIELDS = {
NUMBER: {
name: "Number",
type: "number",
icon: "123",
constraints: {
type: "number",
presence: false,
@ -56,10 +62,12 @@ export const FIELDS = {
BIGINT: {
name: "BigInt",
type: "bigint",
icon: "TagBold",
},
BOOLEAN: {
name: "Boolean",
type: "boolean",
icon: "Boolean",
constraints: {
type: "boolean",
presence: false,
@ -68,6 +76,7 @@ export const FIELDS = {
DATETIME: {
name: "Date/Time",
type: "datetime",
icon: "Calendar",
constraints: {
type: "string",
length: {},
@ -81,6 +90,7 @@ export const FIELDS = {
ATTACHMENT: {
name: "Attachment",
type: "attachment",
icon: "Folder",
constraints: {
type: "array",
presence: false,
@ -89,6 +99,7 @@ export const FIELDS = {
LINK: {
name: "Relationship",
type: "link",
icon: "Link",
constraints: {
type: "array",
presence: false,
@ -97,11 +108,13 @@ export const FIELDS = {
FORMULA: {
name: "Formula",
type: "formula",
icon: "Calculator",
constraints: {},
},
JSON: {
name: "JSON",
type: "json",
icon: "Brackets",
constraints: {
type: "object",
presence: false,

View file

@ -1,7 +1,7 @@
<script>
import { getContext } from "svelte"
import { getContext, onMount, tick } from "svelte"
import GridCell from "./GridCell.svelte"
import { Icon, Popover, Menu, MenuItem } from "@budibase/bbui"
import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui"
import { getColumnIcon } from "../lib/utils"
export let column
@ -16,6 +16,7 @@
sort,
renderedColumns,
dispatch,
subscribe,
config,
ui,
columns,
@ -32,7 +33,9 @@
let anchor
let open = false
let editIsOpen = false
let timeout
let popover
$: sortedBy = column.name === $sort.column
$: canMoveLeft = orderable && idx > 0
@ -44,11 +47,16 @@
? "high-low"
: "Z-A"
const editColumn = () => {
const editColumn = async () => {
editIsOpen = true
await tick()
dispatch("edit-column", column.schema)
open = false
}
const cancelEdit = () => {
popover.hide()
editIsOpen = false
}
const onMouseDown = e => {
if (e.button === 0 && orderable) {
timeout = setTimeout(() => {
@ -109,6 +117,7 @@
columns.actions.saveChanges()
open = false
}
onMount(() => subscribe("close-edit-column", cancelEdit))
</script>
<div
@ -157,57 +166,74 @@
<Popover
bind:open
bind:this={popover}
{anchor}
align="right"
offset={0}
popoverTarget={document.getElementById(`grid-${rand}`)}
animate={false}
customZindex={100}
>
<Menu>
<MenuItem
icon="Edit"
on:click={editColumn}
disabled={!$config.allowSchemaChanges || column.schema.disabled}
{#if editIsOpen}
<div
use:clickOutside={() => {
editIsOpen = false
}}
class="content"
>
Edit column
</MenuItem>
<MenuItem
icon="Label"
on:click={makeDisplayColumn}
disabled={idx === "sticky" ||
!$config.allowSchemaChanges ||
bannedDisplayColumnTypes.includes(column.schema.type)}
>
Use as display column
</MenuItem>
<MenuItem
icon="SortOrderUp"
on:click={sortAscending}
disabled={column.name === $sort.column && $sort.order === "ascending"}
>
Sort {ascendingLabel}
</MenuItem>
<MenuItem
icon="SortOrderDown"
on:click={sortDescending}
disabled={column.name === $sort.column && $sort.order === "descending"}
>
Sort {descendingLabel}
</MenuItem>
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
Move left
</MenuItem>
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
Move right
</MenuItem>
<MenuItem
disabled={idx === "sticky" || !$config.showControls}
icon="VisibilityOff"
on:click={hideColumn}
>
Hide column
</MenuItem>
</Menu>
<slot />
</div>
{:else}
<Menu>
<MenuItem
icon="Edit"
on:click={editColumn}
disabled={!$config.allowSchemaChanges || column.schema.disabled}
>
Edit column
</MenuItem>
<MenuItem
icon="Label"
on:click={makeDisplayColumn}
disabled={idx === "sticky" ||
!$config.allowSchemaChanges ||
bannedDisplayColumnTypes.includes(column.schema.type)}
>
Use as display column
</MenuItem>
<MenuItem
icon="SortOrderUp"
on:click={sortAscending}
disabled={column.name === $sort.column && $sort.order === "ascending"}
>
Sort {ascendingLabel}
</MenuItem>
<MenuItem
icon="SortOrderDown"
on:click={sortDescending}
disabled={column.name === $sort.column && $sort.order === "descending"}
>
Sort {descendingLabel}
</MenuItem>
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
Move left
</MenuItem>
<MenuItem
disabled={!canMoveRight}
icon="ChevronRight"
on:click={moveRight}
>
Move right
</MenuItem>
<MenuItem
disabled={idx === "sticky" || !$config.showControls}
icon="VisibilityOff"
on:click={hideColumn}
>
Hide column
</MenuItem>
</Menu>
{/if}
</Popover>
<style>
@ -255,4 +281,13 @@
.header-cell:hover .sort-indicator {
display: none;
}
.content {
width: 300px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
background: var(--spectrum-alias-background-color-secondary);
}
</style>

View file

@ -18,6 +18,7 @@
let focusedOptionIdx = null
$: options = schema?.constraints?.inclusion || []
$: optionColors = schema?.optionColors || {}
$: editable = focused && !readonly
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
$: {
@ -93,7 +94,7 @@
on:click={editable ? open : null}
>
{#each values as val}
{@const color = getOptionColor(val)}
{@const color = optionColors[val] || getOptionColor(val)}
{#if color}
<div class="badge text" style="--color: {color}">
<span>
@ -121,7 +122,7 @@
use:clickOutside={close}
>
{#each options as option, idx}
{@const color = getOptionColor(option)}
{@const color = optionColors[option] || getOptionColor(option)}
<div
class="option"
on:click={() => toggleOption(option)}

View file

@ -139,9 +139,20 @@
{#if $loaded}
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
<div class="grid-data-inner">
<StickyColumn />
<StickyColumn>
<svelte:fragment slot="edit-column">
<slot name="edit-column" />
</svelte:fragment>
</StickyColumn>
<div class="grid-data-content">
<HeaderRow />
<HeaderRow>
<svelte:fragment slot="add-column">
<slot name="add-column" />
</svelte:fragment>
<svelte:fragment slot="edit-column">
<slot name="edit-column" />
</svelte:fragment>
</HeaderRow>
<GridBody />
</div>
{#if $canAddRows}

View file

@ -1,34 +1,22 @@
<script>
import NewColumnButton from "./NewColumnButton.svelte"
import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon, TempTooltip, TooltipType } from "@budibase/bbui"
import { TempTooltip, TooltipType } from "@budibase/bbui"
const {
renderedColumns,
dispatch,
scroll,
hiddenColumnsWidth,
width,
config,
hasNonAutoColumn,
tableId,
loading,
} = getContext("grid")
$: columnsWidth = $renderedColumns.reduce(
(total, col) => total + col.width,
0
)
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
$: left = Math.min($width - 40, end)
const { renderedColumns, config, hasNonAutoColumn, tableId, loading } =
getContext("grid")
</script>
<div class="header">
<GridScrollWrapper scrollHorizontally>
<div class="row">
{#each $renderedColumns as column, idx}
<HeaderCell {column} {idx} />
<HeaderCell {column} {idx}>
<slot name="edit-column" />
</HeaderCell>
{/each}
</div>
</GridScrollWrapper>
@ -39,13 +27,9 @@
type={TooltipType.Info}
condition={!$hasNonAutoColumn && !$loading}
>
<div
class="add"
style="left:{left}px;"
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
<NewColumnButton>
<slot name="add-column" />
</NewColumnButton>
</TempTooltip>
{/key}
{/if}
@ -61,21 +45,4 @@
.row {
display: flex;
}
.add {
height: var(--default-row-height);
display: grid;
place-items: center;
width: 40px;
position: absolute;
top: 0;
border-left: var(--cell-border);
border-right: var(--cell-border);
border-bottom: var(--cell-border);
background: var(--grid-background-alt);
z-index: 1;
}
.add:hover {
background: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
</style>

View file

@ -0,0 +1,79 @@
<script>
import { getContext, onMount } from "svelte"
import { Icon, Popover, clickOutside } from "@budibase/bbui"
const { renderedColumns, scroll, hiddenColumnsWidth, width, subscribe } =
getContext("grid")
let anchor
let open = false
$: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width),
0
)
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
$: left = Math.min($width - 40, end)
const close = () => {
open = false
}
onMount(() => subscribe("close-edit-column", close))
</script>
<div
id="add-column-button"
bind:this={anchor}
class="add"
style="left:{left}px"
on:click={() => (open = true)}
>
<Icon name="Add" />
</div>
<Popover
bind:open
{anchor}
align="right"
offset={0}
popoverTarget={document.getElementById(`add-column-button`)}
animate={false}
customZindex={100}
>
<div
use:clickOutside={() => {
open = false
}}
class="content"
>
<slot />
</div>
</Popover>
<style>
.add {
height: var(--default-row-height);
display: grid;
place-items: center;
width: 40px;
position: absolute;
top: 0;
border-left: var(--cell-border);
border-right: var(--cell-border);
border-bottom: var(--cell-border);
background: var(--grid-background-alt);
z-index: 1;
}
.add:hover {
background: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
.content {
width: 300px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 20px;
z-index: 2;
background: var(--spectrum-alias-background-color-secondary);
}
</style>

View file

@ -57,7 +57,9 @@
disabled={!$renderedRows.length}
/>
{#if $stickyColumn}
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky">
<slot name="edit-column" />
</HeaderCell>
{/if}
</div>