Merge branch 'develop' into BUDI-7189/crud_row_from_views

This commit is contained in:
Andrew Kingston 2023-08-01 10:38:52 +01:00 committed by GitHub
commit 60faa593a2
27 changed files with 1115 additions and 301 deletions

View File

@ -201,25 +201,24 @@ spec:
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }} image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always imagePullPolicy: Always
{{- if .Values.services.apps.startupProbe }}
{{- with .Values.services.apps.startupProbe }}
startupProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- if .Values.services.apps.livenessProbe }}
{{- with .Values.services.apps.livenessProbe }}
livenessProbe: livenessProbe:
httpGet: {{- toYaml . | nindent 10 }}
path: /health {{- end }}
port: {{ .Values.services.apps.port }} {{- end }}
initialDelaySeconds: 10 {{- if .Values.services.apps.readinessProbe }}
periodSeconds: 5 {{- with .Values.services.apps.readinessProbe }}
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3
readinessProbe: readinessProbe:
httpGet: {{- toYaml . | nindent 10 }}
path: /health {{- end }}
port: {{ .Values.services.apps.port }} {{- end }}
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3
name: bbapps name: bbapps
ports: ports:
- containerPort: {{ .Values.services.apps.port }} - containerPort: {{ .Values.services.apps.port }}

View File

@ -40,24 +40,24 @@ spec:
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }} - image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always imagePullPolicy: Always
name: proxy-service name: proxy-service
{{- if .Values.services.proxy.startupProbe }}
{{- with .Values.services.proxy.startupProbe }}
startupProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- if .Values.services.proxy.livenessProbe }}
{{- with .Values.services.proxy.livenessProbe }}
livenessProbe: livenessProbe:
httpGet: {{- toYaml . | nindent 10 }}
path: /health {{- end }}
port: {{ .Values.services.proxy.port }} {{- end }}
initialDelaySeconds: 0 {{- if .Values.services.proxy.readinessProbe }}
periodSeconds: 5 {{- with .Values.services.proxy.readinessProbe }}
successThreshold: 1
failureThreshold: 2
timeoutSeconds: 3
readinessProbe: readinessProbe:
httpGet: {{- toYaml . | nindent 10 }}
path: /health {{- end }}
port: {{ .Values.services.proxy.port }} {{- end }}
initialDelaySeconds: 0
periodSeconds: 5
successThreshold: 1
failureThreshold: 2
timeoutSeconds: 3
ports: ports:
- containerPort: {{ .Values.services.proxy.port }} - containerPort: {{ .Values.services.proxy.port }}
env: env:

View File

@ -190,24 +190,24 @@ spec:
{{ end }} {{ end }}
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }} image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
imagePullPolicy: Always imagePullPolicy: Always
{{- if .Values.services.worker.startupProbe }}
{{- with .Values.services.worker.startupProbe }}
startupProbe:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- if .Values.services.worker.livenessProbe }}
{{- with .Values.services.worker.livenessProbe }}
livenessProbe: livenessProbe:
httpGet: {{- toYaml . | nindent 10 }}
path: /health {{- end }}
port: {{ .Values.services.worker.port }} {{- end }}
initialDelaySeconds: 10 {{- if .Values.services.worker.readinessProbe }}
periodSeconds: 5 {{- with .Values.services.worker.readinessProbe }}
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3
readinessProbe: readinessProbe:
httpGet: {{- toYaml . | nindent 10 }}
path: /health {{- end }}
port: {{ .Values.services.worker.port }} {{- end }}
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3
name: bbworker name: bbworker
ports: ports:
- containerPort: {{ .Values.services.worker.port }} - containerPort: {{ .Values.services.worker.port }}

View File

