Merge branch 'chore/npmless-builds' into chore/pipeline_npm_version_updates
This commit is contained in:
commit
9ffd43b682
|
@ -79,7 +79,6 @@ jobs:
|
||||||
- name: Build/release Docker images
|
- name: Build/release Docker images
|
||||||
run: |
|
run: |
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
|
||||||
yarn build
|
|
||||||
yarn build:docker
|
yarn build:docker
|
||||||
env:
|
env:
|
||||||
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USER: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "0.0.999-alpha.38",
|
"version": "2.5.6-alpha.28",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/backend-core",
|
"packages/backend-core",
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
"dev": "yarn run kill-all && lerna link && lerna run --stream --parallel dev:builder --concurrency 1 --stream",
|
"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: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: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",
|
"test": "lerna run --stream test --stream",
|
||||||
"lint:eslint": "eslint packages && eslint qa-core",
|
"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}\"",
|
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { existsSync, readFileSync } from "fs"
|
||||||
|
|
||||||
function isTest() {
|
function isTest() {
|
||||||
return isCypress() || isJest()
|
return isCypress() || isJest()
|
||||||
}
|
}
|
||||||
|
@ -45,6 +47,35 @@ function httpLogging() {
|
||||||
return process.env.HTTP_LOGGING
|
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 = {
|
const environment = {
|
||||||
isTest,
|
isTest,
|
||||||
isJest,
|
isJest,
|
||||||
|
@ -122,6 +153,7 @@ const environment = {
|
||||||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||||
: false,
|
: false,
|
||||||
|
VERSION: findVersion(),
|
||||||
_set(key: any, value: any) {
|
_set(key: any, value: any) {
|
||||||
process.env[key] = value
|
process.env[key] = value
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -23,8 +23,6 @@ import * as installation from "../installation"
|
||||||
import * as configs from "../configs"
|
import * as configs from "../configs"
|
||||||
import { withCache, TTL, CacheKey } from "../cache/generic"
|
import { withCache, TTL, CacheKey } from "../cache/generic"
|
||||||
|
|
||||||
const pkg = require("../../package.json")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An identity can be:
|
* An identity can be:
|
||||||
* - account user (Self host)
|
* - account user (Self host)
|
||||||
|
@ -102,7 +100,7 @@ const identifyInstallationGroup = async (
|
||||||
const id = installId
|
const id = installId
|
||||||
const type = IdentityType.INSTALLATION
|
const type = IdentityType.INSTALLATION
|
||||||
const hosting = getHostingFromEnv()
|
const hosting = getHostingFromEnv()
|
||||||
const version = pkg.version
|
const version = env.VERSION
|
||||||
const environment = getDeploymentEnvironment()
|
const environment = getDeploymentEnvironment()
|
||||||
|
|
||||||
const group: InstallationGroup = {
|
const group: InstallationGroup = {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { EventProcessor } from "../types"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
import * as context from "../../../context"
|
import * as context from "../../../context"
|
||||||
import * as rateLimiting from "./rateLimiting"
|
import * as rateLimiting from "./rateLimiting"
|
||||||
const pkg = require("../../../../package.json")
|
|
||||||
|
|
||||||
const EXCLUDED_EVENTS: Event[] = [
|
const EXCLUDED_EVENTS: Event[] = [
|
||||||
Event.USER_UPDATED,
|
Event.USER_UPDATED,
|
||||||
|
@ -49,7 +48,7 @@ export default class PosthogProcessor implements EventProcessor {
|
||||||
|
|
||||||
properties = this.clearPIIProperties(properties)
|
properties = this.clearPIIProperties(properties)
|
||||||
|
|
||||||
properties.version = pkg.version
|
properties.version = env.VERSION
|
||||||
properties.service = env.SERVICE
|
properties.service = env.SERVICE
|
||||||
properties.environment = identity.environment
|
properties.environment = identity.environment
|
||||||
properties.hosting = identity.hosting
|
properties.hosting = identity.hosting
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { Installation, IdentityType, Database } from "@budibase/types"
|
||||||
import * as context from "./context"
|
import * as context from "./context"
|
||||||
import semver from "semver"
|
import semver from "semver"
|
||||||
import { bustCache, withCache, TTL, CacheKey } from "./cache/generic"
|
import { bustCache, withCache, TTL, CacheKey } from "./cache/generic"
|
||||||
|
import environment from "./environment"
|
||||||
const pkg = require("../package.json")
|
|
||||||
|
|
||||||
export const getInstall = async (): Promise<Installation> => {
|
export const getInstall = async (): Promise<Installation> => {
|
||||||
return withCache(CacheKey.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, {
|
return withCache(CacheKey.INSTALLATION, TTL.ONE_DAY, getInstallFromDB, {
|
||||||
|
@ -18,7 +17,7 @@ async function createInstallDoc(platformDb: Database) {
|
||||||
const install: Installation = {
|
const install: Installation = {
|
||||||
_id: StaticDatabases.PLATFORM_INFO.docs.install,
|
_id: StaticDatabases.PLATFORM_INFO.docs.install,
|
||||||
installId: newid(),
|
installId: newid(),
|
||||||
version: pkg.version,
|
version: environment.VERSION,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await platformDb.put(install)
|
const resp = await platformDb.put(install)
|
||||||
|
@ -80,7 +79,7 @@ export const checkInstallVersion = async (): Promise<void> => {
|
||||||
const install = await getInstall()
|
const install = await getInstall()
|
||||||
|
|
||||||
const currentVersion = install.version
|
const currentVersion = install.version
|
||||||
const newVersion = pkg.version
|
const newVersion = environment.VERSION
|
||||||
|
|
||||||
if (currentVersion !== newVersion) {
|
if (currentVersion !== newVersion) {
|
||||||
const isUpgrade = semver.gt(newVersion, currentVersion)
|
const isUpgrade = semver.gt(newVersion, currentVersion)
|
||||||
|
|
|
@ -10,15 +10,11 @@
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"types": [ "node", "jest" ],
|
"types": ["node", "jest"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["**/*.js", "**/*.ts"],
|
||||||
"**/*.js",
|
|
||||||
"**/*.ts",
|
|
||||||
"package.json"
|
|
||||||
],
|
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -10,7 +10,10 @@ import { auth } from "./stores/portal"
|
||||||
export const API = createAPIClient({
|
export const API = createAPIClient({
|
||||||
attachHeaders: headers => {
|
attachHeaders: headers => {
|
||||||
// Attach app ID header from store
|
// 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
|
// Add csrf token if authenticated
|
||||||
const user = get(auth).user
|
const user = get(auth).user
|
||||||
|
|
|
@ -42,13 +42,14 @@
|
||||||
<GridCreateViewButton />
|
<GridCreateViewButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridManageAccessButton />
|
<GridManageAccessButton />
|
||||||
{#if isUsersTable}
|
|
||||||
<EditRolesButton />
|
|
||||||
{/if}
|
|
||||||
{#if !isInternal}
|
{#if !isInternal}
|
||||||
<GridRelationshipButton />
|
<GridRelationshipButton />
|
||||||
{/if}
|
{/if}
|
||||||
<GridImportButton disabled={isUsersTable} />
|
{#if isUsersTable}
|
||||||
|
<EditRolesButton />
|
||||||
|
{:else}
|
||||||
|
<GridImportButton />
|
||||||
|
{/if}
|
||||||
<GridExportButton />
|
<GridExportButton />
|
||||||
<GridFilterButton />
|
<GridFilterButton />
|
||||||
<GridAddColumnModal />
|
<GridAddColumnModal />
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
||||||
Manage access
|
Access
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ManageAccessModal
|
<ManageAccessModal
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||||
Create view
|
Add view
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<CreateViewModal />
|
<CreateViewModal />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import CreateEditRow from "../../modals/CreateEditRow.svelte"
|
import CreateEditRow from "../../modals/CreateEditRow.svelte"
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { Modal } from "@budibase/bbui"
|
import { Modal, notifications } from "@budibase/bbui"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
const { subscribe, rows } = getContext("grid")
|
const { subscribe, rows } = getContext("grid")
|
||||||
|
@ -9,6 +9,11 @@
|
||||||
let modal
|
let modal
|
||||||
let row
|
let row
|
||||||
|
|
||||||
|
const deleteRow = e => {
|
||||||
|
rows.actions.deleteRows([e.detail])
|
||||||
|
notifications.success("Deleted 1 row")
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() =>
|
onMount(() =>
|
||||||
subscribe("add-row", () => {
|
subscribe("add-row", () => {
|
||||||
row = {}
|
row = {}
|
||||||
|
@ -24,5 +29,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<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>
|
</Modal>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCom
|
||||||
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
import ColumnEditor from "./controls/ColumnEditor/ColumnEditor.svelte"
|
||||||
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
||||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||||
|
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
text: DrawerBindableCombobox,
|
text: DrawerBindableCombobox,
|
||||||
|
@ -43,6 +44,7 @@ const componentMap = {
|
||||||
section: SectionSelect,
|
section: SectionSelect,
|
||||||
filter: FilterEditor,
|
filter: FilterEditor,
|
||||||
url: URLSelect,
|
url: URLSelect,
|
||||||
|
fieldConfiguration: FieldConfiguration,
|
||||||
columns: ColumnEditor,
|
columns: ColumnEditor,
|
||||||
"columns/basic": BasicColumnEditor,
|
"columns/basic": BasicColumnEditor,
|
||||||
"field/sortable": SortableFieldSelect,
|
"field/sortable": SortableFieldSelect,
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -16,6 +16,7 @@
|
||||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||||
import { generate } from "shortid"
|
import { generate } from "shortid"
|
||||||
|
|
||||||
|
export let fieldName = null
|
||||||
export let rules = []
|
export let rules = []
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
export let type
|
export let type
|
||||||
|
@ -124,7 +125,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
|
$: dataSourceSchema = getDataSourceSchema($currentAsset, $selectedComponent)
|
||||||
$: field = $selectedComponent?.field
|
$: field = fieldName || $selectedComponent?.field
|
||||||
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
|
$: schemaRules = parseRulesFromSchema(field, dataSourceSchema || {})
|
||||||
$: fieldType = type?.split("/")[1] || "string"
|
$: fieldType = type?.split("/")[1] || "string"
|
||||||
$: constraintOptions = getConstraintsForType(fieldType)
|
$: constraintOptions = getConstraintsForType(fieldType)
|
||||||
|
@ -140,8 +141,12 @@
|
||||||
const formParent = findClosestMatchingComponent(
|
const formParent = findClosestMatchingComponent(
|
||||||
asset.props,
|
asset.props,
|
||||||
component._id,
|
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)
|
return getSchemaForDatasource(asset, formParent?.dataSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
import * as routify from "@roxi/routify"
|
import * as routify from "@roxi/routify"
|
||||||
import { onDestroy } from "svelte"
|
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({
|
const stopSyncing = syncURLToState({
|
||||||
urlParam: "appId",
|
urlParam: "appId",
|
||||||
stateKey: "selectedAppId",
|
stateKey: "selectedAppId",
|
||||||
|
@ -47,6 +47,7 @@
|
||||||
let deletionModal
|
let deletionModal
|
||||||
let exportPublishedVersion = false
|
let exportPublishedVersion = false
|
||||||
let deletionConfirmationAppName
|
let deletionConfirmationAppName
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
$: app = $overview.selectedApp
|
||||||
$: appId = $overview.selectedAppId
|
$: appId = $overview.selectedAppId
|
||||||
|
@ -56,10 +57,12 @@
|
||||||
$: lockedByYou = $auth.user.email === app?.lockedBy?.email
|
$: lockedByYou = $auth.user.email === app?.lockedBy?.email
|
||||||
|
|
||||||
const initialiseApp = async appId => {
|
const initialiseApp = async appId => {
|
||||||
|
loaded = false
|
||||||
try {
|
try {
|
||||||
const pkg = await API.fetchAppPackage(appId)
|
const pkg = await API.fetchAppPackage(appId)
|
||||||
await store.actions.initialise(pkg)
|
await store.actions.initialise(pkg)
|
||||||
await API.syncApp(appId)
|
await API.syncApp(appId)
|
||||||
|
loaded = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error initialising app overview")
|
notifications.error("Error initialising app overview")
|
||||||
$goto("../../")
|
$goto("../../")
|
||||||
|
@ -228,7 +231,9 @@
|
||||||
active={$isActive("./version")}
|
active={$isActive("./version")}
|
||||||
/>
|
/>
|
||||||
</SideNav>
|
</SideNav>
|
||||||
|
{#if loaded}
|
||||||
<slot />
|
<slot />
|
||||||
|
{/if}
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -3,17 +3,17 @@ import { logging } from "@budibase/backend-core"
|
||||||
logging.disableLogger()
|
logging.disableLogger()
|
||||||
import "./prebuilds"
|
import "./prebuilds"
|
||||||
import "./environment"
|
import "./environment"
|
||||||
|
import { env } from "@budibase/backend-core"
|
||||||
import { getCommands } from "./options"
|
import { getCommands } from "./options"
|
||||||
import { Command } from "commander"
|
import { Command } from "commander"
|
||||||
import { getHelpDescription } from "./utils"
|
import { getHelpDescription } from "./utils"
|
||||||
const json = require("../package.json")
|
|
||||||
|
|
||||||
// add hosting config
|
// add hosting config
|
||||||
async function init() {
|
async function init() {
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
.addHelpCommand("help", getHelpDescription("Help with Budibase commands."))
|
||||||
.helpOption(false)
|
.helpOption(false)
|
||||||
.version(json.version)
|
.version(env.VERSION)
|
||||||
// add commands
|
// add commands
|
||||||
for (let command of getCommands()) {
|
for (let command of getCommands()) {
|
||||||
command.configure(program)
|
command.configure(program)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"require": ["tsconfig-paths/register"],
|
"require": ["tsconfig-paths/register"],
|
||||||
"swc": true
|
"swc": true
|
||||||
},
|
},
|
||||||
|
"references": [{ "path": "../types" }, { "path": "../backend-core" }],
|
||||||
"include": ["src/**/*", "package.json"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4435,6 +4435,48 @@
|
||||||
"key": "row"
|
"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",
|
"name": "Fields",
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "multifield",
|
"type": "fieldConfiguration",
|
||||||
"label": "Fields",
|
"label": "Fields",
|
||||||
"key": "fields",
|
"key": "fields",
|
||||||
"selectAllFields": true
|
"selectAllFields": true
|
||||||
|
@ -5028,6 +5070,17 @@
|
||||||
"invert": true
|
"invert": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"key": "saveButtonLabel",
|
||||||
|
"label": "Save button label",
|
||||||
|
"nested": true,
|
||||||
|
"defaultValue": "Save",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "showSaveButton",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Allow delete",
|
"label": "Allow delete",
|
||||||
|
@ -5038,6 +5091,17 @@
|
||||||
"value": "Update"
|
"value": "Update"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"key": "deleteButtonLabel",
|
||||||
|
"label": "Delete button label",
|
||||||
|
"nested": true,
|
||||||
|
"defaultValue": "Delete",
|
||||||
|
"dependsOn": {
|
||||||
|
"setting": "showDeleteButton",
|
||||||
|
"value": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "url",
|
"type": "url",
|
||||||
"label": "Navigate after button press",
|
"label": "Navigate after button press",
|
||||||
|
|
|
@ -26,6 +26,10 @@
|
||||||
export let titleButtonClickBehaviour
|
export let titleButtonClickBehaviour
|
||||||
export let onClickTitleButton
|
export let onClickTitleButton
|
||||||
export let noRowsMessage
|
export let noRowsMessage
|
||||||
|
export let sidePanelFields
|
||||||
|
export let sidePanelShowDelete
|
||||||
|
export let sidePanelSaveLabel
|
||||||
|
export let sidePanelDeleteLabel
|
||||||
|
|
||||||
const { fetchDatasourceSchema, API } = getContext("sdk")
|
const { fetchDatasourceSchema, API } = getContext("sdk")
|
||||||
const stateKey = `ID_${generate()}`
|
const stateKey = `ID_${generate()}`
|
||||||
|
@ -241,10 +245,12 @@
|
||||||
props={{
|
props={{
|
||||||
dataSource,
|
dataSource,
|
||||||
showSaveButton: true,
|
showSaveButton: true,
|
||||||
showDeleteButton: true,
|
showDeleteButton: sidePanelShowDelete,
|
||||||
|
saveButtonLabel: sidePanelSaveLabel,
|
||||||
|
deleteButtonLabel: sidePanelDeleteLabel,
|
||||||
actionType: "Update",
|
actionType: "Update",
|
||||||
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
rowId: `{{ ${safe("state")}.${safe(stateKey)} }}`,
|
||||||
fields: normalFields,
|
fields: sidePanelFields || normalFields,
|
||||||
title: editTitle,
|
title: editTitle,
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
}}
|
}}
|
||||||
|
@ -266,8 +272,9 @@
|
||||||
dataSource,
|
dataSource,
|
||||||
showSaveButton: true,
|
showSaveButton: true,
|
||||||
showDeleteButton: false,
|
showDeleteButton: false,
|
||||||
|
saveButtonLabel: sidePanelSaveLabel,
|
||||||
actionType: "Create",
|
actionType: "Create",
|
||||||
fields: normalFields,
|
fields: sidePanelFields || normalFields,
|
||||||
title: "Create Row",
|
title: "Create Row",
|
||||||
labelPosition: "left",
|
labelPosition: "left",
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
export let fields
|
export let fields
|
||||||
export let labelPosition
|
export let labelPosition
|
||||||
export let title
|
export let title
|
||||||
|
export let saveButtonLabel
|
||||||
|
export let deleteButtonLabel
|
||||||
export let showSaveButton
|
export let showSaveButton
|
||||||
export let showDeleteButton
|
export let showDeleteButton
|
||||||
export let rowId
|
export let rowId
|
||||||
|
@ -20,10 +22,40 @@
|
||||||
|
|
||||||
const { fetchDatasourceSchema } = getContext("sdk")
|
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 schema
|
||||||
let providerId
|
let providerId
|
||||||
let repeaterId
|
let repeaterId
|
||||||
|
|
||||||
|
$: formattedFields = convertOldFieldFormat(fields)
|
||||||
|
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
|
||||||
|
|
||||||
$: fetchSchema(dataSource)
|
$: fetchSchema(dataSource)
|
||||||
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
$: dataProvider = `{{ literal ${safe(providerId)} }}`
|
||||||
$: filter = [
|
$: filter = [
|
||||||
|
@ -46,9 +78,11 @@
|
||||||
actionType,
|
actionType,
|
||||||
size,
|
size,
|
||||||
disabled,
|
disabled,
|
||||||
fields,
|
fields: fieldsOrDefault,
|
||||||
labelPosition,
|
labelPosition,
|
||||||
title,
|
title,
|
||||||
|
saveButtonLabel,
|
||||||
|
deleteButtonLabel,
|
||||||
showSaveButton,
|
showSaveButton,
|
||||||
showDeleteButton,
|
showDeleteButton,
|
||||||
schema,
|
schema,
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
export let fields
|
export let fields
|
||||||
export let labelPosition
|
export let labelPosition
|
||||||
export let title
|
export let title
|
||||||
|
export let saveButtonLabel
|
||||||
|
export let deleteButtonLabel
|
||||||
export let showSaveButton
|
export let showSaveButton
|
||||||
export let showDeleteButton
|
export let showDeleteButton
|
||||||
export let schema
|
export let schema
|
||||||
|
@ -33,6 +35,12 @@
|
||||||
let formId
|
let formId
|
||||||
|
|
||||||
$: onSave = [
|
$: onSave = [
|
||||||
|
{
|
||||||
|
"##eventHandlerType": "Validate Form",
|
||||||
|
parameters: {
|
||||||
|
componentId: formId,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"##eventHandlerType": "Save Row",
|
"##eventHandlerType": "Save Row",
|
||||||
parameters: {
|
parameters: {
|
||||||
|
@ -163,7 +171,7 @@
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="button"
|
type="button"
|
||||||
props={{
|
props={{
|
||||||
text: "Delete",
|
text: deleteButtonLabel || "Delete",
|
||||||
onClick: onDelete,
|
onClick: onDelete,
|
||||||
quiet: true,
|
quiet: true,
|
||||||
type: "secondary",
|
type: "secondary",
|
||||||
|
@ -175,7 +183,7 @@
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type="button"
|
type="button"
|
||||||
props={{
|
props={{
|
||||||
text: "Save",
|
text: saveButtonLabel || "Save",
|
||||||
onClick: onSave,
|
onClick: onSave,
|
||||||
type: "cta",
|
type: "cta",
|
||||||
}}
|
}}
|
||||||
|
@ -188,14 +196,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
|
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
|
||||||
{#each fields as field, idx}
|
{#each fields as field, idx}
|
||||||
{#if getComponentForField(field)}
|
{#if getComponentForField(field.name)}
|
||||||
<BlockComponent
|
<BlockComponent
|
||||||
type={getComponentForField(field)}
|
type={getComponentForField(field.name)}
|
||||||
props={{
|
props={{
|
||||||
field,
|
validation: field.validation,
|
||||||
label: field,
|
field: field.name,
|
||||||
placeholder: field,
|
label: field.displayName,
|
||||||
disabled,
|
placeholder: field.displayName,
|
||||||
}}
|
}}
|
||||||
order={idx}
|
order={idx}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
blur: () => close(),
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -48,8 +48,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const cellAPI = {
|
const cellAPI = {
|
||||||
focus: () => api?.focus(),
|
focus: () => api?.focus?.(),
|
||||||
blur: () => api?.blur(),
|
blur: () => api?.blur?.(),
|
||||||
|
isActive: () => api?.isActive?.() ?? false,
|
||||||
onKeyDown: (...params) => api?.onKeyDown(...params),
|
onKeyDown: (...params) => api?.onKeyDown(...params),
|
||||||
isReadonly: () => readonly,
|
isReadonly: () => readonly,
|
||||||
getType: () => column.schema.type,
|
getType: () => column.schema.type,
|
||||||
|
@ -67,6 +68,7 @@
|
||||||
{rowIdx}
|
{rowIdx}
|
||||||
{focused}
|
{focused}
|
||||||
{selectedUser}
|
{selectedUser}
|
||||||
|
{readonly}
|
||||||
error={$error}
|
error={$error}
|
||||||
on:click={() => focusedCellId.set(cellId)}
|
on:click={() => focusedCellId.set(cellId)}
|
||||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
export let rowIdx
|
export let rowIdx
|
||||||
export let defaultHeight = false
|
export let defaultHeight = false
|
||||||
export let center = false
|
export let center = false
|
||||||
|
export let readonly = false
|
||||||
|
|
||||||
$: style = getStyle(width, selectedUser)
|
$: style = getStyle(width, selectedUser)
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@
|
||||||
class:focused
|
class:focused
|
||||||
class:error
|
class:error
|
||||||
class:center
|
class:center
|
||||||
|
class:readonly
|
||||||
class:default-height={defaultHeight}
|
class:default-height={defaultHeight}
|
||||||
class:selected-other={selectedUser != null}
|
class:selected-other={selectedUser != null}
|
||||||
on:focus
|
on:focus
|
||||||
|
@ -121,7 +123,8 @@
|
||||||
.cell:hover {
|
.cell:hover {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.cell.highlighted:not(.focused) {
|
.cell.highlighted:not(.focused),
|
||||||
|
.cell.focused.readonly {
|
||||||
--cell-background: var(--cell-background-hover);
|
--cell-background: var(--cell-background-hover);
|
||||||
}
|
}
|
||||||
.cell.selected:not(.focused) {
|
.cell.selected:not(.focused) {
|
||||||
|
|
|
@ -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>
|
|
@ -20,6 +20,7 @@
|
||||||
ui,
|
ui,
|
||||||
columns,
|
columns,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
const bannedDisplayColumnTypes = [
|
const bannedDisplayColumnTypes = [
|
||||||
"link",
|
"link",
|
||||||
"array",
|
"array",
|
||||||
|
@ -92,6 +93,16 @@
|
||||||
columns.actions.changePrimaryDisplay(column.name)
|
columns.actions.changePrimaryDisplay(column.name)
|
||||||
open = false
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -100,7 +111,7 @@
|
||||||
style="flex: 0 0 {column.width}px;"
|
style="flex: 0 0 {column.width}px;"
|
||||||
bind:this={anchor}
|
bind:this={anchor}
|
||||||
class:disabled={$isReordering || $isResizing}
|
class:disabled={$isReordering || $isResizing}
|
||||||
class:sorted={sortedBy}
|
class:sticky={idx === "sticky"}
|
||||||
>
|
>
|
||||||
<GridCell
|
<GridCell
|
||||||
on:mousedown={onMouseDown}
|
on:mousedown={onMouseDown}
|
||||||
|
@ -128,11 +139,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<div class="more" on:click={() => (open = true)}>
|
||||||
class="more"
|
|
||||||
on:mousedown|stopPropagation
|
|
||||||
on:click={() => (open = true)}
|
|
||||||
>
|
|
||||||
<Icon
|
<Icon
|
||||||
size="S"
|
size="S"
|
||||||
name="MoreVertical"
|
name="MoreVertical"
|
||||||
|
@ -187,6 +194,7 @@
|
||||||
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
|
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
|
||||||
Move right
|
Move right
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem icon="VisibilityOff" on:click={hideColumn}>Hide column</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
@ -194,6 +202,10 @@
|
||||||
.header-cell {
|
.header-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.header-cell:not(.sticky):hover,
|
||||||
|
.header-cell:not(.sticky) :global(.cell:hover) {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
.header-cell.disabled {
|
.header-cell.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
@ -202,9 +214,6 @@
|
||||||
gap: calc(2 * var(--cell-spacing));
|
gap: calc(2 * var(--cell-spacing));
|
||||||
background: var(--spectrum-global-color-gray-100);
|
background: var(--spectrum-global-color-gray-100);
|
||||||
}
|
}
|
||||||
.header-cell.sorted :global(.cell) {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
|
@ -31,8 +31,10 @@
|
||||||
isOpen = true
|
isOpen = true
|
||||||
await tick()
|
await tick()
|
||||||
textarea.focus()
|
textarea.focus()
|
||||||
|
if (value?.length > 100) {
|
||||||
textarea.setSelectionRange(0, 0)
|
textarea.setSelectionRange(0, 0)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
textarea?.blur()
|
textarea?.blur()
|
||||||
|
@ -43,6 +45,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: () => open(),
|
focus: () => open(),
|
||||||
blur: () => close(),
|
blur: () => close(),
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: open,
|
focus: open,
|
||||||
blur: close,
|
blur: close,
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -235,6 +235,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: open,
|
focus: open,
|
||||||
blur: close,
|
blur: close,
|
||||||
|
isActive: () => isOpen,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
api = {
|
api = {
|
||||||
focus: () => input?.focus(),
|
focus: () => input?.focus(),
|
||||||
blur: () => input?.blur(),
|
blur: () => input?.blur(),
|
||||||
|
isActive: () => active,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,5 +12,5 @@
|
||||||
on:click={() => dispatch("add-column")}
|
on:click={() => dispatch("add-column")}
|
||||||
disabled={!$config.allowAddColumns}
|
disabled={!$config.allowAddColumns}
|
||||||
>
|
>
|
||||||
Create column
|
Add column
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
icon="TableRowAddBottom"
|
icon="TableRowAddBottom"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => dispatch("add-row")}
|
on:click={() => dispatch("add-row-inline")}
|
||||||
disabled={!loaded ||
|
disabled={!loaded ||
|
||||||
!$config.allowAddRows ||
|
!$config.allowAddRows ||
|
||||||
(!$columns.length && !$stickyColumn)}
|
(!$columns.length && !$stickyColumn)}
|
||||||
>
|
>
|
||||||
Create row
|
Add row
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -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>
|
|
@ -1,13 +1,8 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Modal, ModalContent, Button, notifications } from "@budibase/bbui"
|
||||||
Modal,
|
import { getContext, onMount } from "svelte"
|
||||||
ModalContent,
|
|
||||||
ActionButton,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { selectedRows, rows, config } = getContext("grid")
|
const { selectedRows, rows, config, subscribe } = getContext("grid")
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
|
@ -28,18 +23,21 @@
|
||||||
await rows.actions.deleteRows(rowsToDelete)
|
await rows.actions.deleteRows(rowsToDelete)
|
||||||
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
|
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => subscribe("request-bulk-delete", () => modal?.show()))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if selectedRowCount}
|
{#if selectedRowCount}
|
||||||
<div class="delete-button" data-ignore-click-outside="true">
|
<div class="delete-button" data-ignore-click-outside="true">
|
||||||
<ActionButton
|
<Button
|
||||||
icon="Delete"
|
icon="Delete"
|
||||||
size="S"
|
size="M"
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
disabled={!$config.allowEditRows}
|
disabled={!$config.allowEditRows}
|
||||||
|
cta
|
||||||
>
|
>
|
||||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
||||||
</ActionButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -57,16 +55,12 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.delete-button :global(.spectrum-ActionButton:not(:disabled) *) {
|
.delete-button :global(.spectrum-Button:not(:disabled)) {
|
||||||
color: var(--spectrum-global-color-red-400);
|
background-color: var(--spectrum-global-color-red-400);
|
||||||
}
|
|
||||||
.delete-button :global(.spectrum-ActionButton:not(:disabled)) {
|
|
||||||
border-color: var(--spectrum-global-color-red-400);
|
border-color: var(--spectrum-global-color-red-400);
|
||||||
}
|
}
|
||||||
/*.delete-button.disabled :global(.spectrum-ActionButton *) {*/
|
.delete-button :global(.spectrum-Button:not(:disabled):hover) {
|
||||||
/* color: var(--spectrum-global-color-gray-600);*/
|
background-color: var(--spectrum-global-color-red-500);
|
||||||
/*}*/
|
border-color: var(--spectrum-global-color-red-500);
|
||||||
/*.delete-button.disabled :global(.spectrum-ActionButton) {*/
|
}
|
||||||
/* border-color: var(--spectrum-global-color-gray-400);*/
|
|
||||||
/*}*/
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Toggle } from "@budibase/bbui"
|
import { ActionButton, Popover, Toggle } from "@budibase/bbui"
|
||||||
|
|
||||||
const { columns } = getContext("grid")
|
const { columns, stickyColumn } = getContext("grid")
|
||||||
|
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
@ -48,20 +48,24 @@
|
||||||
selected={open || anyHidden}
|
selected={open || anyHidden}
|
||||||
disabled={!$columns.length}
|
disabled={!$columns.length}
|
||||||
>
|
>
|
||||||
Hide columns
|
Columns
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
<Popover bind:open {anchor} align="left">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
|
{#if $stickyColumn}
|
||||||
|
<Toggle disabled size="S" value={true} />
|
||||||
|
<span>{$stickyColumn.label}</span>
|
||||||
|
{/if}
|
||||||
{#each $columns as column}
|
{#each $columns as column}
|
||||||
<Toggle
|
<Toggle
|
||||||
size="S"
|
size="S"
|
||||||
value={column.visible}
|
value={column.visible}
|
||||||
on:change={e => toggleVisibility(column, e.detail)}
|
on:change={e => toggleVisibility(column, e.detail)}
|
||||||
/>
|
/>
|
||||||
<span>{column.name}</span>
|
<span>{column.label}</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
|
|
@ -36,13 +36,13 @@
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="LineHeight"
|
icon="MoveUpDown"
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
selected={open}
|
selected={open}
|
||||||
>
|
>
|
||||||
Row height
|
Height
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||||
|
|
||||||
const { sort, visibleColumns, stickyColumn } = getContext("grid")
|
const { sort, columns, stickyColumn } = getContext("grid")
|
||||||
const orderOptions = [
|
const orderOptions = [
|
||||||
{ label: "A-Z", value: "ascending" },
|
{ label: "A-Z", value: "ascending" },
|
||||||
{ label: "Z-A", value: "descending" },
|
{ label: "Z-A", value: "descending" },
|
||||||
|
@ -11,15 +11,24 @@
|
||||||
let open = false
|
let open = false
|
||||||
let anchor
|
let anchor
|
||||||
|
|
||||||
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns)
|
$: columnOptions = getColumnOptions($stickyColumn, $columns)
|
||||||
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns)
|
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
|
||||||
|
|
||||||
const getColumnOptions = (stickyColumn, columns) => {
|
const getColumnOptions = (stickyColumn, columns) => {
|
||||||
let options = []
|
let options = []
|
||||||
if (stickyColumn) {
|
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 => {
|
const updateSortColumn = e => {
|
||||||
|
@ -37,13 +46,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we never have a sort column selected that is not visible
|
// Ensure we never have a sort column selected that is not visible
|
||||||
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => {
|
const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
|
||||||
if (!sortColumn) {
|
if (!sortColumn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
sortColumn !== stickyColumn?.name &&
|
sortColumn !== stickyColumn?.name &&
|
||||||
!visibleColumns.some(col => col.name === sortColumn)
|
!columns.some(col => col.name === sortColumn)
|
||||||
) {
|
) {
|
||||||
if (stickyColumn) {
|
if (stickyColumn) {
|
||||||
sort.update(state => ({
|
sort.update(state => ({
|
||||||
|
@ -53,7 +62,7 @@
|
||||||
} else {
|
} else {
|
||||||
sort.update(state => ({
|
sort.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
column: visibleColumns[0]?.name,
|
column: columns[0]?.name,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +75,7 @@
|
||||||
quiet
|
quiet
|
||||||
size="M"
|
size="M"
|
||||||
on:click={() => (open = !open)}
|
on:click={() => (open = !open)}
|
||||||
selected={open || $sort.column}
|
selected={open}
|
||||||
disabled={!columnOptions.length}
|
disabled={!columnOptions.length}
|
||||||
>
|
>
|
||||||
Sort
|
Sort
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
||||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
import AddRowButton from "../controls/AddRowButton.svelte"
|
||||||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
||||||
|
import ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
||||||
|
import NewRow from "./NewRow.svelte"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderHeight,
|
||||||
MaxCellRenderWidthOverflow,
|
MaxCellRenderWidthOverflow,
|
||||||
|
@ -110,6 +112,7 @@
|
||||||
<AddRowButton />
|
<AddRowButton />
|
||||||
<AddColumnButton />
|
<AddColumnButton />
|
||||||
<slot name="controls" />
|
<slot name="controls" />
|
||||||
|
<ColumnWidthButton />
|
||||||
<RowHeightButton />
|
<RowHeightButton />
|
||||||
<HideColumnsButton />
|
<HideColumnsButton />
|
||||||
<SortButton />
|
<SortButton />
|
||||||
|
@ -127,10 +130,11 @@
|
||||||
<HeaderRow />
|
<HeaderRow />
|
||||||
<GridBody />
|
<GridBody />
|
||||||
</div>
|
</div>
|
||||||
|
<BetaButton />
|
||||||
|
<NewRow />
|
||||||
<div class="overlays">
|
<div class="overlays">
|
||||||
<ResizeOverlay />
|
<ResizeOverlay />
|
||||||
<ReorderOverlay />
|
<ReorderOverlay />
|
||||||
<BetaButton />
|
|
||||||
<ScrollOverlay />
|
<ScrollOverlay />
|
||||||
<MenuOverlay />
|
<MenuOverlay />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,11 +2,25 @@
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
import GridRow from "./GridRow.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
|
let body
|
||||||
|
|
||||||
|
$: renderColumnsWidth = $renderedColumns.reduce(
|
||||||
|
(total, col) => (total += col.width),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Observe and record the height of the body
|
// Observe and record the height of the body
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
|
@ -24,6 +38,16 @@
|
||||||
{#each $renderedRows as row, idx}
|
{#each $renderedRows as row, idx}
|
||||||
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
||||||
{/each}
|
{/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>
|
</GridScrollWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -35,4 +59,15 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1 1 auto;
|
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>
|
</style>
|
||||||
|
|
|
@ -29,10 +29,7 @@
|
||||||
// Handles a wheel even and updates the scroll offsets
|
// Handles a wheel even and updates the scroll offsets
|
||||||
const handleWheel = e => {
|
const handleWheel = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const modifier = e.ctrlKey || e.metaKey
|
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
||||||
let x = modifier ? e.deltaY : e.deltaX
|
|
||||||
let y = modifier ? e.deltaX : e.deltaY
|
|
||||||
debouncedHandleWheel(x, y, e.clientY)
|
|
||||||
}
|
}
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
||||||
const { top, left } = $scroll
|
const { top, left } = $scroll
|
||||||
|
|
|
@ -2,8 +2,17 @@
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||||
|
import { Icon } 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>
|
</script>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
@ -14,6 +23,13 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</GridScrollWrapper>
|
</GridScrollWrapper>
|
||||||
|
<div
|
||||||
|
class="add"
|
||||||
|
style="left:{left}px"
|
||||||
|
on:click={() => dispatch("add-column")}
|
||||||
|
>
|
||||||
|
<Icon name="Add" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -27,4 +43,20 @@
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.add {
|
||||||
|
height: var(--default-row-height);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 40px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
border-left: var(--cell-border);
|
||||||
|
border-right: var(--cell-border);
|
||||||
|
border-bottom: var(--cell-border);
|
||||||
|
background: var(--spectrum-global-color-gray-100);
|
||||||
|
}
|
||||||
|
.add:hover {
|
||||||
|
background: var(--spectrum-global-color-gray-200);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
|
@ -1,24 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { Checkbox, Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import GridCell from "../cells/GridCell.svelte"
|
import GridCell from "../cells/GridCell.svelte"
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
import DataCell from "../cells/DataCell.svelte"
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||||
import HeaderCell from "../cells/HeaderCell.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 {
|
const {
|
||||||
rows,
|
rows,
|
||||||
selectedRows,
|
selectedRows,
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
|
renderedColumns,
|
||||||
renderedRows,
|
renderedRows,
|
||||||
focusedCellId,
|
focusedCellId,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
config,
|
config,
|
||||||
selectedCellMap,
|
selectedCellMap,
|
||||||
focusedRow,
|
focusedRow,
|
||||||
dispatch,
|
|
||||||
scrollLeft,
|
scrollLeft,
|
||||||
|
dispatch,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: rowCount = $rows.length
|
$: rowCount = $rows.length
|
||||||
|
@ -37,19 +39,6 @@
|
||||||
$selectedRows = allRows
|
$selectedRows = allRows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectRow = id => {
|
|
||||||
selectedRows.update(state => {
|
|
||||||
let newState = {
|
|
||||||
...state,
|
|
||||||
[id]: !state[id],
|
|
||||||
}
|
|
||||||
if (!newState[id]) {
|
|
||||||
delete newState[id]
|
|
||||||
}
|
|
||||||
return newState
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -58,26 +47,14 @@
|
||||||
class:scrolled={$scrollLeft > 0}
|
class:scrolled={$scrollLeft > 0}
|
||||||
>
|
>
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<GridCell width={GutterWidth} defaultHeight center>
|
<GutterCell
|
||||||
<div class="gutter">
|
disableExpand
|
||||||
<div class="checkbox visible">
|
disableNumber
|
||||||
{#if $config.allowDeleteRows}
|
on:select={selectAll}
|
||||||
<div on:click={selectAll}>
|
defaultHeight
|
||||||
<Checkbox
|
rowSelected={selectedRowCount && selectedRowCount === rowCount}
|
||||||
value={rowCount && selectedRowCount === rowCount}
|
|
||||||
disabled={!$renderedRows.length}
|
disabled={!$renderedRows.length}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if $config.allowExpandRows}
|
|
||||||
<div class="expand">
|
|
||||||
<Icon name="Maximize" size="S" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</GridCell>
|
|
||||||
|
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -95,41 +72,7 @@
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
on:mouseleave={() => ($hoveredRowId = null)}
|
||||||
>
|
>
|
||||||
<GridCell
|
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||||
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>
|
|
||||||
{#if $stickyColumn}
|
{#if $stickyColumn}
|
||||||
<DataCell
|
<DataCell
|
||||||
{row}
|
{row}
|
||||||
|
@ -146,6 +89,24 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/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>
|
</GridScrollWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,6 +117,7 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
background: var(--cell-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add right border */
|
/* Add right border */
|
||||||
|
@ -203,43 +165,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
.row.new :global(*:hover) {
|
||||||
/* Styles for gutter */
|
cursor: pointer;
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
export const Padding = 276
|
export const Padding = 128
|
||||||
export const MaxCellRenderHeight = 252
|
export const MaxCellRenderHeight = 252
|
||||||
export const MaxCellRenderWidthOverflow = 200
|
export const MaxCellRenderWidthOverflow = 200
|
||||||
export const ScrollBarSize = 8
|
export const ScrollBarSize = 8
|
||||||
export const GutterWidth = 72
|
export const GutterWidth = 72
|
||||||
export const DefaultColumnWidth = 200
|
export const DefaultColumnWidth = 200
|
||||||
export const MinColumnWidth = 100
|
export const MinColumnWidth = 80
|
||||||
export const SmallRowHeight = 36
|
export const SmallRowHeight = 36
|
||||||
export const MediumRowHeight = 64
|
export const MediumRowHeight = 64
|
||||||
export const LargeRowHeight = 92
|
export const LargeRowHeight = 92
|
||||||
export const DefaultRowHeight = SmallRowHeight
|
export const DefaultRowHeight = SmallRowHeight
|
||||||
|
export const NewRowID = "new"
|
||||||
|
export const BlankRowID = "blank"
|
||||||
|
export const RowPageSize = 100
|
||||||
|
|
|
@ -15,7 +15,7 @@ const TypeIconMap = {
|
||||||
number: "123",
|
number: "123",
|
||||||
boolean: "Boolean",
|
boolean: "Boolean",
|
||||||
attachment: "AppleFiles",
|
attachment: "AppleFiles",
|
||||||
link: "Link",
|
link: "DataCorrelated",
|
||||||
formula: "Calculator",
|
formula: "Calculator",
|
||||||
json: "Brackets",
|
json: "Brackets",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { debounce } from "../../../utils/utils"
|
import { debounce } from "../../../utils/utils"
|
||||||
|
import { NewRowID } from "../lib/constants"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
enrichedRows,
|
enrichedRows,
|
||||||
|
@ -10,15 +11,24 @@
|
||||||
stickyColumn,
|
stickyColumn,
|
||||||
focusedCellAPI,
|
focusedCellAPI,
|
||||||
clipboard,
|
clipboard,
|
||||||
|
dispatch,
|
||||||
|
selectedRows,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
// Global key listener which intercepts all key events
|
// Global key listener which intercepts all key events
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
// If nothing selected avoid processing further key presses
|
// If nothing selected avoid processing further key presses
|
||||||
if (!$focusedCellId) {
|
if (!$focusedCellId) {
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab" || e.key?.startsWith("Arrow")) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
focusFirstCell()
|
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
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,10 +36,20 @@
|
||||||
// Always intercept certain key presses
|
// Always intercept certain key presses
|
||||||
const api = $focusedCellAPI
|
const api = $focusedCellAPI
|
||||||
if (e.key === "Escape") {
|
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") {
|
} else if (e.key === "Tab") {
|
||||||
|
e.preventDefault()
|
||||||
api?.blur?.()
|
api?.blur?.()
|
||||||
changeFocusedColumn(1)
|
changeFocusedColumn(1)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the key event to the selected cell and let it decide whether to
|
// Pass the key event to the selected cell and let it decide whether to
|
||||||
|
@ -54,8 +74,12 @@
|
||||||
clipboard.actions.copy()
|
clipboard.actions.copy()
|
||||||
break
|
break
|
||||||
case "v":
|
case "v":
|
||||||
|
if (!api?.isReadonly()) {
|
||||||
clipboard.actions.paste()
|
clipboard.actions.paste()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
case "Enter":
|
||||||
|
dispatch("add-row-inline")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
@ -73,11 +97,19 @@
|
||||||
break
|
break
|
||||||
case "Delete":
|
case "Delete":
|
||||||
case "Backspace":
|
case "Backspace":
|
||||||
|
if (Object.keys($selectedRows).length) {
|
||||||
|
dispatch("request-bulk-delete")
|
||||||
|
} else {
|
||||||
deleteSelectedCell()
|
deleteSelectedCell()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case "Enter":
|
case "Enter":
|
||||||
focusCell()
|
focusCell()
|
||||||
break
|
break
|
||||||
|
case " ":
|
||||||
|
case "Space":
|
||||||
|
toggleSelectRow()
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
startEnteringValue(e.key, e.which)
|
startEnteringValue(e.key, e.which)
|
||||||
}
|
}
|
||||||
|
@ -156,7 +188,7 @@
|
||||||
|
|
||||||
// Focuses the cell and starts entering a new value
|
// Focuses the cell and starts entering a new value
|
||||||
const startEnteringValue = (key, keyCode) => {
|
const startEnteringValue = (key, keyCode) => {
|
||||||
if ($focusedCellAPI) {
|
if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
|
||||||
const type = $focusedCellAPI.getType()
|
const type = $focusedCellAPI.getType()
|
||||||
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
||||||
$focusedCellAPI.setValue(parseInt(key))
|
$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(() => {
|
onMount(() => {
|
||||||
document.addEventListener("keydown", handleKeyDown)
|
document.addEventListener("keydown", handleKeyDown)
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
copiedCell,
|
copiedCell,
|
||||||
clipboard,
|
clipboard,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
focusedCellAPI,
|
||||||
} = getContext("grid")
|
} = getContext("grid")
|
||||||
|
|
||||||
$: style = makeStyle($menu)
|
$: style = makeStyle($menu)
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="Paste"
|
icon="Paste"
|
||||||
disabled={$copiedCell == null}
|
disabled={$copiedCell == null || $focusedCellAPI?.isReadonly()}
|
||||||
on:click={clipboard.actions.paste}
|
on:click={clipboard.actions.paste}
|
||||||
on:click={menu.actions.close}
|
on:click={menu.actions.close}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { fetchData } from "../../../fetch/fetchData"
|
import { fetchData } from "../../../fetch/fetchData"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
|
import { NewRowID, RowPageSize } from "../lib/constants"
|
||||||
|
|
||||||
const initialSortState = {
|
const initialSortState = {
|
||||||
column: null,
|
column: null,
|
||||||
|
@ -16,6 +17,7 @@ export const createStores = () => {
|
||||||
const sort = writable(initialSortState)
|
const sort = writable(initialSortState)
|
||||||
const rowChangeCache = writable({})
|
const rowChangeCache = writable({})
|
||||||
const inProgressChanges = writable({})
|
const inProgressChanges = writable({})
|
||||||
|
const hasNextPage = writable(false)
|
||||||
|
|
||||||
// Generate a lookup map to quick find a row by ID
|
// Generate a lookup map to quick find a row by ID
|
||||||
const rowLookupMap = derived(
|
const rowLookupMap = derived(
|
||||||
|
@ -50,6 +52,7 @@ export const createStores = () => {
|
||||||
sort,
|
sort,
|
||||||
rowChangeCache,
|
rowChangeCache,
|
||||||
inProgressChanges,
|
inProgressChanges,
|
||||||
|
hasNextPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +73,7 @@ export const deriveStores = context => {
|
||||||
rowChangeCache,
|
rowChangeCache,
|
||||||
inProgressChanges,
|
inProgressChanges,
|
||||||
previousFocusedRowId,
|
previousFocusedRowId,
|
||||||
|
hasNextPage,
|
||||||
} = context
|
} = context
|
||||||
const instanceLoaded = writable(false)
|
const instanceLoaded = writable(false)
|
||||||
const fetch = writable(null)
|
const fetch = writable(null)
|
||||||
|
@ -114,7 +118,7 @@ export const deriveStores = context => {
|
||||||
filter: [],
|
filter: [],
|
||||||
sortColumn: initialSortState.column,
|
sortColumn: initialSortState.column,
|
||||||
sortOrder: initialSortState.order,
|
sortOrder: initialSortState.order,
|
||||||
limit: 100,
|
limit: RowPageSize,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -122,6 +126,7 @@ export const deriveStores = context => {
|
||||||
// Subscribe to changes of this fetch model
|
// Subscribe to changes of this fetch model
|
||||||
unsubscribe = newFetch.subscribe($fetch => {
|
unsubscribe = newFetch.subscribe($fetch => {
|
||||||
if ($fetch.loaded && !$fetch.loading) {
|
if ($fetch.loaded && !$fetch.loading) {
|
||||||
|
hasNextPage.set($fetch.hasNextPage)
|
||||||
const $instanceLoaded = get(instanceLoaded)
|
const $instanceLoaded = get(instanceLoaded)
|
||||||
const resetRows = $fetch.resetKey !== lastResetKey
|
const resetRows = $fetch.resetKey !== lastResetKey
|
||||||
lastResetKey = $fetch.resetKey
|
lastResetKey = $fetch.resetKey
|
||||||
|
@ -230,7 +235,7 @@ export const deriveStores = context => {
|
||||||
if (bubble) {
|
if (bubble) {
|
||||||
throw error
|
throw error
|
||||||
} else {
|
} else {
|
||||||
handleValidationError("new", error)
|
handleValidationError(NewRowID, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { writable, derived, get } from "svelte/store"
|
import { writable, derived, get } from "svelte/store"
|
||||||
import { tick } from "svelte"
|
import { tick } from "svelte"
|
||||||
import { Padding, GutterWidth, DefaultRowHeight } from "../lib/constants"
|
import { Padding, GutterWidth } from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
const scroll = writable({
|
const scroll = writable({
|
||||||
|
@ -29,7 +29,7 @@ export const deriveStores = context => {
|
||||||
// Derive vertical limits
|
// Derive vertical limits
|
||||||
const contentHeight = derived(
|
const contentHeight = derived(
|
||||||
[rows, rowHeight],
|
[rows, rowHeight],
|
||||||
([$rows, $rowHeight]) => $rows.length * $rowHeight + Padding,
|
([$rows, $rowHeight]) => ($rows.length + 1) * $rowHeight + Padding,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const maxScrollTop = derived(
|
const maxScrollTop = derived(
|
||||||
|
@ -138,7 +138,7 @@ export const initialise = context => {
|
||||||
const $scroll = get(scroll)
|
const $scroll = get(scroll)
|
||||||
const $bounds = get(bounds)
|
const $bounds = get(bounds)
|
||||||
const $rowHeight = get(rowHeight)
|
const $rowHeight = get(rowHeight)
|
||||||
const verticalOffset = DefaultRowHeight * 1.5
|
const verticalOffset = 60
|
||||||
|
|
||||||
// Ensure vertical position is viewable
|
// Ensure vertical position is viewable
|
||||||
if ($focusedRow) {
|
if ($focusedRow) {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { writable, get, derived } from "svelte/store"
|
import { writable, get, derived } from "svelte/store"
|
||||||
|
import { tick } from "svelte"
|
||||||
import {
|
import {
|
||||||
DefaultRowHeight,
|
DefaultRowHeight,
|
||||||
LargeRowHeight,
|
LargeRowHeight,
|
||||||
MediumRowHeight,
|
MediumRowHeight,
|
||||||
|
NewRowID,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
|
||||||
export const createStores = () => {
|
export const createStores = () => {
|
||||||
|
@ -49,14 +51,14 @@ export const deriveStores = context => {
|
||||||
([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
|
([$focusedCellId, $rowLookupMap, $enrichedRows]) => {
|
||||||
const rowId = $focusedCellId?.split("-")[0]
|
const rowId = $focusedCellId?.split("-")[0]
|
||||||
|
|
||||||
if (rowId === "new") {
|
// Edge case for new rows
|
||||||
// Edge case for new row
|
if (rowId === NewRowID) {
|
||||||
return { _id: rowId }
|
return { _id: NewRowID }
|
||||||
} else {
|
}
|
||||||
|
|
||||||
// All normal rows
|
// All normal rows
|
||||||
const index = $rowLookupMap[rowId]
|
const index = $rowLookupMap[rowId]
|
||||||
return $enrichedRows[index]
|
return $enrichedRows[index]
|
||||||
}
|
|
||||||
},
|
},
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
@ -101,7 +103,10 @@ export const initialise = context => {
|
||||||
} = context
|
} = context
|
||||||
|
|
||||||
// Ensure we clear invalid rows from state if they disappear
|
// 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 $focusedCellId = get(focusedCellId)
|
||||||
const $selectedRows = get(selectedRows)
|
const $selectedRows = get(selectedRows)
|
||||||
const $hoveredRowId = get(hoveredRowId)
|
const $hoveredRowId = get(hoveredRowId)
|
||||||
|
@ -140,20 +145,6 @@ export const initialise = context => {
|
||||||
lastFocusedRowId = id
|
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
|
// Remove hovered row when a cell is selected
|
||||||
focusedCellId.subscribe(cell => {
|
focusedCellId.subscribe(cell => {
|
||||||
if (cell && get(hoveredRowId)) {
|
if (cell && get(hoveredRowId)) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { derived, get } from "svelte/store"
|
||||||
import {
|
import {
|
||||||
MaxCellRenderHeight,
|
MaxCellRenderHeight,
|
||||||
MaxCellRenderWidthOverflow,
|
MaxCellRenderWidthOverflow,
|
||||||
|
MinColumnWidth,
|
||||||
ScrollBarSize,
|
ScrollBarSize,
|
||||||
} from "../lib/constants"
|
} from "../lib/constants"
|
||||||
|
|
||||||
|
@ -45,11 +46,16 @@ export const deriveStores = context => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Derive visible columns
|
// Derive visible columns
|
||||||
|
const scrollLeftRounded = derived(scrollLeft, $scrollLeft => {
|
||||||
|
const interval = MinColumnWidth
|
||||||
|
return Math.round($scrollLeft / interval) * interval
|
||||||
|
})
|
||||||
const renderedColumns = derived(
|
const renderedColumns = derived(
|
||||||
[visibleColumns, scrollLeft, width],
|
[visibleColumns, scrollLeftRounded, width],
|
||||||
([$visibleColumns, $scrollLeft, $width]) => {
|
([$visibleColumns, $scrollLeft, $width], set) => {
|
||||||
if (!$visibleColumns.length) {
|
if (!$visibleColumns.length) {
|
||||||
return []
|
set([])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let startColIdx = 0
|
let startColIdx = 0
|
||||||
let rightEdge = $visibleColumns[0].width
|
let rightEdge = $visibleColumns[0].width
|
||||||
|
@ -69,19 +75,17 @@ export const deriveStores = context => {
|
||||||
leftEdge += $visibleColumns[endColIdx].width
|
leftEdge += $visibleColumns[endColIdx].width
|
||||||
endColIdx++
|
endColIdx++
|
||||||
}
|
}
|
||||||
const nextRenderedColumns = $visibleColumns.slice(startColIdx, endColIdx)
|
// Render an additional column on either side to account for
|
||||||
|
// debounce column updates based on scroll position
|
||||||
// Cautiously shrink the number of rendered columns.
|
const next = $visibleColumns.slice(
|
||||||
// This is to avoid rapidly shrinking and growing the visible column count
|
Math.max(0, startColIdx - 1),
|
||||||
// which results in remounting cells
|
endColIdx + 1
|
||||||
const currentCount = get(renderedColumns).length
|
)
|
||||||
if (currentCount === nextRenderedColumns.length + 1) {
|
const current = get(renderedColumns)
|
||||||
return $visibleColumns.slice(startColIdx, endColIdx + 1)
|
if (JSON.stringify(next) !== JSON.stringify(current)) {
|
||||||
} else {
|
set(next)
|
||||||
return nextRenderedColumns
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const hiddenColumnsWidth = derived(
|
const hiddenColumnsWidth = derived(
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import env from "../../environment"
|
import env from "../../environment"
|
||||||
import packageJson from "../../../package.json"
|
|
||||||
import {
|
import {
|
||||||
createLinkView,
|
createLinkView,
|
||||||
createRoutingView,
|
createRoutingView,
|
||||||
|
@ -24,6 +23,7 @@ import {
|
||||||
migrations,
|
migrations,
|
||||||
objectStore,
|
objectStore,
|
||||||
ErrorCode,
|
ErrorCode,
|
||||||
|
env as envCore,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
import { USERS_TABLE_SCHEMA } from "../../constants"
|
||||||
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
import { buildDefaultDocs } from "../../db/defaultData/datasource_bb_default"
|
||||||
|
@ -264,7 +264,7 @@ async function performAppCreate(ctx: UserCtx) {
|
||||||
_rev: undefined,
|
_rev: undefined,
|
||||||
appId,
|
appId,
|
||||||
type: "app",
|
type: "app",
|
||||||
version: packageJson.version,
|
version: envCore.VERSION,
|
||||||
componentLibraries: ["@budibase/standard-components"],
|
componentLibraries: ["@budibase/standard-components"],
|
||||||
name: name,
|
name: name,
|
||||||
url: url,
|
url: url,
|
||||||
|
@ -433,7 +433,7 @@ export async function updateClient(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update versions in app package
|
// Update versions in app package
|
||||||
const updatedToVersion = packageJson.version
|
const updatedToVersion = envCore.VERSION
|
||||||
const appPackageUpdates = {
|
const appPackageUpdates = {
|
||||||
version: updatedToVersion,
|
version: updatedToVersion,
|
||||||
revertableVersion: currentVersion,
|
revertableVersion: currentVersion,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { checkSlashesInUrl } from "../../utilities"
|
||||||
import { request } from "../../utilities/workerRequests"
|
import { request } from "../../utilities/workerRequests"
|
||||||
import { clearLock as redisClearLock } from "../../utilities/redis"
|
import { clearLock as redisClearLock } from "../../utilities/redis"
|
||||||
import { DocumentType } from "../../db/utils"
|
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"
|
import { events, db as dbCore, cache } from "@budibase/backend-core"
|
||||||
|
|
||||||
async function redirect(ctx: any, method: string, path: string = "global") {
|
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) {
|
export async function getBudibaseVersion(ctx: any) {
|
||||||
const version = require("../../../package.json").version
|
const version = envCore.VERSION
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
version,
|
version,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,32 @@
|
||||||
import {
|
import {
|
||||||
|
Datasource,
|
||||||
|
FieldSchema,
|
||||||
|
FieldType,
|
||||||
FilterType,
|
FilterType,
|
||||||
IncludeRelationship,
|
IncludeRelationship,
|
||||||
Operation,
|
Operation,
|
||||||
PaginationJson,
|
PaginationJson,
|
||||||
RelationshipsJson,
|
RelationshipsJson,
|
||||||
|
RelationshipTypes,
|
||||||
|
Row,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortJson,
|
SortJson,
|
||||||
Datasource,
|
|
||||||
FieldSchema,
|
|
||||||
Row,
|
|
||||||
Table,
|
|
||||||
RelationshipTypes,
|
|
||||||
FieldType,
|
|
||||||
SortType,
|
SortType,
|
||||||
|
Table,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
|
breakExternalTableId,
|
||||||
breakRowIdField,
|
breakRowIdField,
|
||||||
|
convertRowId,
|
||||||
generateRowIdField,
|
generateRowIdField,
|
||||||
isRowId,
|
isRowId,
|
||||||
convertRowId,
|
isSQL,
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
import { getDatasourceAndQuery } from "./utils"
|
import { getDatasourceAndQuery } from "./utils"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
import { breakExternalTableId, isSQL } from "../../../integrations/utils"
|
|
||||||
import { processObjectSync } from "@budibase/string-templates"
|
import { processObjectSync } from "@budibase/string-templates"
|
||||||
import { cloneDeep } from "lodash/fp"
|
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 { db as dbCore } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
@ -382,10 +383,18 @@ export class ExternalRequest {
|
||||||
}
|
}
|
||||||
const display = linkedTable.primaryDisplay
|
const display = linkedTable.primaryDisplay
|
||||||
for (let key of Object.keys(row[relationship.column])) {
|
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] = {
|
row[relationship.column][key] = {
|
||||||
primaryDisplay: display ? related[display] : undefined,
|
primaryDisplay: relatedDisplay || "Invalid display column",
|
||||||
_id: related._id,
|
_id: relatedRow._id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import Router from "@koa/router"
|
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 currentApp from "../middleware/currentapp"
|
||||||
import zlib from "zlib"
|
import zlib from "zlib"
|
||||||
import { mainRoutes, staticRoutes, publicRoutes } from "./routes"
|
import { mainRoutes, staticRoutes, publicRoutes } from "./routes"
|
||||||
import pkg from "../../package.json"
|
|
||||||
import { middleware as pro } from "@budibase/pro"
|
import { middleware as pro } from "@budibase/pro"
|
||||||
export { shutdown } from "./routes/public"
|
export { shutdown } from "./routes/public"
|
||||||
const compress = require("koa-compress")
|
const compress = require("koa-compress")
|
||||||
|
@ -11,7 +10,7 @@ const compress = require("koa-compress")
|
||||||
export const router: Router = new Router()
|
export const router: Router = new Router()
|
||||||
|
|
||||||
router.get("/health", ctx => (ctx.status = 200))
|
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)
|
router.use(middleware.errorHandling)
|
||||||
|
|
||||||
|
|
|
@ -921,6 +921,7 @@ describe("row api - postgres", () => {
|
||||||
[m2mFieldName]: [
|
[m2mFieldName]: [
|
||||||
{
|
{
|
||||||
_id: row._id,
|
_id: row._id,
|
||||||
|
primaryDisplay: "Invalid display column",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -929,6 +930,7 @@ describe("row api - postgres", () => {
|
||||||
[m2mFieldName]: [
|
[m2mFieldName]: [
|
||||||
{
|
{
|
||||||
_id: row._id,
|
_id: row._id,
|
||||||
|
primaryDisplay: "Invalid display column",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
|
@ -79,10 +79,17 @@ function generateSchema(
|
||||||
if (!relatedTable) {
|
if (!relatedTable) {
|
||||||
throw "Referenced table doesn't exist"
|
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.integer(column.foreignKey).unsigned()
|
||||||
|
}
|
||||||
|
|
||||||
schema
|
schema
|
||||||
.foreign(column.foreignKey)
|
.foreign(column.foreignKey)
|
||||||
.references(`${tableName}.${relatedTable.primary[0]}`)
|
.references(`${tableName}.${relatedPrimary}`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {
|
import {
|
||||||
FieldTypes,
|
|
||||||
FormulaTypes,
|
|
||||||
AutoFieldDefaultNames,
|
AutoFieldDefaultNames,
|
||||||
AutoFieldSubTypes,
|
AutoFieldSubTypes,
|
||||||
|
FieldTypes,
|
||||||
|
FormulaTypes,
|
||||||
} from "../../constants"
|
} from "../../constants"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
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
|
* 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
|
const isStatic = schema.formulaType === FormulaTypes.STATIC
|
||||||
if (
|
if (
|
||||||
schema.type !== FieldTypes.FORMULA ||
|
schema.type !== FieldTypes.FORMULA ||
|
||||||
|
schema.formula == null ||
|
||||||
(dynamic && isStatic) ||
|
(dynamic && isStatic) ||
|
||||||
(!dynamic && !isStatic)
|
(!dynamic && !isStatic)
|
||||||
) {
|
) {
|
||||||
|
@ -57,7 +58,6 @@ export function processFormulas(
|
||||||
}
|
}
|
||||||
// iterate through rows and process formula
|
// iterate through rows and process formula
|
||||||
for (let i = 0; i < rowArray.length; i++) {
|
for (let i = 0; i < rowArray.length; i++) {
|
||||||
if (schema.formula) {
|
|
||||||
let row = rowArray[i]
|
let row = rowArray[i]
|
||||||
let context = contextRows ? contextRows[i] : row
|
let context = contextRows ? contextRows[i] : row
|
||||||
rowArray[i] = {
|
rowArray[i] = {
|
||||||
|
@ -66,7 +66,6 @@ export function processFormulas(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return single ? rowArray[0] : rowArray
|
return single ? rowArray[0] : rowArray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,11 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"types": [ "node", "jest" ],
|
"types": ["node", "jest"],
|
||||||
"outDir": "dist",
|
"outDir": "dist/src",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*"],
|
||||||
"src/**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@budibase/types": ["../types/src"],
|
"@budibase/types": ["../types/src"],
|
||||||
"@budibase/backend-core": ["../backend-core/src"],
|
"@budibase/backend-core": ["../backend-core/src"],
|
||||||
|
@ -17,6 +18,12 @@
|
||||||
"require": ["tsconfig-paths/register"],
|
"require": ["tsconfig-paths/register"],
|
||||||
"swc": true
|
"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"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["**/*.js", "**/*.ts", "package.json"],
|
"include": ["**/*.js", "**/*.ts"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "./tsconfig-base.build.json",
|
"extends": "./tsconfig-base.build.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"outDir": "dist/cjs",
|
"outDir": "dist/cjs/src",
|
||||||
"target": "es2015"
|
"target": "es2015"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "./tsconfig-base.build.json",
|
"extends": "./tsconfig-base.build.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"outDir": "dist/mjs",
|
"outDir": "dist/mjs/src",
|
||||||
"target": "esnext"
|
"target": "esnext"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,11 @@
|
||||||
"require": ["tsconfig-paths/register"],
|
"require": ["tsconfig-paths/register"],
|
||||||
"swc": true
|
"swc": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "package.json"],
|
"references": [
|
||||||
|
{ "path": "../types" },
|
||||||
|
{ "path": "../backend-core" },
|
||||||
|
{ "path": "../../../budibase-pro/packages/pro" }
|
||||||
|
],
|
||||||
|
"include": ["src/**/*"],
|
||||||
"exclude": ["dist"]
|
"exclude": ["dist"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"require": ["tsconfig-paths/register"]
|
"require": ["tsconfig-paths/register"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "package.json"],
|
"references": [
|
||||||
|
{ "path": "../packages/types" },
|
||||||
|
{ "path": "../packages/backend-core" }
|
||||||
|
],
|
||||||
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue