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 }}
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:
httpGet:
path: /health
port: {{ .Values.services.apps.port }}
initialDelaySeconds: 10
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
{{- if .Values.services.apps.readinessProbe }}
{{- with .Values.services.apps.readinessProbe }}
readinessProbe:
httpGet:
path: /health
port: {{ .Values.services.apps.port }}
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
failureThreshold: 3
timeoutSeconds: 3
{{- toYaml . | nindent 10 }}
{{- end }}
{{- end }}
name: bbapps
ports:
- containerPort: {{ .Values.services.apps.port }}

View File

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

View File

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

View File

@ -119,15 +119,37 @@ services:
port: 10000
replicaCount: 1
upstreams:
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 }}'
minio: 'http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}'
couchdb: 'http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.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 }}"
minio: "http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}"
couchdb: "http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}"
resources: {}
# annotations:
# co.elastic.logs/module: nginx
# co.elastic.logs/fileset.stdout: access
# co.elastic.logs/fileset.stderr: error
startupProbe:
httpGet:
path: /health
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:
port: 4002
@ -135,23 +157,67 @@ services:
logLevel: info
httpLogging: 1
resources: {}
# 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
startupProbe:
httpGet:
path: /health
port: 4002
scheme: HTTP
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:
port: 4003
replicaCount: 1
logLevel: info
httpLogging: 1
resources: {}
# 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
startupProbe:
httpGet:
path: /health
port: 4003
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:
enabled: true

View File

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

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}
<div class="split-label">
<div class="label-length">
<Label size="M">Max Value</Label>
</div>
<div class="input-length">
<Input
type="number"
label="Min Value"
bind:value={editableColumn.constraints.numericality.greaterThanOrEqualTo}
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"
label="Max Value"
bind:value={editableColumn.constraints.numericality.lessThanOrEqualTo}
/>
</div>
</div>
{:else if editableColumn.type === "link"}
<Select
label="Table"
@ -547,8 +554,12 @@
/>
{:else if editableColumn.type === FORMULA_TYPE}
{#if !table.sql}
<div class="split-label">
<div class="label-length">
<Label size="M">Formula Type</Label>
</div>
<div class="input-length">
<Select
label="Formula type"
bind:value={editableColumn.formulaType}
options={[
{ label: "Dynamic", value: "dynamic" },
@ -559,10 +570,16 @@
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}
<div class="split-label">
<div class="label-length">
<Label size="M">Formula</Label>
</div>
<div class="input-length">
<ModalBindableInput
title="Formula"
label="Formula"
value={editableColumn.formula}
on:change={e => {
editableColumn = {
@ -573,6 +590,8 @@
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 canBeRequired || canBeDisplay}
<div>
{#if canBeRequired}
<Toggle
value={required}
on:change={onChangeRequired}
disabled={primaryDisplay}
thin
text="Required"
/>
{/if}
</div>
</ModalContent>
{/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
<CreateEditColumn
field={editableColumn}
on:updatecolumns={rows.actions.refreshData}
/>
</Modal>
/>

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

@ -75,6 +75,14 @@
{
"name": "Chart",
"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": {
"name": "Form",
"icon": "Form",
@ -3965,6 +4106,10 @@
"label": "Bar",
"value": "bar"
},
{
"label": "Histogram",
"value": "histogram"
},
{
"label": "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,
"name": "Line Chart",
@ -5234,11 +5420,7 @@
"type": "boolean",
"label": "Hide notifications",
"key": "notificationOverride",
"defaultValue": false,
"dependsOn": {
"setting": "showSaveButton",
"value": true
}
"defaultValue": false
}
]
}

View File

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

View File

@ -83,6 +83,7 @@
tableId: dataSource?.tableId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
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 area } from "./AreaChart.svelte"
export { default as candlestick } from "./CandleStickChart.svelte"
export { default as histogram } from "./HistogramChart.svelte"

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,12 +166,24 @@
<Popover
bind:open
bind:this={popover}
{anchor}
align="right"
offset={0}
popoverTarget={document.getElementById(`grid-${rand}`)}
animate={false}
customZindex={100}
>
{#if editIsOpen}
<div
use:clickOutside={() => {
editIsOpen = false
}}
class="content"
>
<slot />
</div>
{:else}
<Menu>
<MenuItem
icon="Edit"
@ -197,7 +218,11 @@
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
Move left
</MenuItem>
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
<MenuItem
disabled={!canMoveRight}
icon="ChevronRight"
on:click={moveRight}
>
Move right
</MenuItem>
<MenuItem
@ -208,6 +233,7 @@
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>