@ -119,15 +119,37 @@ services:
port: 10000 port: 10000
replicaCount: 1 replicaCount: 1
upstreams: upstreams:
apps: 'http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}' apps: "http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}"
worker: 'http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}' worker: "http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}"
minio: 'http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}' minio: "http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}"
couchdb: 'http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}' couchdb: "http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}"
resources: {} resources: {}
# annotations: startupProbe:
# co.elastic.logs/module: nginx httpGet:
# co.elastic.logs/fileset.stdout: access path: /health
# co.elastic.logs/fileset.stderr: error port: 10000
scheme: HTTP
failureThreshold: 30
periodSeconds: 3
readinessProbe:
httpGet:
path: /health
port: 10000
scheme: HTTP
enabled: true
periodSeconds: 3
failureThreshold: 1
livenessProbe:
httpGet:
path: /health
port: 10000
scheme: HTTP
failureThreshold: 3
periodSeconds: 5
# annotations:
# co.elastic.logs/module: nginx
# co.elastic.logs/fileset.stdout: access
# co.elastic.logs/fileset.stderr: error
apps: apps:
port: 4002 port: 4002
@ -135,23 +157,67 @@ services:
logLevel: info logLevel: info
httpLogging: 1 httpLogging: 1
resources: {} resources: {}
# nodeDebug: "" # set the value of NODE_DEBUG startupProbe:
# annotations: httpGet:
# co.elastic.logs/multiline.type: pattern path: /health
# co.elastic.logs/multiline.pattern: '^[[:space:]]' port: 4002
# co.elastic.logs/multiline.negate: false scheme: HTTP
# co.elastic.logs/multiline.match: after failureThreshold: 30
periodSeconds: 3
readinessProbe:
httpGet:
path: /health
port: 4002
scheme: HTTP
enabled: true
periodSeconds: 3
failureThreshold: 1
livenessProbe:
httpGet:
path: /health
port: 4002
scheme: HTTP
failureThreshold: 3
periodSeconds: 5
# nodeDebug: "" # set the value of NODE_DEBUG
# annotations:
# co.elastic.logs/multiline.type: pattern
# co.elastic.logs/multiline.pattern: '^[[:space:]]'
# co.elastic.logs/multiline.negate: false
# co.elastic.logs/multiline.match: after
worker: worker:
port: 4003 port: 4003
replicaCount: 1 replicaCount: 1
logLevel: info logLevel: info
httpLogging: 1 httpLogging: 1
resources: {} resources: {}
# annotations: startupProbe:
# co.elastic.logs/multiline.type: pattern httpGet:
# co.elastic.logs/multiline.pattern: '^[[:space:]]' path: /health
# co.elastic.logs/multiline.negate: false port: 4003
# co.elastic.logs/multiline.match: after scheme: HTTP
failureThreshold: 30
periodSeconds: 3
readinessProbe:
httpGet:
path: /health
port: 4003
scheme: HTTP
enabled: true
periodSeconds: 3
failureThreshold: 1
livenessProbe:
httpGet:
path: /health
port: 4003
scheme: HTTP
failureThreshold: 3
periodSeconds: 5
# annotations:
# co.elastic.logs/multiline.type: pattern
# co.elastic.logs/multiline.pattern: '^[[:space:]]'
# co.elastic.logs/multiline.negate: false
# co.elastic.logs/multiline.match: after
couchdb: couchdb:
enabled: true enabled: true

View File

@ -1,5 +1,5 @@
{ {
"version": "2.8.29-alpha.3", "version": "2.8.29-alpha.7",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

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

@ -75,6 +75,14 @@
{ {
"name": "Chart", "name": "Chart",
"icon": "GraphBarVertical", "icon": "GraphBarVertical",
"children": ["bar", "line", "area", "candlestick", "pie", "donut"] "children": [
"bar",
"line",
"area",
"candlestick",
"pie",
"donut",
"histogram"
]
} }
] ]

View File

@ -2212,6 +2212,147 @@
} }
] ]
}, },
"histogram": {
"name": "Histogram Chart",
"description": "Histogram chart",
"icon": "Histogram",
"size": {
"width": 600,
"height": 400
},
"requiredAncestors": ["dataprovider"],
"settings": [
{
"type": "text",
"label": "Title",
"key": "title"
},
{
"type": "dataProvider",
"label": "Provider",
"key": "dataProvider",
"required": true
},
{
"type": "field",
"label": "Data column",
"key": "valueColumn",
"dependsOn": "dataProvider",
"required": true
},
{
"type": "text",
"label": "Y axis label",
"key": "yAxisLabel",
"defaultValue": "Frequency"
},
{
"type": "text",
"label": "X axis label",
"key": "xAxisLabel"
},
{
"type": "number",
"label": "Bucket count",
"key": "bucketCount",
"defaultValue": 10,
"min": 2
},
{
"type": "boolean",
"label": "Data labels",
"key": "dataLabels",
"defaultValue": false
},
{
"type": "text",
"label": "Width",
"key": "width"
},
{
"type": "text",
"label": "Height",
"key": "height",
"defaultValue": "400"
},
{
"type": "select",
"label": "Colors",
"key": "palette",
"defaultValue": "Palette 1",
"options": [
"Custom",
"Palette 1",
"Palette 2",
"Palette 3",
"Palette 4",
"Palette 5",
"Palette 6",
"Palette 7",
"Palette 8",
"Palette 9",
"Palette 10"
]
},
{
"type": "color",
"label": "C1",
"key": "c1",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C2",
"key": "c2",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C3",
"key": "c3",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C4",
"key": "c4",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "color",
"label": "C5",
"key": "c5",
"dependsOn": {
"setting": "palette",
"value": "Custom"
}
},
{
"type": "boolean",
"label": "Animate",
"key": "animate",
"defaultValue": true
},
{
"type": "boolean",
"label": "Horizontal",
"key": "horizontal",
"defaultValue": false
}
]
},
"form": { "form": {
"name": "Form", "name": "Form",
"icon": "Form", "icon": "Form",
@ -3965,6 +4106,10 @@
"label": "Bar", "label": "Bar",
"value": "bar" "value": "bar"
}, },
{
"label": "Histogram",
"value": "histogram"
},
{ {
"label": "Line", "label": "Line",
"value": "line" "value": "line"
@ -4215,6 +4360,47 @@
} }
] ]
}, },
{
"section": true,
"name": "Histogram Chart",
"icon": "Histogram",
"dependsOn": {
"setting": "chartType",
"value": "histogram"
},
"settings": [
{
"type": "field",
"label": "Value column",
"key": "valueColumn",
"dependsOn": "dataSource",
"required": true
},
{
"type": "text",
"label": "Y axis label",
"key": "yAxisLabel"
},
{
"type": "text",
"label": "X axis label",
"key": "xAxisLabel"
},
{
"type": "boolean",
"label": "Horizontal",
"key": "horizontal",
"defaultValue": false
},
{
"type": "number",
"label": "Bucket count",
"key": "bucketCount",
"defaultValue": 10,
"min": 2
}
]
},
{ {
"section": true, "section": true,
"name": "Line Chart", "name": "Line Chart",
@ -5234,11 +5420,7 @@
"type": "boolean", "type": "boolean",
"label": "Hide notifications", "label": "Hide notifications",
"key": "notificationOverride", "key": "notificationOverride",
"defaultValue": false, "defaultValue": false
"dependsOn": {
"setting": "showSaveButton",
"value": true
}
} }
] ]
} }

