Merge branch 'chore/npmless-builds' into chore/pipeline_npm_version_updates

This commit is contained in:
Adria Navarro 2023-05-02 14:07:44 +01:00
commit 9ffd43b682
71 changed files with 1461 additions and 557 deletions

View File

@ -79,7 +79,6 @@ jobs:
- name: Build/release Docker images
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
yarn build
yarn build:docker
env:
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}

View File

@ -1,5 +1,5 @@
{
"version": "0.0.999-alpha.38",
"version": "2.5.6-alpha.28",
"npmClient": "yarn",
"packages": [
"packages/backend-core",

View File

@ -43,7 +43,7 @@
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
"dev:noserver": "yarn run kill-builder && lerna link && lerna run --stream dev:stack:up && lerna run --stream --parallel dev:builder --concurrency 1 --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream --parallel dev:builder --concurrency 1 --scope @budibase/backend-core --scope @budibase/worker --scope @budibase/server",
"dev:built": "cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream --parallel dev:built",
"test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages && eslint qa-core",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",

View File

@ -1,3 +1,5 @@
import { existsSync, readFileSync } from "fs"
function isTest() {
return isCypress() || isJest()
}
@ -45,6 +47,35 @@ function httpLogging() {
return process.env.HTTP_LOGGING
}
function findVersion() {
function findFileInAncestors(
fileName: string,
currentDir: string
): string | null {
const filePath = `${currentDir}/${fileName}`
if (existsSync(filePath)) {
return filePath
}
const parentDir = `${currentDir}/..`
if (parentDir === currentDir) {
// reached root directory
return null
}
return findFileInAncestors(fileName, parentDir)
}
try {
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
const content = readFileSync(packageJsonFile!, "utf-8")
const version = JSON.parse(content).version
return version
} catch {
throw new Error("Cannot find a valid version in its package.json")
}
}
const environment = {
isTest,
isJest,
@ -122,6 +153,7 @@ const environment = {
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
? process.env.ENABLE_SSO_MAINTENANCE_MODE
: false,
VERSION: findVersion(),
_set(key: any, value: any) {
process.env[key] = value
// @ts-ignore

View File

@ -23,8 +23,6 @@ import * as installation from "../installation"
import * as configs from "../configs"
import { withCache, TTL, CacheKey } from "../cache/generic"
const pkg = require("../../package.json")
/**
* An identity can be:
* - account user (Self host)
@ -102,7 +100,7 @@ const identifyInstallationGroup = async (
const id = installId
const type = IdentityType.INSTALLATION
const hosting = getHostingFromEnv()
const version = pkg.version
const version = env.VERSION
const environment = getDeploymentEnvironment()
const group: InstallationGroup = {

View File

@ -4,7 +4,6 @@ import { EventProcessor } from "../types"
import env from "../../../environment"
import * as context from "../../../context"
import * as rateLimiting from "./rateLimiting"
const pkg = require("../../../../package.json")
const EXCLUDED_EVENTS: Event[] = [
Event.USER_UPDATED,
@ -49,7 +48,7 @@ export default class PosthogProcessor implements EventProcessor {
properties = this.clearPIIProperties(properties)
properties.version = pkg.version
properties.version = env.VERSION
properties.service = env.SERVICE
properties.environment = identity.environment
properties.hosting = identity.hosting

View File

@ -6,8 +6,7 @@ import { Installation, IdentityType, Database } from "@budibase/types"
import * as context from "./context"
import semver from "semver"
import { bustCache, withCache, TTL, CacheKey } from "./cache/generic"
const pkg = require("../package.json")
import environment from "./environment"
export const getInstall = async (): Promise<Installation> => {
return withCache(CacheKey.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, {
@ -18,7 +17,7 @@ async function createInstallDoc(platformDb: Database) {
const install: Installation = {
_id: StaticDatabases.PLATFORM_INFO.docs.install,
installId: newid(),
version: pkg.version,
version: environment.VERSION,
}
try {
const resp = await platformDb.put(install)
@ -80,7 +79,7 @@ export const checkInstallVersion = async (): Promise<void> => {
const install = await getInstall()
const currentVersion = install.version
const newVersion = pkg.version
const newVersion = environment.VERSION
if (currentVersion !== newVersion) {
const isUpgrade = semver.gt(newVersion, currentVersion)

View File

@ -10,15 +10,11 @@
"incremental": true,
"sourceMap": true,
"declaration": true,
"types": [ "node", "jest" ],
"types": ["node", "jest"],
"outDir": "dist",
"skipLibCheck": true
},
"include": [
"**/*.js",
"**/*.ts",
"package.json"
],
"include": ["**/*.js", "**/*.ts"],
"exclude": [
"node_modules",
"dist",

View File

@ -10,7 +10,10 @@ import { auth } from "./stores/portal"
export const API = createAPIClient({
attachHeaders: headers => {
// Attach app ID header from store
headers["x-budibase-app-id"] = get(store).appId
let appId = get(store).appId
if (appId) {
headers["x-budibase-app-id"] = appId
}
// Add csrf token if authenticated
const user = get(auth).user

View File

@ -42,13 +42,14 @@
<GridCreateViewButton />
{/if}
<GridManageAccessButton />
{#if isUsersTable}
<EditRolesButton />
{/if}
{#if !isInternal}
<GridRelationshipButton />
{/if}
<GridImportButton disabled={isUsersTable} />
{#if isUsersTable}
<EditRolesButton />
{:else}
<GridImportButton />
{/if}
<GridExportButton />
<GridFilterButton />
<GridAddColumnModal />

View File

@ -16,7 +16,7 @@
</script>
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
Manage access
Access
</ActionButton>
<Modal bind:this={modal}>
<ManageAccessModal

View File

@ -11,7 +11,7 @@
</script>
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
Create view
Add view
</ActionButton>
<Modal bind:this={modal}>
<CreateViewModal />

View File

@ -1,7 +1,7 @@
<script>
import CreateEditRow from "../../modals/CreateEditRow.svelte"
import { getContext, onMount } from "svelte"
import { Modal } from "@budibase/bbui"
import { Modal, notifications } from "@budibase/bbui"
import { cloneDeep } from "lodash/fp"
const { subscribe, rows } = getContext("grid")
@ -9,6 +9,11 @@
let modal
let row
const deleteRow = e => {
rows.actions.deleteRows([e.detail])
notifications.success("Deleted 1 row")
}
onMount(() =>
subscribe("add-row", () => {
row = {}
@ -24,5 +29,9 @@
</script>
<Modal bind:this={modal}>
<CreateEditRow {row} on:updaterows={e => rows.actions.refreshRow(e.detail)} />
<CreateEditRow
{row}
on:updaterows={e => rows.actions.refreshRow(e.detail)}
on:deleteRows={deleteRow}
/>
</Modal>

View File

@ -21,6 +21,7 @@ import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCom
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
import BarButtonList from "./controls/BarButtonList.svelte"
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
const componentMap = {
text: DrawerBindableCombobox,
@ -43,6 +44,7 @@ const componentMap = {
section: SectionSelect,
filter: FilterEditor,
url: URLSelect,
fieldConfiguration: FieldConfiguration,
columns: ColumnEditor,
"columns/basic": BasicColumnEditor,
"field/sortable": SortableFieldSelect,

View File

@ -0,0 +1,91 @@
<script>
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import { getFields } from "helpers/searchFields"
export let componentInstance
export let value = []
export let allowCellEditing = true
export let subject = "Table"
const dispatch = createEventDispatcher()
let drawer
let boundValue
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = allowCellEditing
? Object.keys(schema || {})
: enrichedSchemaFields?.map(field => field.name)
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
allowLinks: true,
})
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
// Don't show ID and rev in tables
if (schema) {
delete schema._id
delete schema._rev
}
return schema
}
const updateBoundValue = value => {
boundValue = cloneDeep(value)
}
const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) {
return []
}
// We need to account for legacy configs which would just be an array
// of strings
if (typeof columns[0] === "string") {
columns = columns.map(col => ({
name: col,
displayName: col,
}))
}
return columns.filter(column => {
return options.includes(column.name)
})
}
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
}
const save = () => {
dispatch("change", getValidColumns(boundValue, options))
drawer.hide()
}
</script>
<ActionButton on:click={open}>Configure columns</ActionButton>
<Drawer bind:this={drawer} title="{subject} Columns">
<svelte:fragment slot="description">
Configure the columns in your {subject.toLowerCase()}.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer
slot="body"
bind:columns={boundValue}
{options}
{schema}
{allowCellEditing}
/>
</Drawer>

View File

@ -0,0 +1,26 @@
<script>
import { DrawerContent, Drawer, Button, Icon } from "@budibase/bbui"
import ValidationDrawer from "components/design/settings/controls/ValidationEditor/ValidationDrawer.svelte"
export let column
export let type
let drawer
</script>
<Icon name="Settings" hoverable size="S" on:click={drawer.show} />
<Drawer bind:this={drawer} title="Field Validation">
<svelte:fragment slot="description">
"{column.name}" field validation
</svelte:fragment>
<Button cta slot="buttons" on:click={drawer.hide}>Save</Button>
<DrawerContent slot="body">
<div class="container">
<ValidationDrawer
slot="body"
bind:rules={column.validation}
fieldName={column.name}
{type}
/>
</div>
</DrawerContent>
</Drawer>

View File

@ -0,0 +1,202 @@
<script>
import {
Button,
Icon,
DrawerContent,
Layout,
Select,
Label,
Body,
Input,
} from "@budibase/bbui"
import { flip } from "svelte/animate"
import { dndzone } from "svelte-dnd-action"
import { generate } from "shortid"
import CellEditor from "./CellEditor.svelte"
export let columns = []
export let options = []
export let schema = {}
const flipDurationMs = 150
let dragDisabled = true
$: unselectedColumns = getUnselectedColumns(options, columns)
$: columns.forEach(column => {
if (!column.id) {
column.id = generate()
}
})
const getUnselectedColumns = (allColumns, selectedColumns) => {
let optionsObj = {}
allColumns.forEach(option => {
optionsObj[option] = true
})
selectedColumns?.forEach(column => {
delete optionsObj[column.name]
})
return Object.keys(optionsObj)
}
const getRemainingColumnOptions = selectedColumn => {
if (!selectedColumn || unselectedColumns.includes(selectedColumn)) {
return unselectedColumns
}
return [selectedColumn, ...unselectedColumns]
}
const addColumn = () => {
columns = [...columns, {}]
}
const removeColumn = id => {
columns = columns.filter(column => column.id !== id)
}
const updateColumnOrder = e => {
columns = e.detail.items
}
const handleFinalize = e => {
updateColumnOrder(e)
dragDisabled = true
}
const addAllColumns = () => {
let newColumns = columns || []
options.forEach(field => {
const fieldSchema = schema[field]
const hasCol = columns && columns.findIndex(x => x.name === field) !== -1
if (!fieldSchema?.autocolumn && !hasCol) {
newColumns.push({
name: field,
displayName: field,
})
}
})
columns = newColumns
}
const reset = () => {
columns = []
}
const getFieldType = column => {
return `validation/${schema[column.name]?.type}`
}
</script>
<DrawerContent>
<div class="container">
<Layout noPadding gap="S">
{#if columns?.length}
<Layout noPadding gap="XS">
<div class="column">
<div />
<Label size="L">Column</Label>
<Label size="L">Label</Label>
<div />
<div />
</div>
<div
class="columns"
use:dndzone={{
items: columns,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled,
}}
on:finalize={handleFinalize}
on:consider={updateColumnOrder}
>
{#each columns as column (column.id)}
<div class="column" animate:flip={{ duration: flipDurationMs }}>
<div
class="handle"
aria-label="drag-handle"
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
on:mousedown={() => (dragDisabled = false)}
>
<Icon name="DragHandle" size="XL" />
</div>
<Select
bind:value={column.name}
placeholder="Column"
options={getRemainingColumnOptions(column.name)}
on:change={e => (column.displayName = e.detail)}
/>
<Input bind:value={column.displayName} placeholder="Label" />
<CellEditor type={getFieldType(column)} bind:column />
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeColumn(column.id)}
disabled={columns.length === 1}
/>
</div>
{/each}
</div>
</Layout>
{:else}
<div class="column">
<div class="wide">
<Body size="S">Add columns to be included in your form below.</Body>
</div>
</div>
{/if}
<div class="column">
<div class="buttons wide">
<Button secondary icon="Add" on:click={addColumn}>Add column</Button>
<Button secondary quiet on:click={addAllColumns}>
Add all columns
</Button>
{#if columns?.length}
<Button secondary quiet on:click={reset}>Reset columns</Button>
{/if}
</div>
</div>
</Layout>
</div>
</DrawerContent>
<style>
.container {
width: 100%;
max-width: 600px;
margin: 0 auto;
}
.columns {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-m);
}
.column {
gap: var(--spacing-l);
display: grid;
grid-template-columns: 20px 1fr 1fr 16px 16px;
align-items: center;
border-radius: var(--border-radius-s);
transition: background-color ease-in-out 130ms;
}
.column:hover {
background-color: var(--spectrum-global-color-gray-100);
}
.handle {
display: grid;
place-items: center;
}
.wide {
grid-column: 2 / -1;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-m);
}
</style>

View File

@ -0,0 +1,89 @@
<script>
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import { getFields } from "helpers/searchFields"
export let componentInstance
export let value = []
const convertOldColumnFormat = oldColumns => {
if (typeof oldColumns?.[0] === "string") {
value = oldColumns.map(field => ({ name: field, displayName: field }))
}
}
$: convertOldColumnFormat(value)
const dispatch = createEventDispatcher()
let drawer
let boundValue
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options)
$: updateBoundValue(sanitisedValue)
$: enrichedSchemaFields = getFields(Object.values(schema || {}), {
allowLinks: true,
})
const getSchema = (asset, datasource) => {
const schema = getSchemaForDatasource(asset, datasource).schema
// Don't show ID and rev in tables
if (schema) {
delete schema._id
delete schema._rev
}
return schema
}
const updateBoundValue = value => {
boundValue = cloneDeep(value)
}
const getValidColumns = (columns, options) => {
if (!Array.isArray(columns) || !columns.length) {
return []
}
// We need to account for legacy configs which would just be an array
// of strings
if (typeof columns[0] === "string") {
columns = columns.map(col => ({
name: col,
displayName: col,
}))
}
return columns.filter(column => {
return options.includes(column.name)
})
}
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
}
const save = () => {
dispatch("change", getValidColumns(boundValue, options))
drawer.hide()
}
</script>
<ActionButton on:click={open}>Configure fields</ActionButton>
<Drawer bind:this={drawer} title="Form Fields">
<svelte:fragment slot="description">
Configure the fields in your form.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer>

View File

@ -16,6 +16,7 @@
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { generate } from "shortid"
export let fieldName = null
export let rules = []
export let bindings = []
export let type
@ -124,7 +125,7 @@
}
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
$: field = $selectedComponent?.field
$: field = fieldName || $selectedComponent?.field
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
$: fieldType = type?.split("/")[1] || "string"
$: constraintOptions = getConstraintsForType(fieldType)
@ -140,8 +141,12 @@
const formParent = findClosestMatchingComponent(
asset.props,
component._id,
component => component._component.endsWith("/form")
component =>
component._component.endsWith("/form") ||
component._component.endsWith("/formblock") ||
component._component.endsWith("/tableblock")
)
return getSchemaForDatasource(asset, formParent?.dataSource)
}

View File

@ -33,7 +33,7 @@
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
// Keep URL and state in sync for selected screen ID
// Keep URL and state in sync for selected app ID
const stopSyncing = syncURLToState({
urlParam: "appId",
stateKey: "selectedAppId",
@ -47,6 +47,7 @@
let deletionModal
let exportPublishedVersion = false
let deletionConfirmationAppName
let loaded = false
$: app = $overview.selectedApp
$: appId = $overview.selectedAppId
@ -56,10 +57,12 @@
$: lockedByYou = $auth.user.email === app?.lockedBy?.email
const initialiseApp = async appId => {
loaded = false
try {
const pkg = await API.fetchAppPackage(appId)
await store.actions.initialise(pkg)
await API.syncApp(appId)
loaded = true
} catch (error) {
notifications.error("Error initialising app overview")
$goto("../../")
@ -228,7 +231,9 @@
active={$isActive("./version")}
/>
</SideNav>
{#if loaded}
<slot />
{/if}
</Content>
</Layout>
</Page>

View File

@ -3,17 +3,17 @@ import { logging } from "@budibase/backend-core"
logging.disableLogger()
import "./prebuilds"
import "./environment"
import { env } from "@budibase/backend-core"
import { getCommands } from "./options"
import { Command } from "commander"
import { getHelpDescription } from "./utils"
const json = require("../package.json")
// add hosting config
async function init() {
const program = new Command()
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
.helpOption(false)
.version(json.version)
.version(env.VERSION)
// add commands
for (let command of getCommands()) {
command.configure(program)

View File

@ -15,7 +15,7 @@
"require": ["tsconfig-paths/register"],
"swc": true
},
"include": ["src/**/*", "package.json"],
"references": [{ "path": "../types" }, { "path": "../backend-core" }],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -4435,6 +4435,48 @@
"key": "row"
}
]
},
{
"label": "Fields",
"type": "fieldConfiguration",
"key": "sidePanelFields",
"nested": true,
"dependsOn": {
"setting": "clickBehaviour",
"value": "details"
}
},
{
"label": "Show delete",
"type": "boolean",
"key": "sidePanelShowDelete",
"nested": true,
"dependsOn": {
"setting": "clickBehaviour",
"value": "details"
}
},
{
"label": "Save label",
"type": "text",
"key": "sidePanelSaveLabel",
"defaultValue": "Save",
"nested": true,
"dependsOn": {
"setting": "clickBehaviour",
"value": "details"
}
},
{
"label": "Delete label",
"type": "text",
"key": "sidePanelDeleteLabel",
"defaultValue": "Delete",
"nested": true,
"dependsOn": {
"setting": "clickBehaviour",
"value": "details"
}
}
]
},
@ -4979,7 +5021,7 @@
"name": "Fields",
"settings": [
{
"type": "multifield",
"type": "fieldConfiguration",
"label": "Fields",
"key": "fields",
"selectAllFields": true
@ -5028,6 +5070,17 @@
"invert": true
}
},
{
"type": "text",
"key": "saveButtonLabel",
"label": "Save button label",
"nested": true,
"defaultValue": "Save",
"dependsOn": {
"setting": "showSaveButton",
"value": true
}
},
{
"type": "boolean",
"label": "Allow delete",
@ -5038,6 +5091,17 @@
"value": "Update"
}
},
{
"type": "text",
"key": "deleteButtonLabel",
"label": "Delete button label",
"nested": true,
"defaultValue": "Delete",
"dependsOn": {
"setting": "showDeleteButton",
"value": true
}
},
{
"type": "url",
"label": "Navigate after button press",

View File

@ -26,6 +26,10 @@
export let titleButtonClickBehaviour
export let onClickTitleButton
export let noRowsMessage
export let sidePanelFields
export let sidePanelShowDelete
export let sidePanelSaveLabel
export let sidePanelDeleteLabel
const { fetchDatasourceSchema, API } = getContext("sdk")
const stateKey = `ID_${generate()}`
@ -241,10 +245,12 @@
props={{
dataSource,
showSaveButton: true,
showDeleteButton: true,
showDeleteButton: sidePanelShowDelete,
saveButtonLabel: sidePanelSaveLabel,
deleteButtonLabel: sidePanelDeleteLabel,
actionType: "Update",
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
fields: normalFields,
fields: sidePanelFields || normalFields,
title: editTitle,
labelPosition: "left",
}}
@ -266,8 +272,9 @@
dataSource,
showSaveButton: true,
showDeleteButton: false,
saveButtonLabel: sidePanelSaveLabel,
actionType: "Create",
fields: normalFields,
fields: sidePanelFields || normalFields,
title: "Create Row",
labelPosition: "left",
}}

View File

@ -12,6 +12,8 @@
export let fields
export let labelPosition
export let title
export let saveButtonLabel
export let deleteButtonLabel
export let showSaveButton
export let showDeleteButton
export let rowId
@ -20,10 +22,40 @@
const { fetchDatasourceSchema } = getContext("sdk")
const convertOldFieldFormat = fields => {
if (typeof fields?.[0] === "string") {
return fields.map(field => ({ name: field, displayName: field }))
}
return fields
}
const getDefaultFields = (fields, schema) => {
if (schema && (!fields || fields.length === 0)) {
const defaultFields = []
Object.values(schema).forEach(field => {
if (field.autocolumn) return
defaultFields.push({
name: field.name,
displayName: field.name,
})
})
return defaultFields
}
return fields
}
let schema
let providerId
let repeaterId
$: formattedFields = convertOldFieldFormat(fields)
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
$: fetchSchema(dataSource)
$: dataProvider = `{{ literal ${safe(providerId)} }}`
$: filter = [
@ -46,9 +78,11 @@
actionType,
size,
disabled,
fields,
fields: fieldsOrDefault,
labelPosition,
title,
saveButtonLabel,
deleteButtonLabel,
showSaveButton,
showDeleteButton,
schema,

View File

@ -11,6 +11,8 @@
export let fields
export let labelPosition
export let title
export let saveButtonLabel
export let deleteButtonLabel
export let showSaveButton
export let showDeleteButton
export let schema
@ -33,6 +35,12 @@
let formId
$: onSave = [
{
"##eventHandlerType": "Validate Form",
parameters: {
componentId: formId,
},
},
{
"##eventHandlerType": "Save Row",
parameters: {
@ -163,7 +171,7 @@
<BlockComponent
type="button"
props={{
text: "Delete",
text: deleteButtonLabel || "Delete",
onClick: onDelete,
quiet: true,
type: "secondary",
@ -175,7 +183,7 @@
<BlockComponent
type="button"
props={{
text: "Save",
text: saveButtonLabel || "Save",
onClick: onSave,
type: "cta",
}}
@ -188,14 +196,14 @@
{/if}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
{#each fields as field, idx}
{#if getComponentForField(field)}
{#if getComponentForField(field.name)}
<BlockComponent
type={getComponentForField(field)}
type={getComponentForField(field.name)}
props={{
field,
label: field,
placeholder: field,
disabled,
validation: field.validation,
field: field.name,
label: field.displayName,
placeholder: field.displayName,
}}
order={idx}
/>

View File

@ -72,6 +72,7 @@
api = {
focus: () => open(),
blur: () => close(),
isActive: () => isOpen,
onKeyDown,
}
})

View File

@ -48,8 +48,9 @@
}
const cellAPI = {
focus: () => api?.focus(),
blur: () => api?.blur(),
focus: () => api?.focus?.(),
blur: () => api?.blur?.(),
isActive: () => api?.isActive?.() ?? false,
onKeyDown: (...params) => api?.onKeyDown(...params),
isReadonly: () => readonly,
getType: () => column.schema.type,
@ -67,6 +68,7 @@
{rowIdx}
{focused}
{selectedUser}
{readonly}
error={$error}
on:click={() => focusedCellId.set(cellId)}
on:contextmenu={e => menu.actions.open(cellId, e)}

View File

@ -8,6 +8,7 @@
export let rowIdx
export let defaultHeight = false
export let center = false
export let readonly = false
$: style = getStyle(width, selectedUser)
@ -27,6 +28,7 @@
class:focused
class:error
class:center
class:readonly
class:default-height={defaultHeight}
class:selected-other={selectedUser != null}
on:focus
@ -121,7 +123,8 @@
.cell:hover {
cursor: default;
}
.cell.highlighted:not(.focused) {
.cell.highlighted:not(.focused),
.cell.focused.readonly {
--cell-background: var(--cell-background-hover);
}
.cell.selected:not(.focused) {

View File

@ -0,0 +1,127 @@
<script>
import { GutterWidth } from "../lib/constants"
import { getContext } from "svelte"
import { Checkbox, Icon } from "@budibase/bbui"
import GridCell from "./GridCell.svelte"
import { createEventDispatcher } from "svelte"
export let row
export let rowFocused = false
export let rowHovered = false
export let rowSelected = false
export let disableExpand = false
export let disableNumber = false
export let defaultHeight = false
export let disabled = false
const { config, dispatch, selectedRows } = getContext("grid")
const svelteDispatch = createEventDispatcher()
const select = () => {
svelteDispatch("select")
const id = row?._id
if (id) {
selectedRows.update(state => {
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
}
}
const expand = () => {
svelteDispatch("expand")
if (row) {
dispatch("edit-row", row)
}
}
</script>
<GridCell
width={GutterWidth}
highlighted={rowFocused || rowHovered}
selected={rowSelected}
{defaultHeight}
>
<div class="gutter">
{#if $$slots.default}
<slot />
{:else}
<div
on:click={select}
class="checkbox"
class:visible={$config.allowDeleteRows &&
(disableNumber || rowSelected || rowHovered || rowFocused)}
>
<Checkbox value={rowSelected} {disabled} />
</div>
{#if !disableNumber}
<div
class="number"
class:visible={!$config.allowDeleteRows ||
!(rowSelected || rowHovered || rowFocused)}
>
{row.__idx + 1}
</div>
{/if}
{/if}
{#if $config.allowExpandRows}
<div
class="expand"
class:visible={!disableExpand && (rowFocused || rowHovered)}
>
<Icon name="Maximize" hoverable size="S" on:click={expand} />
</div>
{/if}
</div>
</GridCell>
<style>
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr auto;
gap: var(--cell-spacing);
}
.checkbox,
.number {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
}
.checkbox :global(.spectrum-Checkbox) {
min-height: 0;
height: 20px;
}
.checkbox :global(.spectrum-Checkbox-box) {
margin: 3px 0 0 0;
}
.number {
color: var(--spectrum-global-color-gray-500);
}
.checkbox.visible,
.number.visible {
display: flex;
}
.expand {
opacity: 0;
margin-right: 4px;
}
.expand :global(.spectrum-Icon) {
pointer-events: none;
}
.expand.visible {
opacity: 1;
}
.expand.visible :global(.spectrum-Icon) {
pointer-events: all;
}
</style>

View File

@ -20,6 +20,7 @@
ui,
columns,
} = getContext("grid")
const bannedDisplayColumnTypes = [
"link",
"array",
@ -92,6 +93,16 @@
columns.actions.changePrimaryDisplay(column.name)
open = false
}
const hideColumn = () => {
columns.update(state => {
const index = state.findIndex(col => col.name === column.name)
state[index].visible = false
return state.slice()
})
columns.actions.saveChanges()
open = false
}
</script>
<div
@ -100,7 +111,7 @@
style="flex: 0 0 {column.width}px;"
bind:this={anchor}
class:disabled={$isReordering || $isResizing}
class:sorted={sortedBy}
class:sticky={idx === "sticky"}
>
<GridCell
on:mousedown={onMouseDown}
@ -128,11 +139,7 @@
/>
</div>
{/if}
<div
class="more"
on:mousedown|stopPropagation
on:click={() => (open = true)}
>
<div class="more" on:click={() => (open = true)}>
<Icon
size="S"
name="MoreVertical"
@ -187,6 +194,7 @@
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
Move right
</MenuItem>
<MenuItem icon="VisibilityOff" on:click={hideColumn}>Hide column</MenuItem>
</Menu>
</Popover>
@ -194,6 +202,10 @@
.header-cell {
display: flex;
}
.header-cell:not(.sticky):hover,
.header-cell:not(.sticky) :global(.cell:hover) {
cursor: grab;
}
.header-cell.disabled {
pointer-events: none;
}
@ -202,9 +214,6 @@
gap: calc(2 * var(--cell-spacing));
background: var(--spectrum-global-color-gray-100);
}
.header-cell.sorted :global(.cell) {
background: var(--spectrum-global-color-gray-200);
}
.name {
flex: 1 1 auto;

View File

@ -31,8 +31,10 @@
isOpen = true
await tick()
textarea.focus()
if (value?.length > 100) {
textarea.setSelectionRange(0, 0)
}
}
const close = () => {
textarea?.blur()
@ -43,6 +45,7 @@
api = {
focus: () => open(),
blur: () => close(),
isActive: () => isOpen,
onKeyDown,
}
})

View File

@ -73,6 +73,7 @@
api = {
focus: open,
blur: close,
isActive: () => isOpen,
onKeyDown,
}
})

View File

@ -235,6 +235,7 @@
api = {
focus: open,
blur: close,
isActive: () => isOpen,
onKeyDown,
}
})

View File

@ -33,6 +33,7 @@
api = {
focus: () => input?.focus(),
blur: () => input?.blur(),
isActive: () => active,
onKeyDown,
}
})

View File

@ -12,5 +12,5 @@
on:click={() => dispatch("add-column")}
disabled={!$config.allowAddColumns}
>
Create column
Add column
</ActionButton>

View File

@ -9,10 +9,10 @@
icon="TableRowAddBottom"
quiet
size="M"
on:click={() => dispatch("add-row")}
on:click={() => dispatch("add-row-inline")}
disabled={!loaded ||
!$config.allowAddRows ||
(!$columns.length && !$stickyColumn)}
>
Create row
Add row
</ActionButton>

View File

@ -0,0 +1,91 @@
<script>
import { getContext } from "svelte"
import { ActionButton, Popover } from "@budibase/bbui"
import { DefaultColumnWidth } from "../lib/constants"
const { stickyColumn, columns } = getContext("grid")
const smallSize = 120
const mediumSize = DefaultColumnWidth
const largeSize = DefaultColumnWidth * 1.5
let open = false
let anchor
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
$: allSmall = allCols.every(col => col.width === smallSize)
$: allMedium = allCols.every(col => col.width === mediumSize)
$: allLarge = allCols.every(col => col.width === largeSize)
$: custom = !allSmall && !allMedium && !allLarge
$: sizeOptions = [
{
label: "Small",
size: smallSize,
selected: allSmall,
},
{
label: "Medium",
size: mediumSize,
selected: allMedium,
},
{
label: "Large",
size: largeSize,
selected: allLarge,
},
]
const changeColumnWidth = async width => {
columns.update(state => {
state.forEach(column => {
column.width = width
})
return state
})
if ($stickyColumn) {
stickyColumn.update(state => ({
...state,
width,
}))
}
await columns.actions.saveChanges()
}
</script>
<div bind:this={anchor}>
<ActionButton
icon="MoveLeftRight"
quiet
size="M"
on:click={() => (open = !open)}
selected={open}
disabled={!allCols.length}
>
Width
</ActionButton>
</div>
<Popover bind:open {anchor} align="left">
<div class="content">
{#each sizeOptions as option}
<ActionButton
quiet
on:click={() => changeColumnWidth(option.size)}
selected={option.selected}
>
{option.label}
</ActionButton>
{/each}
{#if custom}
<ActionButton selected={custom} quiet>Custom</ActionButton>
{/if}
</div>
</Popover>
<style>
.content {
padding: 12px;
display: flex;
align-items: center;
gap: 8px;
}
</style>

View File

@ -1,13 +1,8 @@
<script>
import {
Modal,
ModalContent,
ActionButton,
notifications,
} from "@budibase/bbui"
import { getContext } from "svelte"
import { Modal, ModalContent, Button, notifications } from "@budibase/bbui"
import { getContext, onMount } from "svelte"
const { selectedRows, rows, config } = getContext("grid")
const { selectedRows, rows, config, subscribe } = getContext("grid")
let modal
@ -28,18 +23,21 @@
await rows.actions.deleteRows(rowsToDelete)
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
}
onMount(() => subscribe("request-bulk-delete", () => modal?.show()))
</script>
{#if selectedRowCount}
<div class="delete-button" data-ignore-click-outside="true">
<ActionButton
<Button
icon="Delete"
size="S"
size="M"
on:click={modal.show}
disabled={!$config.allowEditRows}
cta
>
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
</ActionButton>
</Button>
</div>
{/if}
@ -57,16 +55,12 @@
</Modal>
<style>
.delete-button :global(.spectrum-ActionButton:not(:disabled) *) {
color: var(--spectrum-global-color-red-400);
}
.delete-button :global(.spectrum-ActionButton:not(:disabled)) {
.delete-button :global(.spectrum-Button:not(:disabled)) {
background-color: var(--spectrum-global-color-red-400);
border-color: var(--spectrum-global-color-red-400);
}
/*.delete-button.disabled :global(.spectrum-ActionButton *) {*/
/* color: var(--spectrum-global-color-gray-600);*/
/*}*/
/*.delete-button.disabled :global(.spectrum-ActionButton) {*/
/* border-color: var(--spectrum-global-color-gray-400);*/
/*}*/
.delete-button :global(.spectrum-Button:not(:disabled):hover) {
background-color: var(--spectrum-global-color-red-500);
border-color: var(--spectrum-global-color-red-500);
}
</style>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte"
import { ActionButton, Popover, Toggle } from "@budibase/bbui"
const { columns } = getContext("grid")
const { columns, stickyColumn } = getContext("grid")
let open = false
let anchor
@ -48,20 +48,24 @@
selected={open || anyHidden}
disabled={!$columns.length}
>
Hide columns
Columns
</ActionButton>
</div>
<Popover bind:open {anchor} align="left">
<div class="content">
<div class="columns">
{#if $stickyColumn}
<Toggle disabled size="S" value={true} />
<span>{$stickyColumn.label}</span>
{/if}
{#each $columns as column}
<Toggle
size="S"
value={column.visible}
on:change={e => toggleVisibility(column, e.detail)}
/>
<span>{column.name}</span>
<span>{column.label}</span>
{/each}
</div>
<div class="buttons">

View File

@ -36,13 +36,13 @@
<div bind:this={anchor}>
<ActionButton
icon="LineHeight"
icon="MoveUpDown"
quiet
size="M"
on:click={() => (open = !open)}
selected={open}
>
Row height
Height
</ActionButton>
</div>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte"
import { ActionButton, Popover, Select } from "@budibase/bbui"
const { sort, visibleColumns, stickyColumn } = getContext("grid")
const { sort, columns, stickyColumn } = getContext("grid")
const orderOptions = [
{ label: "A-Z", value: "ascending" },
{ label: "Z-A", value: "descending" },
@ -11,15 +11,24 @@
let open = false
let anchor
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns)
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns)
$: columnOptions = getColumnOptions($stickyColumn, $columns)
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
const getColumnOptions = (stickyColumn, columns) => {
let options = []
if (stickyColumn) {
options.push(stickyColumn.name)
options.push({
label: stickyColumn.label || stickyColumn.name,
value: stickyColumn.name,
})
}
return [...options, ...columns.map(col => col.name)]
return [
...options,
...columns.map(col => ({
label: col.label || col.name,
value: col.name,
})),
]
}
const updateSortColumn = e => {
@ -37,13 +46,13 @@
}
// Ensure we never have a sort column selected that is not visible
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => {
const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
if (!sortColumn) {
return
}
if (
sortColumn !== stickyColumn?.name &&
!visibleColumns.some(col => col.name === sortColumn)
!columns.some(col => col.name === sortColumn)
) {
if (stickyColumn) {
sort.update(state => ({
@ -53,7 +62,7 @@
} else {
sort.update(state => ({
...state,
column: visibleColumns[0]?.name,
column: columns[0]?.name,
}))
}
}
@ -66,7 +75,7 @@
quiet
size="M"
on:click={() => (open = !open)}
selected={open || $sort.column}
selected={open}
disabled={!columnOptions.length}
>
Sort

View File

@ -22,6 +22,8 @@
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
import AddRowButton from "../controls/AddRowButton.svelte"
import RowHeightButton from "../controls/RowHeightButton.svelte"
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
import NewRow from "./NewRow.svelte"
import {
MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
@ -110,6 +112,7 @@
<AddRowButton />
<AddColumnButton />
<slot name="controls" />
<ColumnWidthButton />
<RowHeightButton />
<HideColumnsButton />
<SortButton />
@ -127,10 +130,11 @@
<HeaderRow />
<GridBody />
</div>
<BetaButton />
<NewRow />
<div class="overlays">
<ResizeOverlay />
<ReorderOverlay />
<BetaButton />
<ScrollOverlay />
<MenuOverlay />
</div>

View File

@ -2,11 +2,25 @@
import { getContext, onMount } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import GridRow from "./GridRow.svelte"
import { BlankRowID } from "../lib/constants"
const { bounds, renderedRows, rowVerticalInversionIndex } = getContext("grid")
const {
bounds,
renderedRows,
renderedColumns,
rowVerticalInversionIndex,
config,
hoveredRowId,
dispatch,
} = getContext("grid")
let body
$: renderColumnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width),
0
)
onMount(() => {
// Observe and record the height of the body
const observer = new ResizeObserver(() => {
@ -24,6 +38,16 @@
{#each $renderedRows as row, idx}
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
{/each}
{#if $config.allowAddRows && $renderedColumns.length}
<div
class="blank"
class:highlighted={$hoveredRowId === BlankRowID}
style="width:{renderColumnsWidth}px"
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
on:mouseleave={() => ($hoveredRowId = null)}
on:click={() => dispatch("add-row-inline")}
/>
{/if}
</GridScrollWrapper>
</div>
@ -35,4 +59,15 @@
overflow: hidden;
flex: 1 1 auto;
}
.blank {
height: var(--row-height);
background: var(--cell-background);
border-bottom: var(--cell-border);
border-right: var(--cell-border);
position: absolute;
}
.blank.highlighted {
background: var(--cell-background-hover);
cursor: pointer;
}
</style>

View File

@ -29,10 +29,7 @@
// Handles a wheel even and updates the scroll offsets
const handleWheel = e => {
e.preventDefault()
const modifier = e.ctrlKey || e.metaKey
let x = modifier ? e.deltaY : e.deltaX
let y = modifier ? e.deltaX : e.deltaY
debouncedHandleWheel(x, y, e.clientY)
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
}
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
const { top, left } = $scroll

View File

@ -2,8 +2,17 @@
import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte"
import { Icon } from "@budibase/bbui"
const { renderedColumns } = getContext("grid")
const { renderedColumns, dispatch, scroll, hiddenColumnsWidth, width } =
getContext("grid")
$: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width),
0
)
$: end = $hiddenColumnsWidth + columnsWidth - 1 - $scroll.left
$: left = Math.min($width - 40, end)
</script>
<div class="header">
@ -14,6 +23,13 @@
{/each}
</div>
</GridScrollWrapper>
<div
class="add"
style="left:{left}px"
on:click={() => dispatch("add-column")}
>
<Icon name="Add" />
</div>
</div>
<style>
@ -27,4 +43,20 @@
.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(--spectrum-global-color-gray-100);
}
.add:hover {
background: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
</style>

View File

@ -0,0 +1,265 @@
<script>
import { getContext, onDestroy, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
import { fade } from "svelte/transition"
import { GutterWidth } from "../lib/constants"
import { NewRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte"
const {
hoveredRowId,
focusedCellId,
stickyColumn,
scroll,
dispatch,
rows,
focusedCellAPI,
tableId,
subscribe,
renderedRows,
renderedColumns,
rowHeight,
hasNextPage,
maxScrollTop,
rowVerticalInversionIndex,
columnHorizontalInversionIndex,
} = getContext("grid")
let isAdding = false
let newRow = {}
let offset = 0
$: firstColumn = $stickyColumn || $renderedColumns[0]
$: width = GutterWidth + ($stickyColumn?.width || 0)
$: $tableId, (isAdding = false)
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
const shouldInvertY = (offset, inversionIndex, rows) => {
if (offset === 0) {
return false
}
return rows.length >= inversionIndex
}
const addRow = async () => {
// Blur the active cell and tick to let final value updates propagate
$focusedCellAPI?.blur()
await tick()
// Create row
const newRowIndex = offset ? undefined : 0
const savedRow = await rows.actions.addRow(newRow, newRowIndex)
if (savedRow) {
// Reset state
clear()
// Select the first cell if possible
if (firstColumn) {
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
}
}
}
const clear = () => {
isAdding = false
$focusedCellId = null
$hoveredRowId = null
document.removeEventListener("keydown", handleKeyPress)
}
const startAdding = async () => {
if (isAdding) {
return
}
// If we have a next page of data then we aren't truly at the bottom, so we
// render the add row component at the top
if ($hasNextPage) {
offset = 0
}
// If we don't have a next page then we're at the bottom and can scroll to
// the max available offset
else {
scroll.update(state => ({
...state,
top: $maxScrollTop,
}))
offset = $renderedRows.length * $rowHeight - ($maxScrollTop % $rowHeight)
if ($renderedRows.length !== 0) {
offset -= 1
}
}
// Update state and select initial cell
newRow = {}
isAdding = true
$hoveredRowId = NewRowID
if (firstColumn) {
$focusedCellId = `${NewRowID}-${firstColumn.name}`
}
// Attach key listener
document.addEventListener("keydown", handleKeyPress)
}
const updateValue = (rowId, columnName, val) => {
newRow[columnName] = val
}
const addViaModal = () => {
clear()
dispatch("add-row")
}
const handleKeyPress = e => {
if (!isAdding) {
return
}
if (e.key === "Escape") {
// Only close the new row component if we aren't actively inside a cell
if (!$focusedCellAPI?.isActive()) {
e.preventDefault()
clear()
}
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
addRow()
}
}
onMount(() => subscribe("add-row-inline", startAdding))
onDestroy(() => {
document.removeEventListener("keydown", handleKeyPress)
})
</script>
<!-- Only show new row functionality if we have any columns -->
{#if isAdding}
<div
class="container"
class:floating={offset > 0}
style="--offset:{offset}px; --sticky-width:{width}px;"
>
<div class="underlay sticky" transition:fade={{ duration: 130 }} />
<div class="underlay" transition:fade={{ duration: 130 }} />
<div class="sticky-column" transition:fade={{ duration: 130 }}>
<GutterCell on:expand={addViaModal} rowHovered>
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
</GutterCell>
{#if $stickyColumn}
{@const cellId = `${NewRowID}-${$stickyColumn.name}`}
<DataCell
{cellId}
rowFocused
column={$stickyColumn}
row={newRow}
focused={$focusedCellId === cellId}
width={$stickyColumn.width}
{updateValue}
rowIdx={0}
{invertY}
/>
{/if}
</div>
<div class="normal-columns" transition:fade={{ duration: 130 }}>
<GridScrollWrapper scrollHorizontally wheelInteractive>
<div class="row">
{#each $renderedColumns as column, columnIdx}
{@const cellId = `new-${column.name}`}
{#key cellId}
<DataCell
{cellId}
{column}
{updateValue}
rowFocused
row={newRow}
focused={$focusedCellId === cellId}
width={column.width}
rowIdx={0}
invertX={columnIdx >= $columnHorizontalInversionIndex}
{invertY}
/>
{/key}
{/each}
</div>
</GridScrollWrapper>
</div>
<div class="buttons" transition:fade={{ duration: 130 }}>
<Button size="M" cta on:click={addRow}>Save</Button>
<Button size="M" secondary newStyles on:click={clear}>Cancel</Button>
</div>
</div>
{/if}
<style>
.container {
position: absolute;
top: var(--default-row-height);
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
align-items: stretch;
}
.container :global(.cell) {
--cell-background: var(--spectrum-global-color-gray-75) !important;
}
.container.floating :global(.cell) {
height: calc(var(--row-height) + 1px);
border-top: var(--cell-border);
}
/* Underlay sits behind everything */
.underlay {
position: absolute;
content: "";
left: 0;
top: 0;
height: 100%;
width: 100%;
background: var(--cell-background);
opacity: 0.8;
}
.underlay.sticky {
z-index: 2;
width: var(--sticky-width);
}
/* Floating buttons which sit on top of the underlay but below the sticky column */
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
pointer-events: all;
z-index: 3;
position: absolute;
top: calc(var(--row-height) + var(--offset) + 24px);
left: var(--gutter-width);
}
/* Sticky column styles */
.sticky-column {
display: flex;
z-index: 4;
position: relative;
align-self: flex-start;
flex: 0 0 var(--sticky-width);
}
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
}
.sticky-column,
.normal-columns {
margin-top: var(--offset);
}
/* Normal column styles */
.row {
width: 0;
display: flex;
}
</style>

View File

@ -1,247 +0,0 @@
<script>
import GridCell from "../cells/GridCell.svelte"
import { getContext, onMount } from "svelte"
import { Icon, Button } from "@budibase/bbui"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte"
import { fly } from "svelte/transition"
import { GutterWidth } from "../lib/constants"
const {
hoveredRowId,
focusedCellId,
stickyColumn,
scroll,
config,
dispatch,
visibleColumns,
rows,
showHScrollbar,
tableId,
subscribe,
scrollLeft,
} = getContext("grid")
let isAdding = false
let newRow = {}
let touched = false
$: firstColumn = $stickyColumn || $visibleColumns[0]
$: rowHovered = $hoveredRowId === "new"
$: rowFocused = $focusedCellId?.startsWith("new-")
$: width = GutterWidth + ($stickyColumn?.width || 0)
$: $tableId, (isAdding = false)
const addRow = async () => {
// Create row
const savedRow = await rows.actions.addRow(newRow, 0)
if (savedRow) {
// Select the first cell if possible
if (firstColumn) {
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
}
// Reset state
isAdding = false
scroll.set({
left: 0,
top: 0,
})
}
}
const cancel = () => {
isAdding = false
}
const startAdding = () => {
newRow = {}
isAdding = true
if (firstColumn) {
$focusedCellId = `new-${firstColumn.name}`
}
}
const updateValue = (rowId, columnName, val) => {
touched = true
newRow[columnName] = val
}
const addViaModal = () => {
isAdding = false
dispatch("add-row")
}
onMount(() => subscribe("add-row-inline", startAdding))
</script>
<!-- Only show new row functionality if we have any columns -->
{#if isAdding}
<div class="container" transition:fly={{ y: 20, duration: 130 }}>
<div class="content" class:above-scrollbar={$showHScrollbar}>
<div
class="new-row"
on:mouseenter={() => ($hoveredRowId = "new")}
on:mouseleave={() => ($hoveredRowId = null)}
>
<div
class="sticky-column"
style="flex: 0 0 {width}px"
class:scrolled={$scrollLeft > 0}
>
<GridCell width={GutterWidth} {rowHovered} {rowFocused}>
<div class="gutter">
<div class="number">1</div>
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</GridCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
focused={$focusedCellId === cellId}
{rowFocused}
width={$stickyColumn.width}
{updateValue}
rowIdx={0}
/>
{/if}
</div>
<GridScrollWrapper scrollHorizontally wheelInteractive>
<div class="row">
{#each $visibleColumns as column}
{@const cellId = `new-${column.name}`}
{#key cellId}
<DataCell
{cellId}
{column}
row={newRow}
{rowHovered}
focused={$focusedCellId === cellId}
{rowFocused}
width={column.width}
{updateValue}
rowIdx={0}
/>
{/key}
{/each}
</div>
</GridScrollWrapper>
</div>
</div>
<div class="buttons">
<Button size="M" cta on:click={addRow}>Save</Button>
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
</div>
</div>
{/if}
<style>
.container {
pointer-events: none;
position: absolute;
top: var(--row-height);
left: 0;
width: 100%;
padding-bottom: 800px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.container:before {
position: absolute;
content: "";
left: 0;
top: 0;
height: 100%;
width: 100%;
background: var(--cell-background);
opacity: 0.8;
z-index: -1;
}
.content {
pointer-events: all;
background: var(--background);
border-bottom: var(--cell-border);
}
.new-row {
display: flex;
bottom: 0;
left: 0;
width: 100%;
transition: margin-bottom 130ms ease-out;
}
.new-row :global(.cell) {
--cell-background: var(--background) !important;
border-bottom: none;
}
.sticky-column {
display: flex;
z-index: 1;
position: relative;
}
/* Don't show borders between cells in the sticky column */
.sticky-column :global(.cell:not(:last-child)) {
border-right: none;
}
.row {
width: 0;
display: flex;
}
/* Add shadow when scrolled */
.sticky-column.scrolled {
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
}
.sticky-column.scrolled:after {
content: "";
width: 10px;
height: 100%;
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
left: 100%;
top: 0;
position: absolute;
}
/* Styles for gutter */
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr auto;
gap: var(--cell-spacing);
}
/* Floating buttons */
.buttons {
display: flex;
flex-direction: row;
gap: 8px;
margin: 24px 0 0 var(--gutter-width);
pointer-events: all;
align-self: flex-start;
}
.number {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--spectrum-global-color-gray-500);
}
</style>

View File

@ -1,24 +1,26 @@
<script>
import { getContext } from "svelte"
import { Checkbox, Icon } from "@budibase/bbui"
import { Icon } from "@budibase/bbui"
import GridCell from "../cells/GridCell.svelte"
import DataCell from "../cells/DataCell.svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte"
import { GutterWidth } from "../lib/constants"
import { GutterWidth, BlankRowID } from "../lib/constants"
import GutterCell from "../cells/GutterCell.svelte"
const {
rows,
selectedRows,
stickyColumn,
renderedColumns,
renderedRows,
focusedCellId,
hoveredRowId,
config,
selectedCellMap,
focusedRow,
dispatch,
scrollLeft,
dispatch,
} = getContext("grid")
$: rowCount = $rows.length
@ -37,19 +39,6 @@
$selectedRows = allRows
}
}
const selectRow = id => {
selectedRows.update(state => {
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
}
</script>
<div
@ -58,26 +47,14 @@
class:scrolled={$scrollLeft > 0}
>
<div class="header row">
<GridCell width={GutterWidth} defaultHeight center>
<div class="gutter">
<div class="checkbox visible">
{#if $config.allowDeleteRows}
<div on:click={selectAll}>
<Checkbox
value={rowCount && selectedRowCount === rowCount}
<GutterCell
disableExpand
disableNumber
on:select={selectAll}
defaultHeight
rowSelected={selectedRowCount && selectedRowCount === rowCount}
disabled={!$renderedRows.length}
/>
</div>
{/if}
</div>
{#if $config.allowExpandRows}
<div class="expand">
<Icon name="Maximize" size="S" />
</div>
{/if}
</div>
</GridCell>
{#if $stickyColumn}
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
{/if}
@ -95,41 +72,7 @@
on:mouseenter={() => ($hoveredRowId = row._id)}
on:mouseleave={() => ($hoveredRowId = null)}
>
<GridCell
width={GutterWidth}
highlighted={rowFocused || rowHovered}
selected={rowSelected}
>
<div class="gutter">
<div
on:click={() => selectRow(row._id)}
class="checkbox"
class:visible={$config.allowDeleteRows &&
(rowSelected || rowHovered || rowFocused)}
>
<Checkbox value={rowSelected} />
</div>
<div
class="number"
class:visible={!$config.allowDeleteRows ||
!(rowSelected || rowHovered || rowFocused)}
>
{row.__idx + 1}
</div>
{#if $config.allowExpandRows}
<div class="expand" class:visible={rowFocused || rowHovered}>
<Icon
name="Maximize"
hoverable
size="S"
on:click={() => {
dispatch("edit-row", row)
}}
/>
</div>
{/if}
</div>
</GridCell>
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
{#if $stickyColumn}
<DataCell
{row}
@ -146,6 +89,24 @@
{/if}
</div>
{/each}
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
<div
class="row new"
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
on:mouseleave={() => ($hoveredRowId = null)}
on:click={() => dispatch("add-row-inline")}
>
<GutterCell disableExpand rowHovered={$hoveredRowId === BlankRowID}>
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
</GutterCell>
{#if $stickyColumn}
<GridCell
width={$stickyColumn.width}
highlighted={$hoveredRowId === BlankRowID}
/>
{/if}
</div>
{/if}
</GridScrollWrapper>
</div>
</div>
@ -156,6 +117,7 @@
flex-direction: column;
position: relative;
z-index: 2;
background: var(--cell-background);
}
/* Add right border */
@ -203,43 +165,7 @@
position: relative;
flex: 1 1 auto;
}
/* Styles for gutter */
.gutter {
flex: 1 1 auto;
display: grid;
align-items: center;
padding: var(--cell-padding);
grid-template-columns: 1fr auto;
gap: var(--cell-spacing);
}
.checkbox,
.number {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
}
.checkbox :global(.spectrum-Checkbox) {
min-height: 0;
height: 20px;
}
.checkbox :global(.spectrum-Checkbox-box) {
margin: 3px 0 0 0;
}
.number {
color: var(--spectrum-global-color-gray-500);
}
.checkbox.visible,
.number.visible {
display: flex;
}
.expand {
opacity: 0;
pointer-events: none;
}
.expand.visible {
opacity: 1;
pointer-events: all;
.row.new :global(*:hover) {
cursor: pointer;
}
</style>

View File

@ -1,11 +1,14 @@
export const Padding = 276
export const Padding = 128
export const MaxCellRenderHeight = 252
export const MaxCellRenderWidthOverflow = 200
export const ScrollBarSize = 8
export const GutterWidth = 72
export const DefaultColumnWidth = 200
export const MinColumnWidth = 100
export const MinColumnWidth = 80
export const SmallRowHeight = 36
export const MediumRowHeight = 64
export const LargeRowHeight = 92
export const DefaultRowHeight = SmallRowHeight
export const NewRowID = "new"
export const BlankRowID = "blank"
export const RowPageSize = 100

View File

@ -15,7 +15,7 @@ const TypeIconMap = {
number: "123",
boolean: "Boolean",
attachment: "AppleFiles",
link: "Link",
link: "DataCorrelated",
formula: "Calculator",
json: "Brackets",
}

View File

@ -1,6 +1,7 @@
<script>
import { getContext, onMount } from "svelte"
import { debounce } from "../../../utils/utils"
import { NewRowID } from "../lib/constants"
const {
enrichedRows,
@ -10,15 +11,24 @@
stickyColumn,
focusedCellAPI,
clipboard,
dispatch,
selectedRows,
} = getContext("grid")
// Global key listener which intercepts all key events
const handleKeyDown = e => {
// If nothing selected avoid processing further key presses
if (!$focusedCellId) {
if (e.key === "Tab") {
if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
e.preventDefault()
focusFirstCell()
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
dispatch("add-row-inline")
} else if (e.key === "Delete" || e.key === "Backspace") {
if (Object.keys($selectedRows).length) {
dispatch("request-bulk-delete")
}
}
return
}
@ -26,10 +36,20 @@
// Always intercept certain key presses
const api = $focusedCellAPI
if (e.key === "Escape") {
api?.blur?.()
// By setting a tiny timeout here we can ensure that other listeners
// which depend on being able to read cell state on an escape keypress
// get a chance to observe the true state before we blur
if (api?.isActive()) {
setTimeout(api?.blur, 10)
} else {
$focusedCellId = null
}
return
} else if (e.key === "Tab") {
e.preventDefault()
api?.blur?.()
changeFocusedColumn(1)
return
}
// Pass the key event to the selected cell and let it decide whether to
@ -54,8 +74,12 @@
clipboard.actions.copy()
break
case "v":
if (!api?.isReadonly()) {
clipboard.actions.paste()
}
break
case "Enter":
dispatch("add-row-inline")
}
} else {
switch (e.key) {
@ -73,11 +97,19 @@
break
case "Delete":
case "Backspace":
if (Object.keys($selectedRows).length) {
dispatch("request-bulk-delete")
} else {
deleteSelectedCell()
}
break
case "Enter":
focusCell()
break
case " ":
case "Space":
toggleSelectRow()
break
default:
startEnteringValue(e.key, e.which)
}
@ -156,7 +188,7 @@
// Focuses the cell and starts entering a new value
const startEnteringValue = (key, keyCode) => {
if ($focusedCellAPI) {
if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
const type = $focusedCellAPI.getType()
if (type === "number" && keyCodeIsNumber(keyCode)) {
$focusedCellAPI.setValue(parseInt(key))
@ -171,6 +203,17 @@
}
}
const toggleSelectRow = () => {
const id = $focusedRow?._id
if (!id || id === NewRowID) {
return
}
selectedRows.update(state => {
state[id] = !state[id]
return state
})
}
onMount(() => {
document.addEventListener("keydown", handleKeyDown)
return () => {

View File

@ -13,6 +13,7 @@
copiedCell,
clipboard,
dispatch,
focusedCellAPI,
} = getContext("grid")
$: style = makeStyle($menu)
@ -49,7 +50,7 @@
</MenuItem>
<MenuItem
icon="Paste"
disabled={$copiedCell == null}
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
on:click={clipboard.actions.paste}
on:click={menu.actions.close}
>

View File

@ -1,6 +1,7 @@
import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui"
import { NewRowID, RowPageSize } from "../lib/constants"
const initialSortState = {
column: null,
@ -16,6 +17,7 @@ export const createStores = () => {
const sort = writable(initialSortState)
const rowChangeCache = writable({})
const inProgressChanges = writable({})
const hasNextPage = writable(false)
// Generate a lookup map to quick find a row by ID
const rowLookupMap = derived(
@ -50,6 +52,7 @@ export const createStores = () => {
sort,
rowChangeCache,
inProgressChanges,
hasNextPage,
}
}
@ -70,6 +73,7 @@ export const deriveStores = context => {
rowChangeCache,
inProgressChanges,
previousFocusedRowId,
hasNextPage,
} = context
const instanceLoaded = writable(false)
const fetch = writable(null)
@ -114,7 +118,7 @@ export const deriveStores = context => {
filter: [],
sortColumn: initialSortState.column,
sortOrder: initialSortState.order,
limit: 100,
limit: RowPageSize,
paginate: true,
},
})
@ -122,6 +126,7 @@ export const deriveStores = context => {
// Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe($fetch => {
if ($fetch.loaded && !$fetch.loading) {
hasNextPage.set($fetch.hasNextPage)
const $instanceLoaded = get(instanceLoaded)
const resetRows = $fetch.resetKey !== lastResetKey
lastResetKey = $fetch.resetKey
@ -230,7 +235,7 @@ export const deriveStores = context => {
if (bubble) {
throw error
} else {
handleValidationError("new", error)
handleValidationError(NewRowID, error)
}
}
}

View File

@ -1,6 +1,6 @@
import { writable, derived, get } from "svelte/store"
import { tick } from "svelte"
import { Padding, GutterWidth, DefaultRowHeight } from "../lib/constants"
import { Padding, GutterWidth } from "../lib/constants"
export const createStores = () => {
const scroll = writable({
@ -29,7 +29,7 @@ export const deriveStores = context => {
// Derive vertical limits
const contentHeight = derived(
[rows, rowHeight],
([$rows, $rowHeight]) => $rows.length * $rowHeight + Padding,
([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
0
)
const maxScrollTop = derived(
@ -138,7 +138,7 @@ export const initialise = context => {
const $scroll = get(scroll)
const $bounds = get(bounds)
const $rowHeight = get(rowHeight)
const verticalOffset = DefaultRowHeight * 1.5
const verticalOffset = 60
// Ensure vertical position is viewable
if ($focusedRow) {

View File

@ -1,8 +1,10 @@
import { writable, get, derived } from "svelte/store"
import { tick } from "svelte"
import {
DefaultRowHeight,
LargeRowHeight,
MediumRowHeight,
NewRowID,
} from "../lib/constants"
export const createStores = () => {
@ -49,14 +51,14 @@ export const deriveStores = context => {
([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
const rowId = $focusedCellId?.split("-")[0]
if (rowId === "new") {
// Edge case for new row
return { _id: rowId }
} else {
// Edge case for new rows
if (rowId === NewRowID) {
return { _id: NewRowID }
}
// All normal rows
const index = $rowLookupMap[rowId]
return $enrichedRows[index]
}
},
null
)
@ -101,7 +103,10 @@ export const initialise = context => {
} = context
// Ensure we clear invalid rows from state if they disappear
rows.subscribe(() => {
rows.subscribe(async () => {
// We tick here to ensure other derived stores have properly updated.
// We depend on the row lookup map which is a derived store,
await tick()
const $focusedCellId = get(focusedCellId)
const $selectedRows = get(selectedRows)
const $hoveredRowId = get(hoveredRowId)
@ -140,20 +145,6 @@ export const initialise = context => {
lastFocusedRowId = id
})
// Reset selected rows when selected cell changes
focusedCellId.subscribe(id => {
if (id) {
selectedRows.set({})
}
})
// Unset selected cell when rows are selected
selectedRows.subscribe(rows => {
if (Object.keys(rows || {}).length) {
focusedCellId.set(null)
}
})
// Remove hovered row when a cell is selected
focusedCellId.subscribe(cell => {
if (cell && get(hoveredRowId)) {

View File

@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
import {
MaxCellRenderHeight,
MaxCellRenderWidthOverflow,
MinColumnWidth,
ScrollBarSize,
} from "../lib/constants"
@ -45,11 +46,16 @@ export const deriveStores = context => {
)
// Derive visible columns
const scrollLeftRounded = derived(scrollLeft, $scrollLeft => {
const interval = MinColumnWidth
return Math.round($scrollLeft / interval) * interval
})
const renderedColumns = derived(
[visibleColumns, scrollLeft, width],
([$visibleColumns, $scrollLeft, $width]) => {
[visibleColumns, scrollLeftRounded, width],
([$visibleColumns, $scrollLeft, $width], set) => {
if (!$visibleColumns.length) {
return []
set([])
return
}
let startColIdx = 0
let rightEdge = $visibleColumns[0].width
@ -69,19 +75,17 @@ export const deriveStores = context => {
leftEdge += $visibleColumns[endColIdx].width
endColIdx++
}
const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx)
// Cautiously shrink the number of rendered columns.
// This is to avoid rapidly shrinking and growing the visible column count
// which results in remounting cells
const currentCount = get(renderedColumns).length
if (currentCount === nextRenderedColumns.length + 1) {
return $visibleColumns.slice(startColIdx, endColIdx + 1)
} else {
return nextRenderedColumns
// Render an additional column on either side to account for
// debounce column updates based on scroll position
const next = $visibleColumns.slice(
Math.max(0, startColIdx - 1),
endColIdx + 1
)
const current = get(renderedColumns)
if (JSON.stringify(next) !== JSON.stringify(current)) {
set(next)
}
}
},
[]
)
const hiddenColumnsWidth = derived(

View File

@ -1,5 +1,4 @@
import env from "../../environment"
import packageJson from "../../../package.json"
import {
createLinkView,
createRoutingView,
@ -24,6 +23,7 @@ import {
migrations,
objectStore,
ErrorCode,
env as envCore,
} from "@budibase/backend-core"
import { USERS_TABLE_SCHEMA } from "../../constants"
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
@ -264,7 +264,7 @@ async function performAppCreate(ctx: UserCtx) {
_rev: undefined,
appId,
type: "app",
version: packageJson.version,
version: envCore.VERSION,
componentLibraries: ["@budibase/standard-components"],
name: name,
url: url,
@ -433,7 +433,7 @@ export async function updateClient(ctx: UserCtx) {
}
// Update versions in app package
const updatedToVersion = packageJson.version
const updatedToVersion = envCore.VERSION
const appPackageUpdates = {
version: updatedToVersion,
revertableVersion: currentVersion,

View File

@ -4,7 +4,7 @@ import { checkSlashesInUrl } from "../../utilities"
import { request } from "../../utilities/workerRequests"
import { clearLock as redisClearLock } from "../../utilities/redis"
import { DocumentType } from "../../db/utils"
import { context } from "@budibase/backend-core"
import { context, env as envCore } from "@budibase/backend-core"
import { events, db as dbCore, cache } from "@budibase/backend-core"
async function redirect(ctx: any, method: string, path: string = "global") {
@ -121,7 +121,7 @@ export async function revert(ctx: any) {
}
export async function getBudibaseVersion(ctx: any) {
const version = require("../../../package.json").version
const version = envCore.VERSION
ctx.body = {
version,
}

View File

@ -1,31 +1,32 @@
import {
Datasource,
FieldSchema,
FieldType,
FilterType,
IncludeRelationship,
Operation,
PaginationJson,
RelationshipsJson,
RelationshipTypes,
Row,
SearchFilters,
SortJson,
Datasource,
FieldSchema,
Row,
Table,
RelationshipTypes,
FieldType,
SortType,
Table,
} from "@budibase/types"
import {
breakExternalTableId,
breakRowIdField,
convertRowId,
generateRowIdField,
isRowId,
convertRowId,
isSQL,
} from "../../../integrations/utils"
import { getDatasourceAndQuery } from "./utils"
import { FieldTypes } from "../../../constants"
import { breakExternalTableId, isSQL } from "../../../integrations/utils"
import { processObjectSync } from "@budibase/string-templates"
import { cloneDeep } from "lodash/fp"
import { processFormulas, processDates } from "../../../utilities/rowProcessor"
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
import { db as dbCore } from "@budibase/backend-core"
import sdk from "../../../sdk"
@ -382,10 +383,18 @@ export class ExternalRequest {
}
const display = linkedTable.primaryDisplay
for (let key of Object.keys(row[relationship.column])) {
const related: Row = row[relationship.column][key]
let relatedRow: Row = row[relationship.column][key]
// add this row as context for the relationship
for (let col of Object.values(linkedTable.schema)) {
if (col.type === FieldType.LINK && col.tableId === table._id) {
relatedRow[col.name] = [row]
}
}
relatedRow = processFormulas(linkedTable, relatedRow)
const relatedDisplay = display ? relatedRow[display] : undefined
row[relationship.column][key] = {
primaryDisplay: display ? related[display] : undefined,
_id: related._id,
primaryDisplay: relatedDisplay || "Invalid display column",
_id: relatedRow._id,
}
}
}

View File

@ -1,9 +1,8 @@
import Router from "@koa/router"
import { auth, middleware } from "@budibase/backend-core"
import { auth, middleware, env as envCore } from "@budibase/backend-core"
import currentApp from "../middleware/currentapp"
import zlib from "zlib"
import { mainRoutes, staticRoutes, publicRoutes } from "./routes"
import pkg from "../../package.json"
import { middleware as pro } from "@budibase/pro"
export { shutdown } from "./routes/public"
const compress = require("koa-compress")
@ -11,7 +10,7 @@ const compress = require("koa-compress")
export const router: Router = new Router()
router.get("/health", ctx => (ctx.status = 200))
router.get("/version", ctx => (ctx.body = pkg.version))
router.get("/version", ctx => (ctx.body = envCore.VERSION))
router.use(middleware.errorHandling)

View File

@ -921,6 +921,7 @@ describe("row api - postgres", () => {
[m2mFieldName]: [
{
_id: row._id,
primaryDisplay: "Invalid display column",
},
],
})
@ -929,6 +930,7 @@ describe("row api - postgres", () => {
[m2mFieldName]: [
{
_id: row._id,
primaryDisplay: "Invalid display column",
},
],
})

View File

@ -79,10 +79,17 @@ function generateSchema(
if (!relatedTable) {
throw "Referenced table doesn't exist"
}
const relatedPrimary = relatedTable.primary[0]
const externalType = relatedTable.schema[relatedPrimary].externalType
if (externalType) {
schema.specificType(column.foreignKey, externalType)
} else {
schema.integer(column.foreignKey).unsigned()
}
schema
.foreign(column.foreignKey)
.references(`${tableName}.${relatedTable.primary[0]}`)
.references(`${tableName}.${relatedPrimary}`)
}
break
}

View File

@ -1,11 +1,11 @@
import {
FieldTypes,
FormulaTypes,
AutoFieldDefaultNames,
AutoFieldSubTypes,
FieldTypes,
FormulaTypes,
} from "../../constants"
import { processStringSync } from "@budibase/string-templates"
import { FieldSchema, Table, Row } from "@budibase/types"
import { FieldSchema, FieldType, Row, Table } from "@budibase/types"
/**
* If the subtype has been lost for any reason this works out what
@ -50,6 +50,7 @@ export function processFormulas(
const isStatic = schema.formulaType === FormulaTypes.STATIC
if (
schema.type !== FieldTypes.FORMULA ||
schema.formula == null ||
(dynamic && isStatic) ||
(!dynamic && !isStatic)
) {
@ -57,7 +58,6 @@ export function processFormulas(
}
// iterate through rows and process formula
for (let i = 0; i < rowArray.length; i++) {
if (schema.formula) {
let row = rowArray[i]
let context = contextRows ? contextRows[i] : row
rowArray[i] = {
@ -66,7 +66,6 @@ export function processFormulas(
}
}
}
}
return single ? rowArray[0] : rowArray
}

View File

@ -8,13 +8,11 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"types": [ "node", "jest" ],
"outDir": "dist",
"types": ["node", "jest"],
"outDir": "dist/src",
"skipLibCheck": true
},
"include": [
"src/**/*"
],
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",

View File

@ -5,6 +5,7 @@
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"outDir": "dist",
"paths": {
"@budibase/types": ["../types/src"],
"@budibase/backend-core": ["../backend-core/src"],
@ -17,6 +18,12 @@
"require": ["tsconfig-paths/register"],
"swc": true
},
"include": ["src/**/*", "specs", "package.json"],
"references": [
{ "path": "../types" },
{ "path": "../backend-core" },
{ "path": "../shared-core" },
{ "path": "../../../budibase-pro/packages/pro" }
],
"include": ["src/**/*", "specs"],
"exclude": ["node_modules", "dist"]
}

View File

@ -14,7 +14,7 @@
"outDir": "dist",
"skipLibCheck": true
},
"include": ["**/*.js", "**/*.ts", "package.json"],
"include": ["**/*.js", "**/*.ts"],
"exclude": [
"node_modules",
"dist",

View File

@ -2,7 +2,7 @@
"extends": "./tsconfig-base.build.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist/cjs",
"outDir": "dist/cjs/src",
"target": "es2015"
}
}

View File

@ -2,7 +2,7 @@
"extends": "./tsconfig-base.build.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/mjs",
"outDir": "dist/mjs/src",
"target": "esnext"
}
}

View File

@ -16,6 +16,11 @@
"require": ["tsconfig-paths/register"],
"swc": true
},
"include": ["src/**/*", "package.json"],
"references": [
{ "path": "../types" },
{ "path": "../backend-core" },
{ "path": "../../../budibase-pro/packages/pro" }
],
"include": ["src/**/*"],
"exclude": ["dist"]
}

View File

@ -22,6 +22,10 @@
"ts-node": {
"require": ["tsconfig-paths/register"]
},
"include": ["src/**/*", "package.json"],
"references": [
{ "path": "../packages/types" },
{ "path": "../packages/backend-core" }
],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}