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