1
0
Fork 0
mirror of synced 2024-10-04 03:54:37 +13: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", "dayjs": "^1.10.4",
"easymde": "^2.16.1", "easymde": "^2.16.1",
"svelte-flatpickr": "3.2.3", "svelte-flatpickr": "3.2.3",
"svelte-portal": "^1.0.0" "svelte-portal": "^1.0.0",
"svelte-dnd-action": "^0.9.8"
}, },
"resolutions": { "resolutions": {
"loader-utils": "1.4.1" "loader-utils": "1.4.1"

View file

@ -1,5 +1,4 @@
<script> <script>
//import { createEventDispatcher } from "svelte"
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" 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 offset = 5
export let customHeight export let customHeight
export let animate = true export let animate = true
export let customZindex
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum" $: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
@ -77,8 +78,9 @@
}} }}
on:keydown={handleEscape} on:keydown={handleEscape}
class="spectrum-Popover is-open" class="spectrum-Popover is-open"
class:customZindex
role="presentation" role="presentation"
style="height: {customHeight}" style="height: {customHeight}; --customZindex: {customZindex};"
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }} transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
> >
<slot /> <slot />
@ -92,4 +94,8 @@
border-color: var(--spectrum-global-color-gray-300); border-color: var(--spectrum-global-color-gray-300);
overflow: auto; overflow: auto;
} }
.customZindex {
z-index: var(--customZindex) !important;
}
</style> </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 Slider } from "./Form/Slider.svelte"
export { default as Accordion } from "./Accordion/Accordion.svelte" export { default as Accordion } from "./Accordion/Accordion.svelte"
export { default as File } from "./Form/File.svelte" export { default as File } from "./Form/File.svelte"
export { default as OptionSelectDnD } from "./OptionSelectDnD/OptionSelectDnD.svelte"
// Renderers // Renderers
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte" export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte" export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,22 @@
<script> <script>
import NewColumnButton from "./NewColumnButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon, TempTooltip, TooltipType } from "@budibase/bbui" import { TempTooltip, TooltipType } from "@budibase/bbui"
const { const { renderedColumns, config, hasNonAutoColumn, tableId, loading } =
renderedColumns, getContext("grid")
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)
</script> </script>
<div class="header"> <div class="header">
<GridScrollWrapper scrollHorizontally> <GridScrollWrapper scrollHorizontally>
<div class="row"> <div class="row">
{#each $renderedColumns as column, idx} {#each $renderedColumns as column, idx}
<HeaderCell {column} {idx} /> <HeaderCell {column} {idx}>
<slot name="edit-column" />
</HeaderCell>
{/each} {/each}
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
@ -39,13 +27,9 @@
type={TooltipType.Info} type={TooltipType.Info}
condition={!$hasNonAutoColumn && !$loading} condition={!$hasNonAutoColumn && !$loading}
> >
<div <NewColumnButton>
class="add" <slot name="add-column" />
style="left:{left}px;" </NewColumnButton>
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
</TempTooltip> </TempTooltip>
{/key} {/key}
{/if} {/if}
@ -61,21 +45,4 @@
.row { .row {
display: flex; 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> </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} disabled={!$renderedRows.length}
/> />
{#if $stickyColumn} {#if $stickyColumn}
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" /> <HeaderCell column={$stickyColumn} orderable={false} idx="sticky">
<slot name="edit-column" />
</HeaderCell>
{/if} {/if}
</div> </div>