View File

@ -46,6 +46,9 @@
export let lowColumn export let lowColumn
export let dateColumn export let dateColumn
// Histogram
export let bucketCount
let dataProviderId let dataProviderId
$: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null $: colors = c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null
@ -92,6 +95,7 @@
highColumn, highColumn,
lowColumn, lowColumn,
dateColumn, dateColumn,
bucketCount,
}} }}
/> />
{/if} {/if}

View File

@ -83,6 +83,7 @@
tableId: dataSource?.tableId, tableId: dataSource?.tableId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`, rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`, revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
notificationOverride,
}, },
}, },
{ {

View File

@ -0,0 +1,136 @@
<script>
import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
import ApexChart from "./ApexChart.svelte"
export let title
export let dataProvider
export let valueColumn
export let xAxisLabel
export let yAxisLabel
export let height
export let width
export let dataLabels
export let animate
export let palette
export let c1, c2, c3, c4, c5
export let horizontal
export let bucketCount = 10
$: options = setUpChart(
title,
dataProvider,
valueColumn,
xAxisLabel || valueColumn,
yAxisLabel,
height,
width,
dataLabels,
animate,
palette,
horizontal,
c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null,
customColor,
bucketCount
)
$: customColor = palette === "Custom"
const setUpChart = (
title,
dataProvider,
valueColumn,
xAxisLabel, //freqAxisLabel
yAxisLabel, //valueAxisLabel
height,
width,
dataLabels,
animate,
palette,
horizontal,
colors,
customColor,
bucketCount
) => {
const allCols = [valueColumn]
if (
!dataProvider ||
!dataProvider.rows?.length ||
allCols.find(x => x == null)
) {
return null
}
// Fetch data
const { schema, rows } = dataProvider
const reducer = row => (valid, column) => valid && row[column] != null
const hasAllColumns = row => allCols.reduce(reducer(row), true)
const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)
if (!schema || !data.length) {
return null
}
// Initialise default chart
let builder = new ApexOptionsBuilder()
.type("bar")
.title(title)
.width(width)
.height(height)
.xLabel(horizontal ? yAxisLabel : xAxisLabel)
.yLabel(horizontal ? xAxisLabel : yAxisLabel)
.dataLabels(dataLabels)
.animate(animate)
.palette(palette)
.horizontal(horizontal)
.colors(customColor ? colors : null)
if (horizontal) {
builder = builder.setOption(["plotOptions", "bar", "barHeight"], "90%")
} else {
builder = builder.setOption(["plotOptions", "bar", "columnWidth"], "99%")
}
// Pull occurences of the value.
let flatlist = data.map(row => {
return row[valueColumn]
})
// Build range buckets
let interval = Math.max(...flatlist) / bucketCount
let counts = Array(bucketCount).fill(0)
// Assign row data to a bucket
let buckets = flatlist.reduce((acc, val) => {
let dest = Math.min(Math.floor(val / interval), bucketCount - 1)
acc[dest] = acc[dest] + 1
return acc
}, counts)
const rangeLabel = bucketIdx => {
return `${Math.floor(interval * bucketIdx)} - ${Math.floor(
interval * (bucketIdx + 1)
)}`
}
const series = [
{
name: yAxisLabel,
data: Array.from({ length: buckets.length }, (_, i) => ({
x: rangeLabel(i),
y: buckets[i],
})),
},
]
builder = builder.setOption(["xaxis", "labels"], {
formatter: x => {
return x + ""
},
})
builder = builder.series(series)
return builder.getOptions()
}
</script>
<ApexChart {options} />

View File

@ -4,3 +4,4 @@ export { default as pie } from "./PieChart.svelte"
export { default as donut } from "./DonutChart.svelte" export { default as donut } from "./DonutChart.svelte"
export { default as area } from "./AreaChart.svelte" export { default as area } from "./AreaChart.svelte"
export { default as candlestick } from "./CandleStickChart.svelte" export { default as candlestick } from "./CandleStickChart.svelte"
export { default as histogram } from "./HistogramChart.svelte"

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>