Merge remote-tracking branch 'origin/develop' into feat/per-app-builder-fe

This commit is contained in:
Peter Clement 2023-08-31 11:07:39 +01:00
commit 2360e5d8a5
134 changed files with 2917 additions and 1445 deletions

View File

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

View File

@ -9,6 +9,7 @@
export let fixed = false export let fixed = false
export let inline = false export let inline = false
export let disableCancel = false export let disableCancel = false
export let autoFocus = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let visible = fixed || inline let visible = fixed || inline
@ -53,6 +54,9 @@
} }
async function focusModal(node) { async function focusModal(node) {
if (!autoFocus) {
return
}
await tick() await tick()
// Try to focus first input // Try to focus first input

View File

@ -57,10 +57,8 @@
function calculateIndicatorLength() { function calculateIndicatorLength() {
if (!vertical) { if (!vertical) {
width = $tab.info?.width + "px" width = $tab.info?.width + "px"
height = $tab.info?.height
} else { } else {
height = $tab.info?.height + 4 + "px" height = $tab.info?.height + 4 + "px"
width = $tab.info?.width
} }
} }

View File

@ -351,12 +351,19 @@ const getProviderContextBindings = (asset, dataProviders) => {
schema = info.schema schema = info.schema
table = info.table table = info.table
// For JSON arrays, use the array name as the readable prefix. // Determine what to prefix bindings with
// Otherwise use the table name
if (datasource.type === "jsonarray") { if (datasource.type === "jsonarray") {
// For JSON arrays, use the array name as the readable prefix
const split = datasource.label.split(".") const split = datasource.label.split(".")
readablePrefix = split[split.length - 1] readablePrefix = split[split.length - 1]
} else if (datasource.type === "viewV2") {
// For views, use the view name
const view = Object.values(table?.views || {}).find(
view => view.id === datasource.id
)
readablePrefix = view?.name
} else { } else {
// Otherwise use the table name
readablePrefix = info.table?.name readablePrefix = info.table?.name
} }
} }
@ -464,7 +471,7 @@ const getComponentBindingCategory = (component, context, def) => {
*/ */
export const getUserBindings = () => { export const getUserBindings = () => {
let bindings = [] let bindings = []
const { schema } = getSchemaForTable(TableNames.USERS) const { schema } = getSchemaForDatasourcePlus(TableNames.USERS)
const keys = Object.keys(schema).sort() const keys = Object.keys(schema).sort()
const safeUser = makePropSafe("user") const safeUser = makePropSafe("user")
@ -714,17 +721,25 @@ export const getActionBindings = (actions, actionId) => {
} }
/** /**
* Gets the schema for a certain table ID. * Gets the schema for a certain datasource plus.
* The options which can be passed in are: * The options which can be passed in are:
* formSchema: whether the schema is for a form * formSchema: whether the schema is for a form
* searchableSchema: whether to generate a searchable schema, which may have * searchableSchema: whether to generate a searchable schema, which may have
* fewer fields than a readable schema * fewer fields than a readable schema
* @param tableId the table ID to get the schema for * @param resourceId the DS+ resource ID
* @param options options for generating the schema * @param options options for generating the schema
* @return {{schema: Object, table: Object}} * @return {{schema: Object, table: Object}}
*/ */
export const getSchemaForTable = (tableId, options) => { export const getSchemaForDatasourcePlus = (resourceId, options) => {
return getSchemaForDatasource(null, { type: "table", tableId }, options) const isViewV2 = resourceId?.includes("view_")
const datasource = isViewV2
? {
type: "viewV2",
id: resourceId,
tableId: resourceId.split("_").slice(1, 3).join("_"),
}
: { type: "table", tableId: resourceId }
return getSchemaForDatasource(null, datasource, options)
} }
/** /**
@ -801,9 +816,21 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
// Determine the schema from the backing entity if not already determined // Determine the schema from the backing entity if not already determined
if (table && !schema) { if (table && !schema) {
if (type === "view") { if (type === "view") {
// For views, the schema is pulled from the `views` property of the // Old views
// table
schema = cloneDeep(table.views?.[datasource.name]?.schema) schema = cloneDeep(table.views?.[datasource.name]?.schema)
} else if (type === "viewV2") {
// New views which are DS+
const view = Object.values(table.views || {}).find(
view => view.id === datasource.id
)
schema = cloneDeep(view?.schema)
// Strip hidden fields
Object.keys(schema || {}).forEach(field => {
if (!schema[field].visible) {
delete schema[field]
}
})
} else if ( } else if (
type === "query" && type === "query" &&
(options.formSchema || options.searchableSchema) (options.formSchema || options.searchableSchema)
@ -849,12 +876,12 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
// Determine if we should add ID and rev to the schema // Determine if we should add ID and rev to the schema
const isInternal = table && !table.sql const isInternal = table && !table.sql
const isTable = ["table", "link"].includes(datasource.type) const isDSPlus = ["table", "link", "viewV2"].includes(datasource.type)
// ID is part of the readable schema for all tables // ID is part of the readable schema for all tables
// Rev is part of the readable schema for internal tables only // Rev is part of the readable schema for internal tables only
let addId = isTable let addId = isDSPlus
let addRev = isTable && isInternal let addRev = isDSPlus && isInternal
// Don't add ID or rev for form schemas // Don't add ID or rev for form schemas
if (options.formSchema) { if (options.formSchema) {
@ -864,7 +891,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
// ID is only searchable for internal tables // ID is only searchable for internal tables
else if (options.searchableSchema) { else if (options.searchableSchema) {
addId = isTable && isInternal addId = isDSPlus && isInternal
} }
// Add schema properties if required // Add schema properties if required

View File

@ -1,10 +1,10 @@
import rowListScreen from "./rowListScreen" import rowListScreen from "./rowListScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
const allTemplates = tables => [...rowListScreen(tables)] const allTemplates = datasources => [...rowListScreen(datasources)]
// Allows us to apply common behaviour to all create() functions // Allows us to apply common behaviour to all create() functions
const createTemplateOverride = (frontendState, template) => () => { const createTemplateOverride = template => () => {
const screen = template.create() const screen = template.create()
screen.name = screen.props._id screen.name = screen.props._id
screen.routing.route = screen.routing.route.toLowerCase() screen.routing.route = screen.routing.route.toLowerCase()
@ -12,14 +12,13 @@ const createTemplateOverride = (frontendState, template) => () => {
return screen return screen
} }
export default (frontendState, tables) => { export default datasources => {
const enrichTemplate = template => ({ const enrichTemplate = template => ({
...template, ...template,
create: createTemplateOverride(frontendState, template), create: createTemplateOverride(template),
}) })
const fromScratch = enrichTemplate(createFromScratchScreen) const fromScratch = enrichTemplate(createFromScratchScreen)
const tableTemplates = allTemplates(tables).map(enrichTemplate) const tableTemplates = allTemplates(datasources).map(enrichTemplate)
return [ return [
fromScratch, fromScratch,
...tableTemplates.sort((templateA, templateB) => { ...tableTemplates.sort((templateA, templateB) => {

View File

@ -2,31 +2,26 @@ import sanitizeUrl from "./utils/sanitizeUrl"
import { Screen } from "./utils/Screen" import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component" import { Component } from "./utils/Component"
export default function (tables) { export default function (datasources) {
return tables.map(table => { return datasources.map(datasource => {
return { return {
name: `${table.name} - List`, name: `${datasource.name} - List`,
create: () => createScreen(table), create: () => createScreen(datasource),
id: ROW_LIST_TEMPLATE, id: ROW_LIST_TEMPLATE,
table: table._id, resourceId: datasource.resourceId,
} }
}) })
} }
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE" export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
export const rowListUrl = table => sanitizeUrl(`/${table.name}`) export const rowListUrl = datasource => sanitizeUrl(`/${datasource.name}`)
const generateTableBlock = table => { const generateTableBlock = datasource => {
const tableBlock = new Component("@budibase/standard-components/tableblock") const tableBlock = new Component("@budibase/standard-components/tableblock")
tableBlock tableBlock
.customProps({ .customProps({
title: table.name, title: datasource.name,
dataSource: { dataSource: datasource,
label: table.name,
name: table._id,
tableId: table._id,
type: "table",
},
sortOrder: "Ascending", sortOrder: "Ascending",
size: "spectrum--medium", size: "spectrum--medium",
paginate: true, paginate: true,
@ -36,14 +31,14 @@ const generateTableBlock = table => {
titleButtonText: "Create row", titleButtonText: "Create row",
titleButtonClickBehaviour: "new", titleButtonClickBehaviour: "new",
}) })
.instanceName(`${table.name} - Table block`) .instanceName(`${datasource.name} - Table block`)
return tableBlock return tableBlock
} }
const createScreen = table => { const createScreen = datasource => {
return new Screen() return new Screen()
.route(rowListUrl(table)) .route(rowListUrl(datasource))
.instanceName(`${table.name} - List`) .instanceName(`${datasource.name} - List`)
.addChild(generateTableBlock(table)) .addChild(generateTableBlock(datasource))
.json() .json()
} }

View File

@ -39,7 +39,7 @@
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte" import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
import { LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils } from "@budibase/frontend-core"
import { import {
getSchemaForTable, getSchemaForDatasourcePlus,
getEnvironmentBindings, getEnvironmentBindings,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
@ -67,7 +67,9 @@
$: table = tableId $: table = tableId
? $tables.list.find(table => table._id === inputData.tableId) ? $tables.list.find(table => table._id === inputData.tableId)
: { schema: {} } : { schema: {} }
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema $: schema = getSchemaForDatasourcePlus(tableId, {
searchableSchema: true,
}).schema
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000" $: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
$: isTrigger = block?.type === "TRIGGER" $: isTrigger = block?.type === "TRIGGER"
@ -158,7 +160,7 @@
// instead fetch the schema in the backend at runtime. // instead fetch the schema in the backend at runtime.
let schema let schema
if (e.detail?.tableId) { if (e.detail?.tableId) {
schema = getSchemaForTable(e.detail.tableId, { schema = getSchemaForDatasourcePlus(e.detail.tableId, {
searchableSchema: true, searchableSchema: true,
}).schema }).schema
} }

View File

@ -26,12 +26,14 @@
$: id = $tables.selected?._id $: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS $: isUsersTable = id === TableNames.USERS
$: isInternal = $tables.selected?.type !== "external" $: isInternal = $tables.selected?.type !== "external"
$: gridDatasource = {
$: datasource = $datasources.list.find(datasource => { type: "table",
tableId: id,
}
$: tableDatasource = $datasources.list.find(datasource => {
return datasource._id === $tables.selected?.sourceId return datasource._id === $tables.selected?.sourceId
}) })
$: relationshipsEnabled = relationshipSupport(tableDatasource)
$: relationshipsEnabled = relationshipSupport(datasource)
const relationshipSupport = datasource => { const relationshipSupport = datasource => {
const integration = $integrations[datasource?.source] const integration = $integrations[datasource?.source]
@ -54,12 +56,12 @@
<div class="wrapper"> <div class="wrapper">
<Grid <Grid
{API} {API}
tableId={id} datasource={gridDatasource}
allowAddRows={!isUsersTable} canAddRows={!isUsersTable}
allowDeleteRows={!isUsersTable} canDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false} showAvatars={false}
on:updatetable={handleGridTableUpdate} on:updatedatasource={handleGridTableUpdate}
> >
<svelte:fragment slot="filter"> <svelte:fragment slot="filter">
<GridFilterButton /> <GridFilterButton />
@ -72,9 +74,7 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="controls"> <svelte:fragment slot="controls">
{#if isInternal} <GridCreateViewButton />
<GridCreateViewButton />
{/if}
<GridManageAccessButton /> <GridManageAccessButton />
{#if relationshipsEnabled} {#if relationshipsEnabled}
<GridRelationshipButton /> <GridRelationshipButton />

View File

@ -0,0 +1,49 @@
<script>
import { viewsV2 } from "stores/backend"
import { Grid } from "@budibase/frontend-core"
import { API } from "api"
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
$: id = $viewsV2.selected?.id
$: datasource = {
type: "viewV2",
id,
tableId: $viewsV2.selected?.tableId,
}
const handleGridViewUpdate = async e => {
viewsV2.replaceView(id, e.detail)
}
</script>
<div class="wrapper">
<Grid
{API}
{datasource}
allowAddRows
allowDeleteRows
showAvatars={false}
on:updatedatasource={handleGridViewUpdate}
>
<svelte:fragment slot="filter">
<GridFilterButton />
</svelte:fragment>
<svelte:fragment slot="controls">
<GridCreateEditRowModal />
<GridManageAccessButton />
</svelte:fragment>
</Grid>
</div>
<style>
.wrapper {
flex: 1 1 auto;
margin: -28px -40px -40px -40px;
display: flex;
flex-direction: column;
background: var(--background);
overflow: hidden;
}
</style>

View File

@ -5,6 +5,7 @@
export let resourceId export let resourceId
export let disabled = false export let disabled = false
export let requiresLicence
let modal let modal
let resourcePermissions let resourcePermissions
@ -21,6 +22,7 @@
<Modal bind:this={modal}> <Modal bind:this={modal}>
<ManageAccessModal <ManageAccessModal
{resourceId} {resourceId}
{requiresLicence}
levels={$permissions} levels={$permissions}
permissions={resourcePermissions} permissions={resourcePermissions}
/> />

View File

@ -15,6 +15,7 @@
$: tempValue = filters || [] $: tempValue = filters || []
$: schemaFields = Object.values(schema || {}) $: schemaFields = Object.values(schema || {})
$: text = getText(filters) $: text = getText(filters)
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
const getText = filters => { const getText = filters => {
const count = filters?.filter(filter => filter.field)?.length const count = filters?.filter(filter => filter.field)?.length
@ -22,13 +23,7 @@
} }
</script> </script>
<ActionButton <ActionButton icon="Filter" quiet {disabled} on:click={modal.show} {selected}>
icon="Filter"
quiet
{disabled}
on:click={modal.show}
selected={tempValue?.length > 0}
>
{text} {text}
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -1,7 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Modal, ActionButton } from "@budibase/bbui" import { Modal, ActionButton } from "@budibase/bbui"
import CreateViewModal from "../../modals/CreateViewModal.svelte" import GridCreateViewModal from "../../modals/grid/GridCreateViewModal.svelte"
const { rows, columns } = getContext("grid") const { rows, columns } = getContext("grid")
@ -14,5 +14,5 @@
Add view Add view
</ActionButton> </ActionButton>
<Modal bind:this={modal}> <Modal bind:this={modal}>
<CreateViewModal /> <GridCreateViewModal />
</Modal> </Modal>

View File

@ -2,7 +2,7 @@
import ExportButton from "../ExportButton.svelte" import ExportButton from "../ExportButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { rows, columns, tableId, sort, selectedRows, filter } = const { rows, columns, datasource, sort, selectedRows, filter } =
getContext("grid") getContext("grid")
$: disabled = !$rows.length || !$columns.length $: disabled = !$rows.length || !$columns.length
@ -12,7 +12,7 @@
<span data-ignore-click-outside="true"> <span data-ignore-click-outside="true">
<ExportButton <ExportButton
{disabled} {disabled}
view={$tableId} view={$datasource.tableId}
filters={$filter} filters={$filter}
sorting={{ sorting={{
sortColumn: $sort.column, sortColumn: $sort.column,

View File

@ -2,22 +2,19 @@
import TableFilterButton from "../TableFilterButton.svelte" import TableFilterButton from "../TableFilterButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { columns, tableId, filter, table } = getContext("grid") const { columns, datasource, filter, definition } = getContext("grid")
// Wipe filter whenever table ID changes to avoid using stale filters
$: $tableId, filter.set([])
const onFilter = e => { const onFilter = e => {
filter.set(e.detail || []) filter.set(e.detail || [])
} }
</script> </script>
{#key $tableId} {#key $datasource}
<TableFilterButton <TableFilterButton
schema={$table?.schema} schema={$definition?.schema}
filters={$filter} filters={$filter}
on:change={onFilter} on:change={onFilter}
disabled={!$columns.length} disabled={!$columns.length}
tableId={$tableId} tableId={$datasource.tableId}
/> />
{/key} {/key}

View File

@ -4,12 +4,12 @@
export let disabled = false export let disabled = false
const { rows, tableId, table } = getContext("grid") const { rows, datasource, definition } = getContext("grid")
</script> </script>
<ImportButton <ImportButton
{disabled} {disabled}
tableId={$tableId} tableId={$datasource?.tableId}
tableType={$table?.type} tableType={$definition?.type}
on:importrows={rows.actions.refreshData} on:importrows={rows.actions.refreshData}
/> />

View File

@ -1,8 +1,29 @@
<script> <script>
import { licensing, admin } from "stores/portal"
import ManageAccessButton from "../ManageAccessButton.svelte" import ManageAccessButton from "../ManageAccessButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { tableId } = getContext("grid") const { datasource } = getContext("grid")
$: resourceId = getResourceID($datasource)
const getResourceID = datasource => {
if (!datasource) {
return null
}
return datasource.type === "table" ? datasource.tableId : datasource.id
}
var requiresLicence
$: {
if ($datasource.type === "viewV2" && !$licensing.isViewPermissionsEnabled) {
const requiredLicense = $admin?.cloud ? "Premium" : "Business"
requiresLicence = {
tier: requiredLicense,
message: `A ${requiredLicense} subscription is required to specify access level roles for this view.`,
}
}
}
</script> </script>
<ManageAccessButton resourceId={$tableId} /> <ManageAccessButton {resourceId} {requiresLicence} />

View File

@ -2,12 +2,12 @@
import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte" import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
const { table, rows } = getContext("grid") const { definition, rows } = getContext("grid")
</script> </script>
{#if $table} {#if $definition}
<ExistingRelationshipButton <ExistingRelationshipButton
table={$table} table={$definition}
on:updatecolumns={() => rows.actions.refreshData()} on:updatecolumns={() => rows.actions.refreshData()}
/> />
{/if} {/if}

View File

@ -1,38 +0,0 @@
<script>
import { Input, notifications, ModalContent } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { views as viewsStore } from "stores/backend"
import { tables } from "stores/backend"
let name
let field
$: views = $tables.list.flatMap(table => Object.keys(table.views || {}))
const saveView = async () => {
name = name?.trim()
if (views.includes(name)) {
notifications.error(`View exists with name ${name}`)
return
}
try {
await viewsStore.save({
name,
tableId: $tables.selected._id,
field,
})
notifications.success(`View ${name} created`)
$goto(`../../view/${encodeURIComponent(name)}`)
} catch (error) {
notifications.error("Error creating view")
}
}
</script>
<ModalContent
title="Create View"
confirmText="Create View"
onConfirm={saveView}
>
<Input label="View Name" thin bind:value={name} />
</ModalContent>

View File

@ -7,11 +7,14 @@
notifications, notifications,
Body, Body,
ModalContent, ModalContent,
Tags,
Tag,
} from "@budibase/bbui" } from "@budibase/bbui"
import { capitalise } from "helpers" import { capitalise } from "helpers"
export let resourceId export let resourceId
export let permissions export let permissions
export let requiresLicence
async function changePermission(level, role) { async function changePermission(level, role) {
try { try {
@ -30,22 +33,36 @@
} }
</script> </script>
<ModalContent title="Manage Access" showCancelButton={false} confirmText="Done"> <ModalContent showCancelButton={false} confirmText="Done">
<Body size="S">Specify the minimum access level role for this data.</Body> <span slot="header">
<div class="row"> Manage Access
<Label extraSmall grey>Level</Label> {#if requiresLicence}
<Label extraSmall grey>Role</Label> <span class="lock-tag">
{#each Object.keys(permissions) as level} <Tags>
<Input value={capitalise(level)} disabled /> <Tag icon="LockClosed">{requiresLicence.tier}</Tag>
<Select </Tags>
value={permissions[level]} </span>
on:change={e => changePermission(level, e.detail)} {/if}
options={$roles} </span>
getOptionLabel={x => x.name} {#if requiresLicence}
getOptionValue={x => x._id} <Body size="S">{requiresLicence.message}</Body>
/> {:else}
{/each} <Body size="S">Specify the minimum access level role for this data.</Body>
</div> <div class="row">
<Label extraSmall grey>Level</Label>
<Label extraSmall grey>Role</Label>
{#each Object.keys(permissions) as level}
<Input value={capitalise(level)} disabled />
<Select
value={permissions[level]}
on:change={e => changePermission(level, e.detail)}
options={$roles}
getOptionLabel={x => x.name}
getOptionValue={x => x._id}
/>
{/each}
</div>
{/if}
</ModalContent> </ModalContent>
<style> <style>
@ -54,4 +71,8 @@
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-gap: var(--spacing-s); grid-gap: var(--spacing-s);
} }
.lock-tag {
padding-left: var(--spacing-s);
}
</style> </style>

View File

@ -2,7 +2,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte" import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
const { rows } = getContext("grid") const { datasource } = getContext("grid")
</script> </script>
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} /> <CreateEditColumn on:updatecolumns={datasource.actions.refreshDefinition} />

View File

@ -0,0 +1,60 @@
<script>
import { getContext } from "svelte"
import { Input, notifications, ModalContent } from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { viewsV2 } from "stores/backend"
const { filter, sort, definition } = getContext("grid")
let name
$: views = Object.keys($definition?.views || {}).map(x => x.toLowerCase())
$: nameExists = views.includes(name?.trim().toLowerCase())
const enrichSchema = schema => {
// We need to sure that "visible" is set to true for any fields which have
// not yet been saved with grid metadata attached
const cloned = { ...schema }
Object.entries(cloned).forEach(([field, fieldSchema]) => {
if (fieldSchema.visible == null) {
cloned[field] = { ...cloned[field], visible: true }
}
})
return cloned
}
const saveView = async () => {
name = name?.trim()
try {
const newView = await viewsV2.create({
name,
tableId: $definition._id,
query: $filter,
sort: {
field: $sort.column,
order: $sort.order,
},
schema: enrichSchema($definition.schema),
primaryDisplay: $definition.primaryDisplay,
})
notifications.success(`View ${name} created`)
$goto(`../../view/v2/${newView.id}`)
} catch (error) {
notifications.error("Error creating view")
}
}
</script>
<ModalContent
title="Create View"
confirmText="Create View"
onConfirm={saveView}
disabled={nameExists}
>
<Input
label="View Name"
thin
bind:value={name}
error={nameExists ? "A view already exists with that name" : null}
/>
</ModalContent>

View File

@ -1,7 +1,14 @@
<script> <script>
import { goto, isActive, params } from "@roxi/routify" import { goto, isActive, params } from "@roxi/routify"
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend" import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
import { database, datasources, queries, tables, views } from "stores/backend" import {
database,
datasources,
queries,
tables,
views,
viewsV2,
} from "stores/backend"
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte" import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
import EditQueryPopover from "./popovers/EditQueryPopover.svelte" import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
import NavItem from "components/common/NavItem.svelte" import NavItem from "components/common/NavItem.svelte"
@ -24,6 +31,7 @@
$tables, $tables,
$queries, $queries,
$views, $views,
$viewsV2,
openDataSources openDataSources
) )
$: openDataSource = enrichedDataSources.find(x => x.open) $: openDataSource = enrichedDataSources.find(x => x.open)
@ -41,6 +49,7 @@
tables, tables,
queries, queries,
views, views,
viewsV2,
openDataSources openDataSources
) => { ) => {
if (!datasources?.list?.length) { if (!datasources?.list?.length) {
@ -57,7 +66,8 @@
isActive, isActive,
tables, tables,
queries, queries,
views views,
viewsV2
) )
const onlySource = datasources.list.length === 1 const onlySource = datasources.list.length === 1
return { return {
@ -106,7 +116,8 @@
isActive, isActive,
tables, tables,
queries, queries,
views views,
viewsV2
) => { ) => {
// Check for being on a datasource page // Check for being on a datasource page
if (params.datasourceId === datasource._id) { if (params.datasourceId === datasource._id) {
@ -152,10 +163,16 @@
// Check for a matching view // Check for a matching view
const selectedView = views.selected?.name const selectedView = views.selected?.name
const table = options.find(table => { const viewTable = options.find(table => {
return table.views?.[selectedView] != null return table.views?.[selectedView] != null
}) })
return table != null if (viewTable) {
return true
}
// Check for a matching viewV2
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
return viewV2Table != null
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<script> <script>
import { tables, views, database } from "stores/backend" import { tables, views, viewsV2, database } from "stores/backend"
import { TableNames } from "constants" import { TableNames } from "constants"
import EditTablePopover from "./popovers/EditTablePopover.svelte" import EditTablePopover from "./popovers/EditTablePopover.svelte"
import EditViewPopover from "./popovers/EditViewPopover.svelte" import EditViewPopover from "./popovers/EditViewPopover.svelte"
@ -7,9 +7,6 @@
import { goto, isActive } from "@roxi/routify" import { goto, isActive } from "@roxi/routify"
import { userSelectedResourceMap } from "builderStore" import { userSelectedResourceMap } from "builderStore"
const alphabetical = (a, b) =>
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
export let sourceId export let sourceId
export let selectTable export let selectTable
@ -18,6 +15,17 @@
table => table.sourceId === sourceId && table._id !== TableNames.USERS table => table.sourceId === sourceId && table._id !== TableNames.USERS
) )
.sort(alphabetical) .sort(alphabetical)
const alphabetical = (a, b) => {
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
}
const isViewActive = (view, isActive, views, viewsV2) => {
return (
(isActive("./view/v1") && views.selected?.name === view.name) ||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
)
}
</script> </script>
{#if $database?._id} {#if $database?._id}
@ -37,18 +45,23 @@
<EditTablePopover {table} /> <EditTablePopover {table} />
{/if} {/if}
</NavItem> </NavItem>
{#each [...Object.keys(table.views || {})].sort() as viewName, idx (idx)} {#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
<NavItem <NavItem
indentLevel={2} indentLevel={2}
icon="Remove" icon="Remove"
text={viewName} text={name}
selected={$isActive("./view") && $views.selected?.name === viewName} selected={isViewActive(view, $isActive, $views, $viewsV2)}
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)} on:click={() => {
selectedBy={$userSelectedResourceMap[viewName]} if (view.version === 2) {
$goto(`./view/v2/${view.id}`)
} else {
$goto(`./view/v1/${encodeURIComponent(name)}`)
}
}}
selectedBy={$userSelectedResourceMap[name] ||
$userSelectedResourceMap[view.id]}
> >
<EditViewPopover <EditViewPopover {view} />
view={{ name: viewName, ...table.views[viewName] }}
/>
</NavItem> </NavItem>
{/each} {/each}
{/each} {/each}

View File

@ -35,7 +35,7 @@
screen => screen.autoTableId === table._id screen => screen.autoTableId === table._id
) )
willBeDeleted = ["All table data"].concat( willBeDeleted = ["All table data"].concat(
templateScreens.map(screen => `Screen ${screen.props._instanceName}`) templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`)
) )
confirmDeleteDialog.show() confirmDeleteDialog.show()
} }
@ -44,7 +44,10 @@
const isSelected = $params.tableId === table._id const isSelected = $params.tableId === table._id
try { try {
await tables.delete(table) await tables.delete(table)
await store.actions.screens.delete(templateScreens) // Screens need deleted one at a time because of undo/redo
for (let screen of templateScreens) {
await store.actions.screens.delete(screen)
}
if (table.type === "external") { if (table.type === "external") {
await datasources.fetch() await datasources.fetch()
} }

View File

@ -1,6 +1,5 @@
<script> <script>
import { goto, params } from "@roxi/routify" import { views, viewsV2 } from "stores/backend"
import { views } from "stores/backend"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { import {
@ -24,23 +23,29 @@
const updatedView = cloneDeep(view) const updatedView = cloneDeep(view)
updatedView.name = updatedName updatedView.name = updatedName
await views.save({ if (view.version === 2) {
originalName, await viewsV2.save({
...updatedView, originalName,
}) ...updatedView,
})
} else {
await views.save({
originalName,
...updatedView,
})
}
notifications.success("View renamed successfully") notifications.success("View renamed successfully")
} }
async function deleteView() { async function deleteView() {
try { try {
const isSelected = if (view.version === 2) {
decodeURIComponent($params.viewName) === $views.selectedViewName await viewsV2.delete(view)
const id = view.tableId } else {
await views.delete(view) await views.delete(view)
notifications.success("View deleted")
if (isSelected) {
$goto(`./table/${id}`)
} }
notifications.success("View deleted")
} catch (error) { } catch (error) {
notifications.error("Error deleting view") notifications.error("Error deleting view")
} }

View File

@ -109,7 +109,13 @@
type: "View", type: "View",
name: view.name, name: view.name,
icon: "Remove", icon: "Remove",
action: () => $goto(`./data/view/${view.name}`), action: () => {
if (view.version === 2) {
$goto(`./data/view/v2/${view.id}`)
} else {
$goto(`./data/view/${view.name}`)
}
},
})) ?? []), })) ?? []),
...($queries?.list?.map(query => ({ ...($queries?.list?.map(query => ({
type: "Query", type: "Query",

View File

@ -1,12 +1,20 @@
<script> <script>
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui" import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
import { tables } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings = [] export let bindings = []
$: tableOptions = $tables.list || [] $: tableOptions = $tables.list.map(table => ({
label: table.name,
resourceId: table._id,
}))
$: viewOptions = $viewsV2.list.map(view => ({
label: view.name,
resourceId: view.id,
}))
$: options = [...(tableOptions || []), ...(viewOptions || [])]
</script> </script>
<div class="root"> <div class="root">
@ -15,9 +23,9 @@
<Label>Table</Label> <Label>Table</Label>
<Select <Select
bind:value={parameters.tableId} bind:value={parameters.tableId}
options={tableOptions} {options}
getOptionLabel={table => table.name} getOptionLabel={x => x.label}
getOptionValue={table => table._id} getOptionValue={x => x.resourceId}
/> />
<Label small>Row IDs</Label> <Label small>Row IDs</Label>

View File

@ -1,10 +1,10 @@
<script> <script>
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import {
getContextProviderComponents, getContextProviderComponents,
getSchemaForTable, getSchemaForDatasourcePlus,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
@ -23,7 +23,15 @@
) )
$: providerOptions = getProviderOptions(formComponents, schemaComponents) $: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId) $: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
$: tableOptions = $tables.list || [] $: tableOptions = $tables.list.map(table => ({
label: table.name,
resourceId: table._id,
}))
$: viewOptions = $viewsV2.list.map(view => ({
label: view.name,
resourceId: view.id,
}))
$: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition // Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => { const extractComponentContext = (component, contextType) => {
@ -60,7 +68,7 @@
} }
const getSchemaFields = (asset, tableId) => { const getSchemaFields = (asset, tableId) => {
const { schema } = getSchemaForTable(tableId) const { schema } = getSchemaForDatasourcePlus(tableId)
delete schema._id delete schema._id
delete schema._rev delete schema._rev
return Object.values(schema || {}) return Object.values(schema || {})
@ -89,9 +97,9 @@
<Label small>Duplicate to Table</Label> <Label small>Duplicate to Table</Label>
<Select <Select
bind:value={parameters.tableId} bind:value={parameters.tableId}
options={tableOptions} {options}
getOptionLabel={option => option.name} getOptionLabel={option => option.label}
getOptionValue={option => option._id} getOptionValue={option => option.resourceId}
/> />
<Label small /> <Label small />

View File

@ -1,21 +1,29 @@
<script> <script>
import { Select, Label } from "@budibase/bbui" import { Select, Label } from "@budibase/bbui"
import { tables } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
export let parameters export let parameters
export let bindings = [] export let bindings = []
$: tableOptions = $tables.list || [] $: tableOptions = $tables.list.map(table => ({
label: table.name,
resourceId: table._id,
}))
$: viewOptions = $viewsV2.list.map(view => ({
label: view.name,
resourceId: view.id,
}))
$: options = [...(tableOptions || []), ...(viewOptions || [])]
</script> </script>
<div class="root"> <div class="root">
<Label>Table</Label> <Label>Table</Label>
<Select <Select
bind:value={parameters.tableId} bind:value={parameters.tableId}
options={tableOptions} {options}
getOptionLabel={table => table.name} getOptionLabel={table => table.label}
getOptionValue={table => table._id} getOptionValue={table => table.resourceId}
/> />
<Label small>Row ID</Label> <Label small>Row ID</Label>

View File

@ -1,10 +1,10 @@
<script> <script>
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui" import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { tables } from "stores/backend" import { tables, viewsV2 } from "stores/backend"
import { import {
getContextProviderComponents, getContextProviderComponents,
getSchemaForTable, getSchemaForDatasourcePlus,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import SaveFields from "./SaveFields.svelte" import SaveFields from "./SaveFields.svelte"
@ -24,8 +24,16 @@
"schema" "schema"
) )
$: providerOptions = getProviderOptions(formComponents, schemaComponents) $: providerOptions = getProviderOptions(formComponents, schemaComponents)
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId) $: schemaFields = getSchemaFields(parameters?.tableId)
$: tableOptions = $tables.list || [] $: tableOptions = $tables.list.map(table => ({
label: table.name,
resourceId: table._id,
}))
$: viewOptions = $viewsV2.list.map(view => ({
label: view.name,
resourceId: view.id,
}))
$: options = [...(tableOptions || []), ...(viewOptions || [])]
// Gets a context definition of a certain type from a component definition // Gets a context definition of a certain type from a component definition
const extractComponentContext = (component, contextType) => { const extractComponentContext = (component, contextType) => {
@ -61,8 +69,8 @@
}) })
} }
const getSchemaFields = (asset, tableId) => { const getSchemaFields = resourceId => {
const { schema } = getSchemaForTable(tableId) const { schema } = getSchemaForDatasourcePlus(resourceId)
return Object.values(schema || {}) return Object.values(schema || {})
} }
@ -89,9 +97,9 @@
<Label small>Table</Label> <Label small>Table</Label>
<Select <Select
bind:value={parameters.tableId} bind:value={parameters.tableId}
options={tableOptions} {options}
getOptionLabel={option => option.name} getOptionLabel={option => option.label}
getOptionValue={option => option._id} getOptionValue={option => option.resourceId}
/> />
<Label small /> <Label small />

View File

@ -15,6 +15,8 @@
import { import {
tables as tablesStore, tables as tablesStore,
queries as queriesStore, queries as queriesStore,
viewsV2 as viewsV2Store,
views as viewsStore,
} from "stores/backend" } from "stores/backend"
import { datasources, integrations } from "stores/backend" import { datasources, integrations } from "stores/backend"
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte" import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
@ -39,15 +41,17 @@
tableId: m._id, tableId: m._id,
type: "table", type: "table",
})) }))
$: views = $tablesStore.list.reduce((acc, cur) => { $: viewsV1 = $viewsStore.list.map(view => ({
let viewsArr = Object.entries(cur.views || {}).map(([key, value]) => ({ ...view,
label: key, label: view.name,
name: key, type: "view",
...value, }))
type: "view", $: viewsV2 = $viewsV2Store.list.map(view => ({
})) ...view,
return [...acc, ...viewsArr] label: view.name,
}, []) type: "viewV2",
}))
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
$: queries = $queriesStore.list $: queries = $queriesStore.list
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable) .filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
.map(query => ({ .map(query => ({

View File

@ -1,28 +1,47 @@
<script> <script>
import { Select } from "@budibase/bbui" import { Select } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, onMount } from "svelte"
import { tables as tablesStore } from "stores/backend" import { tables as tablesStore, viewsV2 } from "stores/backend"
export let value export let value
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: tables = $tablesStore.list.map(m => ({ $: tables = $tablesStore.list.map(table => ({
label: m.name, ...table,
tableId: m._id,
type: "table", type: "table",
label: table.name,
resourceId: table._id,
})) }))
$: views = $viewsV2.list.map(view => ({
...view,
type: "viewV2",
label: view.name,
resourceId: view.id,
}))
$: options = [...(tables || []), ...(views || [])]
const onChange = e => { const onChange = e => {
const dataSource = tables?.find(x => x.tableId === e.detail) dispatch(
dispatch("change", dataSource) "change",
options.find(x => x.resourceId === e.detail)
)
} }
onMount(() => {
// Migrate old values before "resourceId" existed
if (value && !value.resourceId) {
const view = views.find(x => x.resourceId === value.id)
const table = tables.find(x => x.resourceId === value._id)
dispatch("change", view || table)
}
})
</script> </script>
<Select <Select
on:change={onChange} on:change={onChange}
value={value?.tableId} value={value?.resourceId}
options={tables} {options}
getOptionValue={x => x.tableId} getOptionValue={x => x.resourceId}
getOptionLabel={x => x.label} getOptionLabel={x => x.label}
/> />

View File

@ -1,5 +1,5 @@
<script> <script>
import TableDataTable from "components/backend/DataTable/DataTable.svelte" import TableDataTable from "components/backend/DataTable/TableDataTable.svelte"
import { tables, database } from "stores/backend" import { tables, database } from "stores/backend"
import { Banner } from "@budibase/bbui" import { Banner } from "@budibase/bbui"

View File

@ -1,14 +1,17 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { views } from "stores/backend" import { views, viewsV2 } from "stores/backend"
import { redirect } from "@roxi/routify" import { redirect } from "@roxi/routify"
onMount(async () => { onMount(async () => {
const { list, selected } = $views if ($viewsV2.selected) {
if (selected) { $redirect(`./v2/${$viewsV2.selected.id}`)
$redirect(`./${encodeURIComponent(selected?.name)}`) } else if ($viewsV2.list?.length) {
} else if (list?.length) { $redirect(`./v2/${$viewsV2.list[0].id}`)
$redirect(`./${encodeURIComponent(list[0].name)}`) } else if ($views.selected) {
$redirect(`./${encodeURIComponent($views.selected?.name)}`)
} else if ($views.list?.length) {
$redirect(`./${encodeURIComponent($views.list[0].name)}`)
} else { } else {
$redirect("../") $redirect("../")
} }

View File

@ -5,15 +5,15 @@
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
$: viewName = $views.selectedViewName $: name = $views.selectedViewName
$: store.actions.websocket.selectResource(viewName) $: store.actions.websocket.selectResource(name)
const stopSyncing = syncURLToState({ const stopSyncing = syncURLToState({
urlParam: "viewName", urlParam: "viewName",
stateKey: "selectedViewName", stateKey: "selectedViewName",
validate: name => $views.list?.some(view => view.name === name), validate: name => $views.list?.some(view => view.name === name),
update: views.select, update: views.select,
fallbackUrl: "../", fallbackUrl: "../../",
store: views, store: views,
routify, routify,
decode: decodeURIComponent, decode: decodeURIComponent,

View File

@ -0,0 +1,5 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>

View File

@ -0,0 +1,25 @@
<script>
import { viewsV2 } from "stores/backend"
import { syncURLToState } from "helpers/urlStateSync"
import * as routify from "@roxi/routify"
import { onDestroy } from "svelte"
import { store } from "builderStore"
$: id = $viewsV2.selectedViewId
$: store.actions.websocket.selectResource(id)
const stopSyncing = syncURLToState({
urlParam: "viewId",
stateKey: "selectedViewId",
validate: id => $viewsV2.list?.some(view => view.id === id),
update: viewsV2.select,
fallbackUrl: "../../",
store: viewsV2,
routify,
decode: decodeURIComponent,
})
onDestroy(stopSyncing)
</script>
<slot />

View File

@ -0,0 +1,5 @@
<script>
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
</script>
<ViewV2DataTable />

View File

@ -0,0 +1,5 @@
<script>
import { redirect } from "@roxi/routify"
$redirect("../")
</script>

View File

@ -297,8 +297,12 @@
width: 100%; width: 100%;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
transition: background 130ms ease-out;
} }
.divider:hover { .divider:hover {
cursor: row-resize; cursor: row-resize;
} }
.divider:hover:after {
background: var(--spectrum-global-color-gray-300);
}
</style> </style>

View File

@ -131,8 +131,7 @@
const completeDatasourceScreenCreation = async () => { const completeDatasourceScreenCreation = async () => {
const screens = selectedTemplates.map(template => { const screens = selectedTemplates.map(template => {
let screenTemplate = template.create() let screenTemplate = template.create()
screenTemplate.datasource = template.datasource screenTemplate.autoTableId = template.resourceId
screenTemplate.autoTableId = template.table
return screenTemplate return screenTemplate
}) })
await createScreens({ screens, screenAccessRole }) await createScreens({ screens, screenAccessRole })
@ -176,10 +175,10 @@
} }
</script> </script>
<Modal bind:this={datasourceModal}> <Modal bind:this={datasourceModal} autoFocus={false}>
<DatasourceModal <DatasourceModal
onConfirm={confirmScreenDatasources} onConfirm={confirmScreenDatasources}
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]} initialScreens={!selectedTemplates ? [] : [...selectedTemplates]}
/> />
</Modal> </Modal>

View File

@ -1,41 +1,30 @@
<script> <script>
import { store } from "builderStore" import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
import { import { datasources } from "stores/backend"
ModalContent,
Layout,
notifications,
Icon,
Body,
} from "@budibase/bbui"
import { tables, datasources } from "stores/backend"
import getTemplates from "builderStore/store/screenTemplates"
import ICONS from "components/backend/DatasourceNavigator/icons" import ICONS from "components/backend/DatasourceNavigator/icons"
import { IntegrationNames } from "constants" import { IntegrationNames } from "constants"
import { onMount } from "svelte" import { onMount } from "svelte"
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
export let onCancel export let onCancel
export let onConfirm export let onConfirm
export let initalScreens = [] export let initialScreens = []
let selectedScreens = [...initalScreens] let selectedScreens = [...initialScreens]
const toggleScreenSelection = (table, datasource) => { $: filteredSources = $datasources.list?.filter(datasource => {
if (selectedScreens.find(s => s.table === table._id)) { return datasource.source !== IntegrationNames.REST && datasource["entities"]
})
const toggleSelection = datasource => {
const { resourceId } = datasource
if (selectedScreens.find(s => s.resourceId === resourceId)) {
selectedScreens = selectedScreens.filter( selectedScreens = selectedScreens.filter(
screen => screen.table !== table._id screen => screen.resourceId !== resourceId
) )
} else { } else {
let partialTemplates = getTemplates($store, $tables.list).reduce( selectedScreens = [...selectedScreens, rowListScreen([datasource])[0]]
(acc, template) => {
if (template.table === table._id) {
template.datasource = datasource.name
acc.push(template)
}
return acc
},
[]
)
selectedScreens = [...partialTemplates, ...selectedScreens]
} }
} }
@ -45,18 +34,6 @@
}) })
} }
$: filteredSources = Array.isArray($datasources.list)
? $datasources.list.reduce((acc, datasource) => {
if (
datasource.source !== IntegrationNames.REST &&
datasource["entities"]
) {
acc.push(datasource)
}
return acc
}, [])
: []
onMount(async () => { onMount(async () => {
try { try {
await datasources.fetch() await datasources.fetch()
@ -81,6 +58,9 @@
</Body> </Body>
<Layout noPadding gap="S"> <Layout noPadding gap="S">
{#each filteredSources as datasource} {#each filteredSources as datasource}
{@const entities = Array.isArray(datasource.entities)
? datasource.entities
: Object.values(datasource.entities || {})}
<div class="data-source-wrap"> <div class="data-source-wrap">
<div class="data-source-header"> <div class="data-source-header">
<svelte:component <svelte:component
@ -90,64 +70,51 @@
/> />
<div class="data-source-name">{datasource.name}</div> <div class="data-source-name">{datasource.name}</div>
</div> </div>
{#if Array.isArray(datasource.entities)} <!-- List all tables -->
{#each datasource.entities.filter(table => table._id !== "ta_users") as table} {#each entities.filter(table => table._id !== "ta_users") as table}
<div {@const views = Object.values(table.views || {}).filter(
class="data-source-entry" view => view.version === 2
class:selected={selectedScreens.find( )}
x => x.table === table._id {@const datasource = {
)} ...table,
on:click={() => toggleScreenSelection(table, datasource)} // Legacy properties
> tableId: table._id,
<svg label: table.name,
width="16px" // New consistent properties
height="16px" resourceId: table._id,
class="spectrum-Icon" name: table.name,
style="color: white" type: "table",
focusable="false" }}
> {@const selected = selectedScreens.find(
<use xlink:href="#spectrum-icon-18-Table" /> screen => screen.resourceId === datasource.resourceId
</svg> )}
{table.name} <DatasourceTemplateRow
{#if selectedScreens.find(x => x.table === table._id)} on:click={() => toggleSelection(datasource)}
<span class="data-source-check"> {selected}
<Icon size="S" name="CheckmarkCircle" /> {datasource}
</span> />
{/if}
</div> <!-- List all views inside this table -->
{#each views as view}
{@const datasource = {
...view,
// Legacy properties
label: view.name,
// New consistent properties
resourceId: view.id,
name: view.name,
type: "viewV2",
}}
{@const selected = selectedScreens.find(
x => x.resourceId === datasource.resourceId
)}
<DatasourceTemplateRow
on:click={() => toggleSelection(datasource)}
{selected}
{datasource}
/>
{/each} {/each}
{/if} {/each}
{#if datasource["entities"] && !Array.isArray(datasource.entities)}
{#each Object.keys(datasource.entities).filter(table => table._id !== "ta_users") as table_key}
<div
class="data-source-entry"
class:selected={selectedScreens.find(
x => x.table === datasource.entities[table_key]._id
)}
on:click={() =>
toggleScreenSelection(
datasource.entities[table_key],
datasource
)}
>
<svg
width="16px"
height="16px"
class="spectrum-Icon"
style="color: white"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
{datasource.entities[table_key].name}
{#if selectedScreens.find(x => x.table === datasource.entities[table_key]._id)}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
{/each}
{/if}
</div> </div>
{/each} {/each}
</Layout> </Layout>
@ -160,42 +127,10 @@
display: grid; display: grid;
grid-gap: var(--spacing-s); grid-gap: var(--spacing-s);
} }
.data-source-header { .data-source-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--spacing-m); gap: var(--spacing-m);
padding-bottom: var(--spacing-xs); padding-bottom: var(--spacing-xs);
} }
.data-source-entry {
cursor: pointer;
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
border-width: 1px;
display: flex;
align-items: center;
}
.data-source-entry:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.data-source-entry .data-source-check {
margin-left: auto;
}
.data-source-entry :global(.spectrum-Icon) {
min-width: 16px;
}
.data-source-entry .data-source-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
display: block;
}
</style> </style>

View File

@ -0,0 +1,42 @@
<script>
import { Icon } from "@budibase/bbui"
export let datasource
export let selected = false
$: icon = datasource.type === "viewV2" ? "Remove" : "Table"
</script>
<div class="data-source-entry" class:selected on:click>
<Icon name={icon} color="var(--spectrum-global-color-gray-600)" />
{datasource.name}
{#if selected}
<span class="data-source-check">
<Icon size="S" name="CheckmarkCircle" />
</span>
{/if}
</div>
<style>
.data-source-entry {
cursor: pointer;
grid-gap: var(--spacing-m);
padding: var(--spectrum-alias-item-padding-s);
background: var(--spectrum-alias-background-color-secondary);
transition: 0.3s all;
border: 1px solid var(--spectrum-global-color-gray-300);
border-radius: 4px;
display: flex;
align-items: center;
}
.data-source-entry:hover,
.selected {
background: var(--spectrum-alias-background-color-tertiary);
}
.data-source-check {
margin-left: auto;
}
.data-source-check :global(.spectrum-Icon) {
color: var(--spectrum-global-color-green-600);
}
</style>

View File

@ -1,6 +1,7 @@
export { database } from "./database" export { database } from "./database"
export { tables } from "./tables" export { tables } from "./tables"
export { views } from "./views" export { views } from "./views"
export { viewsV2 } from "./viewsV2"
export { permissions } from "./permissions" export { permissions } from "./permissions"
export { roles } from "./roles" export { roles } from "./roles"
export { datasources, ImportTableError } from "./datasources" export { datasources, ImportTableError } from "./datasources"

View File

@ -9,7 +9,10 @@ export function createViewsStore() {
const derivedStore = derived([store, tables], ([$store, $tables]) => { const derivedStore = derived([store, tables], ([$store, $tables]) => {
let list = [] let list = []
$tables.list?.forEach(table => { $tables.list?.forEach(table => {
list = list.concat(Object.values(table?.views || {})) const views = Object.values(table?.views || {}).filter(view => {
return view.version !== 2
})
list = list.concat(views)
}) })
return { return {
...$store, ...$store,

View File

@ -0,0 +1,102 @@
import { writable, derived, get } from "svelte/store"
import { tables } from "./"
import { API } from "api"
export function createViewsV2Store() {
const store = writable({
selectedViewId: null,
})
const derivedStore = derived([store, tables], ([$store, $tables]) => {
let list = []
$tables.list?.forEach(table => {
const views = Object.values(table?.views || {}).filter(view => {
return view.version === 2
})
list = list.concat(views)
})
return {
...$store,
list,
selected: list.find(view => view.id === $store.selectedViewId),
}
})
const select = id => {
store.update(state => ({
...state,
selectedViewId: id,
}))
}
const deleteView = async view => {
await API.viewV2.delete(view.id)
replaceView(view.id, null)
}
const create = async view => {
const savedViewResponse = await API.viewV2.create(view)
const savedView = savedViewResponse.data
replaceView(savedView.id, savedView)
return savedView
}
const save = async view => {
const res = await API.viewV2.update(view)
const savedView = res?.data
replaceView(view.id, savedView)
}
// Handles external updates of tables
const replaceView = (viewId, view) => {
if (!viewId) {
return
}
const existingView = get(derivedStore).list.find(view => view.id === viewId)
const tableIndex = get(tables).list.findIndex(table => {
return table._id === view?.tableId || table._id === existingView?.tableId
})
if (tableIndex === -1) {
return
}
// Handle deletion
if (!view) {
tables.update(state => {
delete state.list[tableIndex].views[existingView.name]
return state
})
return
}
// Add new view
if (!existingView) {
tables.update(state => {
state.list[tableIndex].views[view.name] = view
return state
})
}
// Update existing view
else {
tables.update(state => {
// Remove old view
delete state.list[tableIndex].views[existingView.name]
// Add new view
state.list[tableIndex].views[view.name] = view
return state
})
}
}
return {
subscribe: derivedStore.subscribe,
select,
delete: deleteView,
create,
save,
replaceView,
}
}
export const viewsV2 = createViewsV2Store()

View File

@ -128,6 +128,10 @@ export const createLicensingStore = () => {
const perAppBuildersEnabled = license.features.includes( const perAppBuildersEnabled = license.features.includes(
Constants.Features.APP_BUILDERS Constants.Features.APP_BUILDERS
) )
const isViewPermissionsEnabled = license.features.includes(
Constants.Features.VIEW_PERMISSIONS
)
store.update(state => { store.update(state => {
return { return {
...state, ...state,
@ -143,6 +147,7 @@ export const createLicensingStore = () => {
auditLogsEnabled, auditLogsEnabled,
enforceableSSO, enforceableSSO,
syncAutomationsEnabled, syncAutomationsEnabled,
isViewPermissionsEnabled,
perAppBuildersEnabled, perAppBuildersEnabled,
} }
}) })

View File

@ -4622,14 +4622,15 @@
"type": "field/sortable", "type": "field/sortable",
"label": "Sort by", "label": "Sort by",
"key": "sortColumn", "key": "sortColumn",
"placeholder": "None" "placeholder": "Default"
}, },
{ {
"type": "select", "type": "select",
"label": "Sort order", "label": "Sort order",
"key": "sortOrder", "key": "sortOrder",
"options": ["Ascending", "Descending"], "options": ["Ascending", "Descending"],
"defaultValue": "Ascending" "defaultValue": "Ascending",
"dependsOn": "sortColumn"
}, },
{ {
"type": "select", "type": "select",
@ -5271,7 +5272,7 @@
}, },
{ {
"type": "table", "type": "table",
"label": "Table", "label": "Data",
"key": "dataSource" "key": "dataSource"
}, },
{ {
@ -5443,7 +5444,7 @@
"settings": [ "settings": [
{ {
"type": "table", "type": "table",
"label": "Table", "label": "Data",
"key": "dataSource", "key": "dataSource",
"required": true "required": true
}, },
@ -5534,7 +5535,7 @@
"settings": [ "settings": [
{ {
"type": "table", "type": "table",
"label": "Table", "label": "Data",
"key": "table", "key": "table",
"required": true "required": true
}, },
@ -5560,7 +5561,8 @@
"label": "Sort order", "label": "Sort order",
"key": "initialSortOrder", "key": "initialSortOrder",
"options": ["Ascending", "Descending"], "options": ["Ascending", "Descending"],
"defaultValue": "Ascending" "defaultValue": "Ascending",
"dependsOn": "initialSortColumn"
}, },
{ {
"type": "select", "type": "select",

View File

@ -272,12 +272,36 @@
return missing return missing
}) })
// Run any migrations
runMigrations(instance, settingsDefinition)
// Force an initial enrichment of the new settings // Force an initial enrichment of the new settings
enrichComponentSettings(get(context), settingsDefinitionMap, { enrichComponentSettings(get(context), settingsDefinitionMap, {
force: true, force: true,
}) })
} }
const runMigrations = (instance, settingsDefinition) => {
settingsDefinition.forEach(setting => {
// Migrate "table" settings to ensure they have a type and resource ID
if (setting.type === "table") {
const val = instance[setting.key]
if (val) {
if (!val.type) {
val.type = "table"
}
if (!val.resourceId) {
if (val.type === "viewV2") {
val.resourceId = val.id
} else {
val.resourceId = val.tableId
}
}
}
}
})
}
const getSettingsDefinitionMap = settingsDefinition => { const getSettingsDefinitionMap = settingsDefinition => {
let map = {} let map = {}
settingsDefinition?.forEach(setting => { settingsDefinition?.forEach(setting => {

View File

@ -29,6 +29,9 @@
paginate, paginate,
}) })
// Sanitize schema to remove hidden fields
$: schema = sanitizeSchema($fetch.schema)
// Build our action context // Build our action context
$: actions = [ $: actions = [
{ {
@ -66,7 +69,7 @@
rows: $fetch.rows, rows: $fetch.rows,
info: $fetch.info, info: $fetch.info,
datasource: dataSource || {}, datasource: dataSource || {},
schema: $fetch.schema, schema,
rowsLength: $fetch.rows.length, rowsLength: $fetch.rows.length,
// Undocumented properties. These aren't supposed to be used in builder // Undocumented properties. These aren't supposed to be used in builder
@ -94,6 +97,19 @@
}) })
} }
const sanitizeSchema = schema => {
if (!schema) {
return schema
}
let cloned = { ...schema }
Object.entries(cloned).forEach(([field, fieldSchema]) => {
if (fieldSchema.visible === false) {
delete cloned[field]
}
})
return cloned
}
const addQueryExtension = (key, extension) => { const addQueryExtension = (key, extension) => {
if (!key || !extension) { if (!key || !extension) {
return return

View File

@ -38,11 +38,8 @@
class:in-builder={$builderStore.inBuilder} class:in-builder={$builderStore.inBuilder}
> >
<Grid <Grid
tableId={table?.tableId} datasource={table}
{API} {API}
{allowAddRows}
{allowEditRows}
{allowDeleteRows}
{stripeRows} {stripeRows}
{initialFilter} {initialFilter}
{initialSortColumn} {initialSortColumn}
@ -50,9 +47,13 @@
{fixedRowHeight} {fixedRowHeight}
{columnWhitelist} {columnWhitelist}
{schemaOverrides} {schemaOverrides}
canAddRows={allowAddRows}
canEditRows={allowEditRows}
canDeleteRows={allowDeleteRows}
canEditColumns={false}
canExpandRows={false}
canSaveSchema={false}
showControls={false} showControls={false}
allowExpandRows={false}
allowSchemaChanges={false}
notifySuccess={notificationStore.actions.success} notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error} notifyError={notificationStore.actions.error}
/> />

View File

@ -47,7 +47,7 @@
// Accommodate old config to ensure delete button does not reappear // Accommodate old config to ensure delete button does not reappear
$: deleteLabel = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel $: deleteLabel = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel
$: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2"
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: enrichSearchColumns(searchColumns, schema).then( $: enrichSearchColumns(searchColumns, schema).then(
val => (enrichedSearchColumns = val) val => (enrichedSearchColumns = val)
@ -56,7 +56,7 @@
$: editTitle = getEditTitle(detailsFormBlockId, primaryDisplay) $: editTitle = getEditTitle(detailsFormBlockId, primaryDisplay)
$: normalFields = getNormalFields(schema) $: normalFields = getNormalFields(schema)
$: rowClickActions = $: rowClickActions =
clickBehaviour === "actions" || dataSource?.type !== "table" clickBehaviour === "actions" || !isDSPlus
? onClick ? onClick
: [ : [
{ {
@ -78,7 +78,7 @@
}, },
] ]
$: buttonClickActions = $: buttonClickActions =
titleButtonClickBehaviour === "actions" || dataSource?.type !== "table" titleButtonClickBehaviour === "actions" || !isDSPlus
? onClickTitleButton ? onClickTitleButton
: [ : [
{ {

View File

@ -45,7 +45,7 @@
"##eventHandlerType": "Save Row", "##eventHandlerType": "Save Row",
parameters: { parameters: {
providerId: formId, providerId: formId,
tableId: dataSource?.tableId, tableId: dataSource?.resourceId,
notificationOverride, notificationOverride,
}, },
}, },
@ -78,7 +78,7 @@
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",
parameters: { parameters: {
confirm: true, confirm: true,
tableId: dataSource?.tableId, tableId: dataSource?.resourceId,
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`, rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`, revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
notificationOverride, notificationOverride,

View File

@ -45,7 +45,7 @@
return return
} }
nextIndicators[idx].visible = nextIndicators[idx].visible =
nextIndicators[idx].isSidePanel || entries[0].isIntersecting nextIndicators[idx].insideSidePanel || entries[0].isIntersecting
if (++callbackCount === observers.length) { if (++callbackCount === observers.length) {
indicators = nextIndicators indicators = nextIndicators
updating = false updating = false
@ -125,7 +125,7 @@
width: elBounds.width + 4, width: elBounds.width + 4,
height: elBounds.height + 4, height: elBounds.height + 4,
visible: false, visible: false,
isSidePanel: child.classList.contains("side-panel"), insideSidePanel: !!child.closest(".side-panel"),
}) })
}) })
} }

View File

@ -18,6 +18,8 @@ export const createDataSourceStore = () => {
// Extract table ID // Extract table ID
if (dataSource.type === "table" || dataSource.type === "view") { if (dataSource.type === "table" || dataSource.type === "view") {
dataSourceId = dataSource.tableId dataSourceId = dataSource.tableId
} else if (dataSource.type === "viewV2") {
dataSourceId = dataSource.id
} }
// Only one side of the relationship is required as a trigger, as it will // Only one side of the relationship is required as a trigger, as it will
@ -79,7 +81,7 @@ export const createDataSourceStore = () => {
// Fetch related table IDs from table schema // Fetch related table IDs from table schema
let schema let schema
if (options.invalidateRelationships) { if (options.invalidateRelationships && !dataSourceId?.includes("view_")) {
try { try {
const definition = await API.fetchTableDefinition(dataSourceId) const definition = await API.fetchTableDefinition(dataSourceId)
schema = definition?.schema schema = definition?.schema

View File

@ -42,7 +42,7 @@ const saveRowHandler = async (action, context) => {
} }
// Refresh related datasources // Refresh related datasources
await dataSourceStore.actions.invalidateDataSource(row.tableId, { await dataSourceStore.actions.invalidateDataSource(tableId, {
invalidateRelationships: true, invalidateRelationships: true,
}) })
@ -75,7 +75,7 @@ const duplicateRowHandler = async (action, context) => {
} }
// Refresh related datasources // Refresh related datasources
await dataSourceStore.actions.invalidateDataSource(row.tableId, { await dataSourceStore.actions.invalidateDataSource(tableId, {
invalidateRelationships: true, invalidateRelationships: true,
}) })

View File

@ -6,6 +6,7 @@ import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFet
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch.js" import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch.js"
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch.js" import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch.js"
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js" import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch.js"
/** /**
* Fetches the schema of any kind of datasource. * Fetches the schema of any kind of datasource.
@ -21,6 +22,7 @@ export const fetchDatasourceSchema = async (
const handler = { const handler = {
table: TableFetch, table: TableFetch,
view: ViewFetch, view: ViewFetch,
viewV2: ViewV2Fetch,
query: QueryFetch, query: QueryFetch,
link: RelationshipFetch, link: RelationshipFetch,
provider: NestedProviderFetch, provider: NestedProviderFetch,
@ -49,6 +51,15 @@ export const fetchDatasourceSchema = async (
return null return null
} }
// Strip hidden fields from views
if (datasource.type === "viewV2") {
Object.keys(schema).forEach(field => {
if (!schema[field].visible) {
delete schema[field]
}
})
}
// Enrich schema with relationships if required // Enrich schema with relationships if required
if (definition?.sql && options?.enrichRelationships) { if (definition?.sql && options?.enrichRelationships) {
const relationshipAdditions = await getRelationshipSchemaAdditions(schema) const relationshipAdditions = await getRelationshipSchemaAdditions(schema)

View File

@ -23,6 +23,7 @@ import { buildTemplateEndpoints } from "./templates"
import { buildUserEndpoints } from "./user" import { buildUserEndpoints } from "./user"
import { buildSelfEndpoints } from "./self" import { buildSelfEndpoints } from "./self"
import { buildViewEndpoints } from "./views" import { buildViewEndpoints } from "./views"
import { buildViewV2Endpoints } from "./viewsV2"
import { buildLicensingEndpoints } from "./licensing" import { buildLicensingEndpoints } from "./licensing"
import { buildGroupsEndpoints } from "./groups" import { buildGroupsEndpoints } from "./groups"
import { buildPluginEndpoints } from "./plugins" import { buildPluginEndpoints } from "./plugins"
@ -279,5 +280,6 @@ export const createAPIClient = config => {
...buildEventEndpoints(API), ...buildEventEndpoints(API),
...buildAuditLogsEndpoints(API), ...buildAuditLogsEndpoints(API),
...buildLogsEndpoints(API), ...buildLogsEndpoints(API),
viewV2: buildViewV2Endpoints(API),
} }
} }

View File

@ -23,7 +23,23 @@ export const buildRowEndpoints = API => ({
return return
} }
return await API.post({ return await API.post({
url: `/api/${row.tableId}/rows`, url: `/api/${row._viewId || row.tableId}/rows`,
body: row,
suppressErrors,
})
},
/**
* Patches a row in a table.
* @param row the row to patch
* @param suppressErrors whether or not to suppress error notifications
*/
patchRow: async (row, suppressErrors = false) => {
if (!row?.tableId && !row?._viewId) {
return
}
return await API.patch({
url: `/api/${row._viewId || row.tableId}/rows`,
body: row, body: row,
suppressErrors, suppressErrors,
}) })
@ -31,7 +47,7 @@ export const buildRowEndpoints = API => ({
/** /**
* Deletes a row from a table. * Deletes a row from a table.
* @param tableId the ID of the table to delete from * @param tableId the ID of the table or view to delete from
* @param rowId the ID of the row to delete * @param rowId the ID of the row to delete
* @param revId the rev of the row to delete * @param revId the rev of the row to delete
*/ */
@ -50,10 +66,13 @@ export const buildRowEndpoints = API => ({
/** /**
* Deletes multiple rows from a table. * Deletes multiple rows from a table.
* @param tableId the table ID to delete the rows from * @param tableId the table or view ID to delete the rows from
* @param rows the array of rows to delete * @param rows the array of rows to delete
*/ */
deleteRows: async ({ tableId, rows }) => { deleteRows: async ({ tableId, rows }) => {
rows?.forEach(row => {
delete row?._viewId
})
return await API.delete({ return await API.delete({
url: `/api/${tableId}/rows`, url: `/api/${tableId}/rows`,
body: { body: {

View File

@ -0,0 +1,72 @@
export const buildViewV2Endpoints = API => ({
/**
* Fetches the definition of a view
* @param viewId the ID of the view to fetch
*/
fetchDefinition: async viewId => {
return await API.get({
url: `/api/v2/views/${viewId}`,
})
},
/**
* Create a new view
* @param view the view object
*/
create: async view => {
return await API.post({
url: `/api/v2/views`,
body: view,
})
},
/**
* Updates a view
* @param view the view object
*/
update: async view => {
return await API.put({
url: `/api/v2/views/${view.id}`,
body: view,
})
},
/**
* Fetches all rows in a view
* @param viewId the id of the view
* @param query the search query
* @param paginate whether to paginate or not
* @param limit page size
* @param bookmark pagination cursor
* @param sort sort column
* @param sortOrder sort order
* @param sortType sort type (text or numeric)
*/
fetch: async ({
viewId,
query,
paginate,
limit,
bookmark,
sort,
sortOrder,
sortType,
}) => {
return await API.post({
url: `/api/v2/views/${viewId}/search`,
body: {
query,
paginate,
limit,
bookmark,
sort,
sortOrder,
sortType,
},
})
},
/**
* Delete a view
* @param viewId the id of the view
*/
delete: async viewId => {
return await API.delete({ url: `/api/v2/views/${viewId}` })
},
})

View File

@ -34,7 +34,7 @@
column.schema.autocolumn || column.schema.autocolumn ||
column.schema.disabled || column.schema.disabled ||
column.schema.type === "formula" || column.schema.type === "formula" ||
(!$config.allowEditRows && row._id) (!$config.canEditRows && row._id)
// Register this cell API if the row is focused // Register this cell API if the row is focused
$: { $: {
@ -58,9 +58,14 @@
isReadonly: () => readonly, isReadonly: () => readonly,
getType: () => column.schema.type, getType: () => column.schema.type,
getValue: () => row[column.name], getValue: () => row[column.name],
setValue: value => { setValue: (value, options = { save: true }) => {
validation.actions.setError(cellId, null) validation.actions.setError(cellId, null)
updateValue(row._id, column.name, value) updateValue({
rowId: row._id,
column: column.name,
value,
save: options?.save,
})
}, },
} }
</script> </script>

View File

@ -40,7 +40,7 @@
<div <div
on:click={select} on:click={select}
class="checkbox" class="checkbox"
class:visible={$config.allowDeleteRows && class:visible={$config.canDeleteRows &&
(disableNumber || rowSelected || rowHovered || rowFocused)} (disableNumber || rowSelected || rowHovered || rowFocused)}
> >
<Checkbox value={rowSelected} {disabled} /> <Checkbox value={rowSelected} {disabled} />
@ -48,14 +48,14 @@
{#if !disableNumber} {#if !disableNumber}
<div <div
class="number" class="number"
class:visible={!$config.allowDeleteRows || class:visible={!$config.canDeleteRows ||
!(rowSelected || rowHovered || rowFocused)} !(rowSelected || rowHovered || rowFocused)}
> >
{row.__idx + 1} {row.__idx + 1}
</div> </div>
{/if} {/if}
{/if} {/if}
{#if rowSelected && $config.allowDeleteRows} {#if rowSelected && $config.canDeleteRows}
<div class="delete" on:click={() => dispatch("request-bulk-delete")}> <div class="delete" on:click={() => dispatch("request-bulk-delete")}>
<Icon <Icon
name="Delete" name="Delete"
@ -64,7 +64,7 @@
/> />
</div> </div>
{:else} {:else}
<div class="expand" class:visible={$config.allowExpandRows && expandable}> <div class="expand" class:visible={$config.canExpandRows && expandable}>
<Icon <Icon
size="S" size="S"
name="Maximize" name="Maximize"

View File

@ -56,6 +56,7 @@
popover.hide() popover.hide()
editIsOpen = false editIsOpen = false
} }
const onMouseDown = e => { const onMouseDown = e => {
if (e.button === 0 && orderable) { if (e.button === 0 && orderable) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
@ -116,6 +117,7 @@
columns.actions.saveChanges() columns.actions.saveChanges()
open = false open = false
} }
onMount(() => subscribe("close-edit-column", cancelEdit)) onMount(() => subscribe("close-edit-column", cancelEdit))
</script> </script>
@ -170,7 +172,6 @@
align="right" align="right"
offset={0} offset={0}
popoverTarget={document.getElementById(`grid-${rand}`)} popoverTarget={document.getElementById(`grid-${rand}`)}
animate={false}
customZindex={100} customZindex={100}
> >
{#if editIsOpen} {#if editIsOpen}
@ -187,7 +188,7 @@
<MenuItem <MenuItem
icon="Edit" icon="Edit"
on:click={editColumn} on:click={editColumn}
disabled={!$config.allowSchemaChanges || column.schema.disabled} disabled={!$config.canEditColumns || column.schema.disabled}
> >
Edit column Edit column
</MenuItem> </MenuItem>
@ -195,7 +196,6 @@
icon="Label" icon="Label"
on:click={makeDisplayColumn} on:click={makeDisplayColumn}
disabled={idx === "sticky" || disabled={idx === "sticky" ||
!$config.allowSchemaChanges ||
bannedDisplayColumnTypes.includes(column.schema.type)} bannedDisplayColumnTypes.includes(column.schema.type)}
> >
Use as display column Use as display column

View File

@ -3,7 +3,7 @@
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui" import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
import { getColumnIcon } from "../lib/utils" import { getColumnIcon } from "../lib/utils"
const { columns, stickyColumn } = getContext("grid") const { columns, stickyColumn, dispatch } = getContext("grid")
let open = false let open = false
let anchor let anchor
@ -11,33 +11,36 @@
$: anyHidden = $columns.some(col => !col.visible) $: anyHidden = $columns.some(col => !col.visible)
$: text = getText($columns) $: text = getText($columns)
const toggleVisibility = (column, visible) => { const toggleVisibility = async (column, visible) => {
columns.update(state => { columns.update(state => {
const index = state.findIndex(col => col.name === column.name) const index = state.findIndex(col => col.name === column.name)
state[index].visible = visible state[index].visible = visible
return state.slice() return state.slice()
}) })
columns.actions.saveChanges() await columns.actions.saveChanges()
dispatch(visible ? "show-column" : "hide-column")
} }
const showAll = () => { const showAll = async () => {
columns.update(state => { columns.update(state => {
return state.map(col => ({ return state.map(col => ({
...col, ...col,
visible: true, visible: true,
})) }))
}) })
columns.actions.saveChanges() await columns.actions.saveChanges()
dispatch("show-column")
} }
const hideAll = () => { const hideAll = async () => {
columns.update(state => { columns.update(state => {
return state.map(col => ({ return state.map(col => ({
...col, ...col,
visible: false, visible: false,
})) }))
}) })
columns.actions.saveChanges() await columns.actions.saveChanges()
dispatch("hide-column")
} }
const getText = columns => { const getText = columns => {

View File

@ -8,8 +8,14 @@
SmallRowHeight, SmallRowHeight,
} from "../lib/constants" } from "../lib/constants"
const { stickyColumn, columns, rowHeight, table, fixedRowHeight } = const {
getContext("grid") stickyColumn,
columns,
rowHeight,
definition,
fixedRowHeight,
datasource,
} = getContext("grid")
// Some constants for column width options // Some constants for column width options
const smallColSize = 120 const smallColSize = 120
@ -60,8 +66,8 @@
] ]
const changeRowHeight = height => { const changeRowHeight = height => {
columns.actions.saveTable({ datasource.actions.saveDefinition({
...$table, ...$definition,
rowHeight: height, rowHeight: height,
}) })
} }

View File

@ -8,7 +8,6 @@
let anchor let anchor
$: columnOptions = getColumnOptions($stickyColumn, $columns) $: columnOptions = getColumnOptions($stickyColumn, $columns)
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
$: orderOptions = getOrderOptions($sort.column, columnOptions) $: orderOptions = getOrderOptions($sort.column, columnOptions)
const getColumnOptions = (stickyColumn, columns) => { const getColumnOptions = (stickyColumn, columns) => {
@ -46,8 +45,8 @@
const updateSortColumn = e => { const updateSortColumn = e => {
sort.update(state => ({ sort.update(state => ({
...state,
column: e.detail, column: e.detail,
order: e.detail ? state.order : "ascending",
})) }))
} }
@ -57,29 +56,6 @@
order: e.detail, order: e.detail,
})) }))
} }
// Ensure we never have a sort column selected that is not visible
const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
if (!sortColumn) {
return
}
if (
sortColumn !== stickyColumn?.name &&
!columns.some(col => col.name === sortColumn)
) {
if (stickyColumn) {
sort.update(state => ({
...state,
column: stickyColumn.name,
}))
} else {
sort.update(state => ({
...state,
column: columns[0]?.name,
}))
}
}
}
</script> </script>
<div bind:this={anchor}> <div bind:this={anchor}>
@ -98,21 +74,23 @@
<Popover bind:open {anchor} align="left"> <Popover bind:open {anchor} align="left">
<div class="content"> <div class="content">
<Select <Select
placeholder={null} placeholder="Default"
value={$sort.column} value={$sort.column}
options={columnOptions} options={columnOptions}
autoWidth autoWidth
on:change={updateSortColumn} on:change={updateSortColumn}
label="Column" label="Column"
/> />
<Select {#if $sort.column}
placeholder={null} <Select
value={$sort.order} placeholder={null}
options={orderOptions} value={$sort.order || "ascending"}
autoWidth options={orderOptions}
on:change={updateSortOrder} autoWidth
label="Order" on:change={updateSortOrder}
/> label="Order"
/>
{/if}
</div> </div>
</Popover> </Popover>

View File

@ -1,5 +1,6 @@
<script> <script>
import { setContext, onMount } from "svelte" import { setContext, onMount } from "svelte"
import { writable } from "svelte/store"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { clickOutside, ProgressCircle } from "@budibase/bbui" import { clickOutside, ProgressCircle } from "@budibase/bbui"
import { createEventManagers } from "../lib/events" import { createEventManagers } from "../lib/events"
@ -28,14 +29,15 @@
} from "../lib/constants" } from "../lib/constants"
export let API = null export let API = null
export let tableId = null export let datasource = null
export let schemaOverrides = null export let schemaOverrides = null
export let columnWhitelist = null export let columnWhitelist = null
export let allowAddRows = true export let canAddRows = true
export let allowExpandRows = true export let canExpandRows = true
export let allowEditRows = true export let canEditRows = true
export let allowDeleteRows = true export let canDeleteRows = true
export let allowSchemaChanges = true export let canEditColumns = true
export let canSaveSchema = true
export let stripeRows = false export let stripeRows = false
export let collaboration = true export let collaboration = true
export let showAvatars = true export let showAvatars = true
@ -50,11 +52,14 @@
// Unique identifier for DOM nodes inside this instance // Unique identifier for DOM nodes inside this instance
const rand = Math.random() const rand = Math.random()
// Store props in a store for reference in other stores
const props = writable($$props)
// Build up context // Build up context
let context = { let context = {
API: API || createAPIClient(), API: API || createAPIClient(),
rand, rand,
props: $$props, props,
} }
context = { ...context, ...createEventManagers() } context = { ...context, ...createEventManagers() }
context = attachStores(context) context = attachStores(context)
@ -71,19 +76,19 @@
contentLines, contentLines,
gridFocused, gridFocused,
error, error,
canAddRows,
} = context } = context
// Keep config store up to date with props // Keep config store up to date with props
$: config.set({ $: props.set({
tableId, datasource,
schemaOverrides, schemaOverrides,
columnWhitelist, columnWhitelist,
allowAddRows, canAddRows,
allowExpandRows, canExpandRows,
allowEditRows, canEditRows,
allowDeleteRows, canDeleteRows,
allowSchemaChanges, canEditColumns,
canSaveSchema,
stripeRows, stripeRows,
collaboration, collaboration,
showAvatars, showAvatars,
@ -155,7 +160,7 @@
</HeaderRow> </HeaderRow>
<GridBody /> <GridBody />
</div> </div>
{#if $canAddRows} {#if $config.canAddRows}
<NewRow /> <NewRow />
{/if} {/if}
<div class="overlays"> <div class="overlays">
@ -179,7 +184,7 @@
<ProgressCircle /> <ProgressCircle />
</div> </div>
{/if} {/if}
{#if allowDeleteRows} {#if $config.canDeleteRows}
<BulkDeleteHandler /> <BulkDeleteHandler />
{/if} {/if}
<KeyboardManager /> <KeyboardManager />

View File

@ -9,10 +9,10 @@
renderedRows, renderedRows,
renderedColumns, renderedColumns,
rowVerticalInversionIndex, rowVerticalInversionIndex,
canAddRows,
hoveredRowId, hoveredRowId,
dispatch, dispatch,
isDragging, isDragging,
config,
} = getContext("grid") } = getContext("grid")
let body let body
@ -43,7 +43,7 @@
invertY={idx >= $rowVerticalInversionIndex} invertY={idx >= $rowVerticalInversionIndex}
/> />
{/each} {/each}
{#if $canAddRows} {#if $config.canAddRows}
<div <div
class="blank" class="blank"
class:highlighted={$hoveredRowId === BlankRowID} class:highlighted={$hoveredRowId === BlankRowID}

View File

@ -1,12 +1,11 @@
<script> <script>
import NewColumnButton from "./NewColumnButton.svelte" import NewColumnButton from "./NewColumnButton.svelte"
import { getContext } from "svelte" import { getContext } from "svelte"
import GridScrollWrapper from "./GridScrollWrapper.svelte" import GridScrollWrapper from "./GridScrollWrapper.svelte"
import HeaderCell from "../cells/HeaderCell.svelte" import HeaderCell from "../cells/HeaderCell.svelte"
import { TempTooltip, TooltipType } from "@budibase/bbui" import { TempTooltip, TooltipType } from "@budibase/bbui"
const { renderedColumns, config, hasNonAutoColumn, tableId, loading } = const { renderedColumns, config, hasNonAutoColumn, datasource, loading } =
getContext("grid") getContext("grid")
</script> </script>
@ -20,8 +19,8 @@
{/each} {/each}
</div> </div>
</GridScrollWrapper> </GridScrollWrapper>
{#if $config.allowSchemaChanges} {#if $config.canEditColumns}
{#key $tableId} {#key $datasource}
<TempTooltip <TempTooltip
text="Click here to create your first column" text="Click here to create your first column"
type={TooltipType.Info} type={TooltipType.Info}

View File

@ -7,6 +7,7 @@
let anchor let anchor
let open = false let open = false
$: columnsWidth = $renderedColumns.reduce( $: columnsWidth = $renderedColumns.reduce(
(total, col) => (total += col.width), (total, col) => (total += col.width),
0 0
@ -17,6 +18,7 @@
const close = () => { const close = () => {
open = false open = false
} }
onMount(() => subscribe("close-edit-column", close)) onMount(() => subscribe("close-edit-column", close))
</script> </script>
@ -35,7 +37,6 @@
align={$renderedColumns.length ? "right" : "left"} align={$renderedColumns.length ? "right" : "left"}
offset={0} offset={0}
popoverTarget={document.getElementById(`add-column-button`)} popoverTarget={document.getElementById(`add-column-button`)}
animate={false}
customZindex={100} customZindex={100}
> >
<div <div

View File

@ -17,7 +17,7 @@
dispatch, dispatch,
rows, rows,
focusedCellAPI, focusedCellAPI,
tableId, datasource,
subscribe, subscribe,
renderedRows, renderedRows,
renderedColumns, renderedColumns,
@ -28,7 +28,7 @@
columnHorizontalInversionIndex, columnHorizontalInversionIndex,
selectedRows, selectedRows,
loading, loading,
canAddRows, config,
} = getContext("grid") } = getContext("grid")
let visible = false let visible = false
@ -38,7 +38,7 @@
$: firstColumn = $stickyColumn || $renderedColumns[0] $: firstColumn = $stickyColumn || $renderedColumns[0]
$: width = GutterWidth + ($stickyColumn?.width || 0) $: width = GutterWidth + ($stickyColumn?.width || 0)
$: $tableId, (visible = false) $: $datasource, (visible = false)
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows) $: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
$: selectedRowCount = Object.values($selectedRows).length $: selectedRowCount = Object.values($selectedRows).length
$: hasNoRows = !$rows.length $: hasNoRows = !$rows.length
@ -120,8 +120,8 @@
document.addEventListener("keydown", handleKeyPress) document.addEventListener("keydown", handleKeyPress)
} }
const updateValue = (rowId, columnName, val) => { const updateValue = ({ column, value }) => {
newRow[columnName] = val newRow[column] = value
} }
const addViaModal = () => { const addViaModal = () => {
@ -154,7 +154,7 @@
condition={hasNoRows && !$loading} condition={hasNoRows && !$loading}
type={TooltipType.Info} type={TooltipType.Info}
> >
{#if !visible && !selectedRowCount && $canAddRows} {#if !visible && !selectedRowCount && $config.canAddRows}
<div <div
class="new-row-fab" class="new-row-fab"
on:click={() => dispatch("add-row-inline")} on:click={() => dispatch("add-row-inline")}

View File

@ -16,7 +16,7 @@
renderedRows, renderedRows,
focusedCellId, focusedCellId,
hoveredRowId, hoveredRowId,
canAddRows, config,
selectedCellMap, selectedCellMap,
focusedRow, focusedRow,
scrollLeft, scrollLeft,
@ -94,7 +94,7 @@
{/if} {/if}
</div> </div>
{/each} {/each}
{#if $canAddRows} {#if $config.canAddRows}
<div <div
class="row new" class="row new"
on:mouseenter={$isDragging on:mouseenter={$isDragging

View File

@ -3,18 +3,21 @@ import { createWebsocket } from "../../../utils"
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core" import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
export const createGridWebsocket = context => { export const createGridWebsocket = context => {
const { rows, tableId, users, focusedCellId, table, API } = context const { rows, datasource, users, focusedCellId, definition, API } = context
const socket = createWebsocket("/socket/grid") const socket = createWebsocket("/socket/grid")
const connectToTable = tableId => { const connectToDatasource = datasource => {
if (!socket.connected) { if (!socket.connected) {
return return
} }
// Identify which table we are editing // Identify which table we are editing
const appId = API.getAppID() const appId = API.getAppID()
socket.emit( socket.emit(
GridSocketEvent.SelectTable, GridSocketEvent.SelectDatasource,
{ tableId, appId }, {
datasource,
appId,
},
({ users: gridUsers }) => { ({ users: gridUsers }) => {
users.set(gridUsers) users.set(gridUsers)
} }
@ -23,7 +26,7 @@ export const createGridWebsocket = context => {
// Built-in events // Built-in events
socket.on("connect", () => { socket.on("connect", () => {
connectToTable(get(tableId)) connectToDatasource(get(datasource))
}) })
socket.on("connect_error", err => { socket.on("connect_error", err => {
console.log("Failed to connect to grid websocket:", err.message) console.log("Failed to connect to grid websocket:", err.message)
@ -48,16 +51,19 @@ export const createGridWebsocket = context => {
}) })
// Table events // Table events
socket.onOther(GridSocketEvent.TableChange, ({ table: newTable }) => { socket.onOther(
// Only update table if one exists. If the table was deleted then we don't GridSocketEvent.DatasourceChange,
// want to know - let the builder navigate away ({ datasource: newDatasource }) => {
if (newTable) { // Only update definition if one exists. If the datasource was deleted
table.set(newTable) // then we don't want to know - let the builder navigate away
if (newDatasource) {
definition.set(newDatasource)
}
} }
}) )
// Change websocket connection when table changes // Change websocket connection when table changes
tableId.subscribe(connectToTable) datasource.subscribe(connectToDatasource)
// Notify selected cell changes // Notify selected cell changes
focusedCellId.subscribe($focusedCellId => { focusedCellId.subscribe($focusedCellId => {

View File

@ -4,7 +4,7 @@
import { NewRowID } from "../lib/constants" import { NewRowID } from "../lib/constants"
const { const {
enrichedRows, rows,
focusedCellId, focusedCellId,
visibleColumns, visibleColumns,
focusedRow, focusedRow,
@ -16,7 +16,6 @@
config, config,
menu, menu,
gridFocused, gridFocused,
canAddRows,
} = getContext("grid") } = getContext("grid")
const ignoredOriginSelectors = [ const ignoredOriginSelectors = [
@ -46,12 +45,12 @@
e.preventDefault() e.preventDefault()
focusFirstCell() focusFirstCell()
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { } else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
if ($canAddRows) { if ($config.canAddRows) {
e.preventDefault() e.preventDefault()
dispatch("add-row-inline") dispatch("add-row-inline")
} }
} else if (e.key === "Delete" || e.key === "Backspace") { } else if (e.key === "Delete" || e.key === "Backspace") {
if (Object.keys($selectedRows).length && $config.allowDeleteRows) { if (Object.keys($selectedRows).length && $config.canDeleteRows) {
dispatch("request-bulk-delete") dispatch("request-bulk-delete")
} }
} }
@ -100,7 +99,7 @@
} }
break break
case "Enter": case "Enter":
if ($canAddRows) { if ($config.canAddRows) {
dispatch("add-row-inline") dispatch("add-row-inline")
} }
} }
@ -120,7 +119,7 @@
break break
case "Delete": case "Delete":
case "Backspace": case "Backspace":
if (Object.keys($selectedRows).length && $config.allowDeleteRows) { if (Object.keys($selectedRows).length && $config.canDeleteRows) {
dispatch("request-bulk-delete") dispatch("request-bulk-delete")
} else { } else {
deleteSelectedCell() deleteSelectedCell()
@ -131,7 +130,7 @@
break break
case " ": case " ":
case "Space": case "Space":
if ($config.allowDeleteRows) { if ($config.canDeleteRows) {
toggleSelectRow() toggleSelectRow()
} }
break break
@ -143,7 +142,7 @@
// Focuses the first cell in the grid // Focuses the first cell in the grid
const focusFirstCell = () => { const focusFirstCell = () => {
const firstRow = $enrichedRows[0] const firstRow = $rows[0]
if (!firstRow) { if (!firstRow) {
return return
} }
@ -184,7 +183,7 @@
if (!$focusedRow) { if (!$focusedRow) {
return return
} }
const newRow = $enrichedRows[$focusedRow.__idx + delta] const newRow = $rows[$focusedRow.__idx + delta]
if (newRow) { if (newRow) {
const split = $focusedCellId.split("-") const split = $focusedCellId.split("-")
$focusedCellId = `${newRow._id}-${split[1]}` $focusedCellId = `${newRow._id}-${split[1]}`
@ -216,13 +215,15 @@
if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) { if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
const type = $focusedCellAPI.getType() const type = $focusedCellAPI.getType()
if (type === "number" && keyCodeIsNumber(keyCode)) { if (type === "number" && keyCodeIsNumber(keyCode)) {
$focusedCellAPI.setValue(parseInt(key)) // Update the value locally but don't save it yet
$focusedCellAPI.setValue(parseInt(key), { save: false })
$focusedCellAPI.focus() $focusedCellAPI.focus()
} else if ( } else if (
["string", "barcodeqr", "longform"].includes(type) && ["string", "barcodeqr", "longform"].includes(type) &&
(keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode)) (keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
) { ) {
$focusedCellAPI.setValue(key) // Update the value locally but don't save it yet
$focusedCellAPI.setValue(key, { save: false })
$focusedCellAPI.focus() $focusedCellAPI.focus()
} }
} }

View File

@ -17,7 +17,6 @@
focusedCellAPI, focusedCellAPI,
focusedRowId, focusedRowId,
notifications, notifications,
canAddRows,
} = getContext("grid") } = getContext("grid")
$: style = makeStyle($menu) $: style = makeStyle($menu)
@ -68,9 +67,7 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Maximize" icon="Maximize"
disabled={isNewRow || disabled={isNewRow || !$config.canEditRows || !$config.canExpandRows}
!$config.allowEditRows ||
!$config.allowExpandRows}
on:click={() => dispatch("edit-row", $focusedRow)} on:click={() => dispatch("edit-row", $focusedRow)}
on:click={menu.actions.close} on:click={menu.actions.close}
> >
@ -94,14 +91,14 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Duplicate" icon="Duplicate"
disabled={isNewRow || !$canAddRows} disabled={isNewRow || !$config.canAddRows}
on:click={duplicate} on:click={duplicate}
> >
Duplicate row Duplicate row
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Delete" icon="Delete"
disabled={isNewRow || !$config.allowDeleteRows} disabled={isNewRow || !$config.canDeleteRows}
on:click={deleteRow} on:click={deleteRow}
> >
Delete row Delete row

View File

@ -8,7 +8,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const createActions = context => {
const { copiedCell, focusedCellAPI } = context const { copiedCell, focusedCellAPI } = context
const copy = () => { const copy = () => {

View File

@ -35,20 +35,10 @@ export const createStores = () => {
[] []
) )
// Checks if we have a certain column by name
const hasColumn = column => {
const $columns = get(columns)
const $sticky = get(stickyColumn)
return $columns.some(col => col.name === column) || $sticky?.name === column
}
return { return {
columns: { columns: {
...columns, ...columns,
subscribe: enrichedColumns.subscribe, subscribe: enrichedColumns.subscribe,
actions: {
hasColumn,
},
}, },
stickyColumn, stickyColumn,
visibleColumns, visibleColumns,
@ -56,12 +46,35 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { table, columns, stickyColumn, API, dispatch, config } = context const { columns, stickyColumn } = context
// Updates the tables primary display column // Derive if we have any normal columns
const hasNonAutoColumn = derived(
[columns, stickyColumn],
([$columns, $stickyColumn]) => {
let allCols = $columns || []
if ($stickyColumn) {
allCols = [...allCols, $stickyColumn]
}
const normalCols = allCols.filter(column => {
return !column.schema?.autocolumn
})
return normalCols.length > 0
}
)
return {
hasNonAutoColumn,
}
}
export const createActions = context => {
const { columns, stickyColumn, datasource, definition } = context
// Updates the datasources primary display column
const changePrimaryDisplay = async column => { const changePrimaryDisplay = async column => {
return await saveTable({ return await datasource.actions.saveDefinition({
...get(table), ...get(definition),
primaryDisplay: column, primaryDisplay: column,
}) })
} }
@ -83,29 +96,14 @@ export const deriveStores = context => {
await saveChanges() await saveChanges()
} }
// Derive if we have any normal columns // Persists column changes by saving metadata against datasource schema
const hasNonAutoColumn = derived(
[columns, stickyColumn],
([$columns, $stickyColumn]) => {
let allCols = $columns || []
if ($stickyColumn) {
allCols = [...allCols, $stickyColumn]
}
const normalCols = allCols.filter(column => {
return !column.schema?.autocolumn
})
return normalCols.length > 0
}
)
// Persists column changes by saving metadata against table schema
const saveChanges = async () => { const saveChanges = async () => {
const $columns = get(columns) const $columns = get(columns)
const $table = get(table) const $definition = get(definition)
const $stickyColumn = get(stickyColumn) const $stickyColumn = get(stickyColumn)
const newSchema = cloneDeep($table.schema) const newSchema = cloneDeep($definition.schema)
// Build new updated table schema // Build new updated datasource schema
Object.keys(newSchema).forEach(column => { Object.keys(newSchema).forEach(column => {
// Respect order specified by columns // Respect order specified by columns
const index = $columns.findIndex(x => x.name === column) const index = $columns.findIndex(x => x.name === column)
@ -125,31 +123,17 @@ export const deriveStores = context => {
} }
}) })
await saveTable({ ...$table, schema: newSchema }) await datasource.actions.saveDefinition({
} ...$definition,
schema: newSchema,
const saveTable = async newTable => { })
// Update local state
table.set(newTable)
// Update server
if (get(config).allowSchemaChanges) {
await API.saveTable(newTable)
}
// Broadcast change to external state can be updated, as this change
// will not be received by the builder websocket because we caused it ourselves
dispatch("updatetable", newTable)
} }
return { return {
hasNonAutoColumn,
columns: { columns: {
...columns, ...columns,
actions: { actions: {
...columns.actions,
saveChanges, saveChanges,
saveTable,
changePrimaryDisplay, changePrimaryDisplay,
changeAllColumnWidths, changeAllColumnWidths,
}, },
@ -158,51 +142,7 @@ export const deriveStores = context => {
} }
export const initialise = context => { export const initialise = context => {
const { table, columns, stickyColumn, schemaOverrides, columnWhitelist } = const { definition, columns, stickyColumn, schema } = context
context
const schema = derived(
[table, schemaOverrides, columnWhitelist],
([$table, $schemaOverrides, $columnWhitelist]) => {
if (!$table?.schema) {
return null
}
let newSchema = { ...$table?.schema }
// Edge case to temporarily allow deletion of duplicated user
// fields that were saved with the "disabled" flag set.
// By overriding the saved schema we ensure only overrides can
// set the disabled flag.
// TODO: remove in future
Object.keys(newSchema).forEach(field => {
newSchema[field] = {
...newSchema[field],
disabled: false,
}
})
// Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) {
newSchema[field] = {
...newSchema[field],
...$schemaOverrides[field],
}
}
})
// Apply whitelist if specified
if ($columnWhitelist?.length) {
Object.keys(newSchema).forEach(key => {
if (!$columnWhitelist.includes(key)) {
delete newSchema[key]
}
})
}
return newSchema
}
)
// Merge new schema fields with existing schema in order to preserve widths // Merge new schema fields with existing schema in order to preserve widths
schema.subscribe($schema => { schema.subscribe($schema => {
@ -211,12 +151,12 @@ export const initialise = context => {
stickyColumn.set(null) stickyColumn.set(null)
return return
} }
const $table = get(table) const $definition = get(definition)
// Find primary display // Find primary display
let primaryDisplay let primaryDisplay
if ($table.primaryDisplay && $schema[$table.primaryDisplay]) { if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) {
primaryDisplay = $table.primaryDisplay primaryDisplay = $definition.primaryDisplay
} }
// Get field list // Get field list

View File

@ -1,12 +1,12 @@
import { writable } from "svelte/store"
import { derivedMemo } from "../../../utils" import { derivedMemo } from "../../../utils"
import { derived } from "svelte/store"
export const createStores = context => { export const createStores = context => {
const config = writable(context.props) const { props } = context
const getProp = prop => derivedMemo(config, $config => $config[prop]) const getProp = prop => derivedMemo(props, $props => $props[prop])
// Derive and memoize some props so that we can react to them in isolation // Derive and memoize some props so that we can react to them in isolation
const tableId = getProp("tableId") const datasource = getProp("datasource")
const initialSortColumn = getProp("initialSortColumn") const initialSortColumn = getProp("initialSortColumn")
const initialSortOrder = getProp("initialSortOrder") const initialSortOrder = getProp("initialSortOrder")
const initialFilter = getProp("initialFilter") const initialFilter = getProp("initialFilter")
@ -17,8 +17,7 @@ export const createStores = context => {
const notifyError = getProp("notifyError") const notifyError = getProp("notifyError")
return { return {
config, datasource,
tableId,
initialSortColumn, initialSortColumn,
initialSortOrder, initialSortOrder,
initialFilter, initialFilter,
@ -29,3 +28,31 @@ export const createStores = context => {
notifyError, notifyError,
} }
} }
export const deriveStores = context => {
const { props, hasNonAutoColumn } = context
// Derive features
const config = derived(
[props, hasNonAutoColumn],
([$props, $hasNonAutoColumn]) => {
let config = { ...$props }
// Disable some features if we're editing a view
if ($props.datasource?.type === "viewV2") {
config.canEditColumns = false
}
// Disable adding rows if we don't have any valid columns
if (!$hasNonAutoColumn) {
config.canAddRows = false
}
return config
}
)
return {
config,
}
}

View File

@ -0,0 +1,131 @@
import { derived, get, writable } from "svelte/store"
export const createStores = () => {
const definition = writable(null)
return {
definition,
}
}
export const deriveStores = context => {
const { definition, schemaOverrides, columnWhitelist } = context
const schema = derived(
[definition, schemaOverrides, columnWhitelist],
([$definition, $schemaOverrides, $columnWhitelist]) => {
if (!$definition?.schema) {
return null
}
let newSchema = { ...$definition?.schema }
// Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) {
newSchema[field] = {
...newSchema[field],
...$schemaOverrides[field],
}
}
})
// Apply whitelist if specified
if ($columnWhitelist?.length) {
Object.keys(newSchema).forEach(key => {
if (!$columnWhitelist.includes(key)) {
delete newSchema[key]
}
})
}
return newSchema
}
)
return {
schema,
}
}
export const createActions = context => {
const { datasource, definition, config, dispatch, table, viewV2 } = context
// Gets the appropriate API for the configured datasource type
const getAPI = () => {
const $datasource = get(datasource)
switch ($datasource?.type) {
case "table":
return table
case "viewV2":
return viewV2
default:
return null
}
}
// Refreshes the datasource definition
const refreshDefinition = async () => {
return await getAPI()?.actions.refreshDefinition()
}
// Saves the datasource definition
const saveDefinition = async newDefinition => {
// Update local state
definition.set(newDefinition)
// Update server
if (get(config).canSaveSchema) {
await getAPI()?.actions.saveDefinition(newDefinition)
}
// Broadcast change to external state can be updated, as this change
// will not be received by the builder websocket because we caused it ourselves
dispatch("updatedatasource", newDefinition)
}
// Adds a row to the datasource
const addRow = async row => {
return await getAPI()?.actions.addRow(row)
}
// Updates an existing row in the datasource
const updateRow = async row => {
return await getAPI()?.actions.updateRow(row)
}
// Deletes rows from the datasource
const deleteRows = async rows => {
return await getAPI()?.actions.deleteRows(rows)
}
// Gets a single row from a datasource
const getRow = async id => {
return await getAPI()?.actions.getRow(id)
}
// Checks if a certain datasource config is valid
const isDatasourceValid = datasource => {
return getAPI()?.actions.isDatasourceValid(datasource)
}
// Checks if this datasource can use a specific column by name
const canUseColumn = name => {
return getAPI()?.actions.canUseColumn(name)
}
return {
datasource: {
...datasource,
actions: {
refreshDefinition,
saveDefinition,
addRow,
updateRow,
deleteRows,
getRow,
isDatasourceValid,
canUseColumn,
},
},
}
}

View File

@ -1,10 +1,10 @@
import { writable } from "svelte/store" import { writable, get } from "svelte/store"
export const createStores = context => { export const createStores = context => {
const { props } = context const { props } = context
// Initialise to default props // Initialise to default props
const filter = writable(props.initialFilter) const filter = writable(get(props).initialFilter)
return { return {
filter, filter,

View File

@ -15,16 +15,20 @@ import * as Config from "./config"
import * as Sort from "./sort" import * as Sort from "./sort"
import * as Filter from "./filter" import * as Filter from "./filter"
import * as Notifications from "./notifications" import * as Notifications from "./notifications"
import * as Table from "./table"
import * as ViewV2 from "./viewV2"
import * as Datasource from "./datasource"
const DependencyOrderedStores = [ const DependencyOrderedStores = [
Config,
Notifications,
Sort, Sort,
Filter, Filter,
Bounds, Bounds,
Scroll, Scroll,
Rows, Table,
ViewV2,
Datasource,
Columns, Columns,
Rows,
UI, UI,
Validation, Validation,
Resize, Resize,
@ -34,6 +38,8 @@ const DependencyOrderedStores = [
Menu, Menu,
Pagination, Pagination,
Clipboard, Clipboard,
Config,
Notifications,
] ]
export const attachStores = context => { export const attachStores = context => {
@ -47,6 +53,11 @@ export const attachStores = context => {
context = { ...context, ...store.deriveStores?.(context) } context = { ...context, ...store.deriveStores?.(context) }
} }
// Action creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.createActions?.(context) }
}
// Initialise any store logic // Initialise any store logic
for (let store of DependencyOrderedStores) { for (let store of DependencyOrderedStores) {
store.initialise?.(context) store.initialise?.(context)

View File

@ -12,7 +12,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const createActions = context => {
const { menu, focusedCellId, rand } = context const { menu, focusedCellId, rand } = context
const open = (cellId, e) => { const open = (cellId, e) => {

View File

@ -1,4 +1,4 @@
import { derived } from "svelte/store" import { derived, get } from "svelte/store"
export const initialise = context => { export const initialise = context => {
const { scrolledRowCount, rows, visualRowCapacity } = context const { scrolledRowCount, rows, visualRowCapacity } = context
@ -11,13 +11,12 @@ export const initialise = context => {
[scrolledRowCount, rowCount, visualRowCapacity], [scrolledRowCount, rowCount, visualRowCapacity],
([$scrolledRowCount, $rowCount, $visualRowCapacity]) => { ([$scrolledRowCount, $rowCount, $visualRowCapacity]) => {
return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity) return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity)
}, }
100
) )
// Fetch next page when fewer than 25 remaining rows to scroll // Fetch next page when fewer than 25 remaining rows to scroll
remainingRows.subscribe(remaining => { remainingRows.subscribe(remaining => {
if (remaining < 25) { if (remaining < 25 && get(rowCount)) {
rows.actions.loadNextPage() rows.actions.loadNextPage()
} }
}) })

View File

@ -23,7 +23,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const createActions = context => {
const { const {
reorder, reorder,
columns, columns,

View File

@ -19,7 +19,7 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const createActions = context => {
const { resize, columns, stickyColumn, ui } = context const { resize, columns, stickyColumn, ui } = context
// Starts resizing a certain column // Starts resizing a certain column

View File

@ -3,30 +3,24 @@ import { fetchData } from "../../../fetch/fetchData"
import { NewRowID, RowPageSize } from "../lib/constants" import { NewRowID, RowPageSize } from "../lib/constants"
import { tick } from "svelte" import { tick } from "svelte"
const SuppressErrors = true
export const createStores = () => { export const createStores = () => {
const rows = writable([]) const rows = writable([])
const table = writable(null)
const loading = writable(false) const loading = writable(false)
const loaded = writable(false) const loaded = writable(false)
const rowChangeCache = writable({}) const rowChangeCache = writable({})
const inProgressChanges = writable({}) const inProgressChanges = writable({})
const hasNextPage = writable(false) const hasNextPage = writable(false)
const error = writable(null) const error = writable(null)
const fetch = writable(null)
// Generate a lookup map to quick find a row by ID // Generate a lookup map to quick find a row by ID
const rowLookupMap = derived( const rowLookupMap = derived(rows, $rows => {
rows, let map = {}
$rows => { for (let i = 0; i < $rows.length; i++) {
let map = {} map[$rows[i]._id] = i
for (let i = 0; i < $rows.length; i++) { }
map[$rows[i]._id] = i return map
} })
return map
},
{}
)
// Mark loaded as true if we've ever stopped loading // Mark loaded as true if we've ever stopped loading
let hasStartedLoading = false let hasStartedLoading = false
@ -38,10 +32,25 @@ export const createStores = () => {
} }
}) })
// Enrich rows with an index property and any pending changes
const enrichedRows = derived(
[rows, rowChangeCache],
([$rows, $rowChangeCache]) => {
return $rows.map((row, idx) => ({
...row,
...$rowChangeCache[row._id],
__idx: idx,
}))
}
)
return { return {
rows, rows: {
...rows,
subscribe: enrichedRows.subscribe,
},
fetch,
rowLookupMap, rowLookupMap,
table,
loaded, loaded,
loading, loading,
rowChangeCache, rowChangeCache,
@ -51,15 +60,15 @@ export const createStores = () => {
} }
} }
export const deriveStores = context => { export const createActions = context => {
const { const {
rows, rows,
rowLookupMap, rowLookupMap,
table, definition,
filter, filter,
loading, loading,
sort, sort,
tableId, datasource,
API, API,
scroll, scroll,
validation, validation,
@ -71,37 +80,29 @@ export const deriveStores = context => {
hasNextPage, hasNextPage,
error, error,
notifications, notifications,
fetch,
} = context } = context
const instanceLoaded = writable(false) const instanceLoaded = writable(false)
const fetch = writable(null)
// Local cache of row IDs to speed up checking if a row exists // Local cache of row IDs to speed up checking if a row exists
let rowCacheMap = {} let rowCacheMap = {}
// Enrich rows with an index property and any pending changes // Reset everything when datasource changes
const enrichedRows = derived(
[rows, rowChangeCache],
([$rows, $rowChangeCache]) => {
return $rows.map((row, idx) => ({
...row,
...$rowChangeCache[row._id],
__idx: idx,
}))
},
[]
)
// Reset everything when table ID changes
let unsubscribe = null let unsubscribe = null
let lastResetKey = null let lastResetKey = null
tableId.subscribe(async $tableId => { datasource.subscribe(async $datasource => {
// Unsub from previous fetch if one exists // Unsub from previous fetch if one exists
unsubscribe?.() unsubscribe?.()
fetch.set(null) fetch.set(null)
instanceLoaded.set(false) instanceLoaded.set(false)
loading.set(true) loading.set(true)
// Tick to allow other reactive logic to update stores when table ID changes // Abandon if we don't have a valid datasource
if (!datasource.actions.isDatasourceValid($datasource)) {
return
}
// Tick to allow other reactive logic to update stores when datasource changes
// before proceeding. This allows us to wipe filters etc if needed. // before proceeding. This allows us to wipe filters etc if needed.
await tick() await tick()
const $filter = get(filter) const $filter = get(filter)
@ -110,10 +111,7 @@ export const deriveStores = context => {
// Create new fetch model // Create new fetch model
const newFetch = fetchData({ const newFetch = fetchData({
API, API,
datasource: { datasource: $datasource,
type: "table",
tableId: $tableId,
},
options: { options: {
filter: $filter, filter: $filter,
sortColumn: $sort.column, sortColumn: $sort.column,
@ -142,7 +140,7 @@ export const deriveStores = context => {
const previousResetKey = lastResetKey const previousResetKey = lastResetKey
lastResetKey = $fetch.resetKey lastResetKey = $fetch.resetKey
// If resetting rows due to a table change, wipe data and wait for // If resetting rows due to a datasource change, wipe data and wait for
// derived stores to compute. This prevents stale data being passed // derived stores to compute. This prevents stale data being passed
// to cells when we save the new schema. // to cells when we save the new schema.
if (!$instanceLoaded && previousResetKey) { if (!$instanceLoaded && previousResetKey) {
@ -152,16 +150,12 @@ export const deriveStores = context => {
// Reset state properties when dataset changes // Reset state properties when dataset changes
if (!$instanceLoaded || resetRows) { if (!$instanceLoaded || resetRows) {
table.set($fetch.definition) definition.set($fetch.definition)
sort.set({
column: $fetch.sortColumn,
order: $fetch.sortOrder,
})
} }
// Reset scroll state when data changes // Reset scroll state when data changes
if (!$instanceLoaded) { if (!$instanceLoaded) {
// Reset both top and left for a new table ID // Reset both top and left for a new datasource ID
instanceLoaded.set(true) instanceLoaded.set(true)
scroll.set({ top: 0, left: 0 }) scroll.set({ top: 0, left: 0 })
} else if (resetRows) { } else if (resetRows) {
@ -180,19 +174,6 @@ export const deriveStores = context => {
fetch.set(newFetch) fetch.set(newFetch)
}) })
// Update fetch when filter or sort config changes
filter.subscribe($filter => {
get(fetch)?.update({
filter: $filter,
})
})
sort.subscribe($sort => {
get(fetch)?.update({
sortOrder: $sort.order,
sortColumn: $sort.column,
})
})
// Gets a row by ID // Gets a row by ID
const getRow = id => { const getRow = id => {
const index = get(rowLookupMap)[id] const index = get(rowLookupMap)[id]
@ -211,7 +192,7 @@ export const deriveStores = context => {
let erroredColumns = [] let erroredColumns = []
let missingColumns = [] let missingColumns = []
for (let column of keys) { for (let column of keys) {
if (columns.actions.hasColumn(column)) { if (datasource.actions.canUseColumn(column)) {
erroredColumns.push(column) erroredColumns.push(column)
} else { } else {
missingColumns.push(column) missingColumns.push(column)
@ -252,11 +233,9 @@ export const deriveStores = context => {
// Adds a new row // Adds a new row
const addRow = async (row, idx, bubble = false) => { const addRow = async (row, idx, bubble = false) => {
try { try {
// Create row // Create row. Spread row so we can mutate and enrich safely.
const newRow = await API.saveRow( let newRow = { ...row }
{ ...row, tableId: get(tableId) }, newRow = await datasource.actions.addRow(newRow)
SuppressErrors
)
// Update state // Update state
if (idx != null) { if (idx != null) {
@ -294,21 +273,6 @@ export const deriveStores = context => {
} }
} }
// Fetches a row by ID using the search endpoint
const fetchRow = async id => {
const res = await API.searchTable({
tableId: get(tableId),
limit: 1,
query: {
equal: {
_id: id,
},
},
paginate: false,
})
return res?.rows?.[0]
}
// Replaces a row in state with the newly defined row, handling updates, // Replaces a row in state with the newly defined row, handling updates,
// addition and deletion // addition and deletion
const replaceRow = (id, row) => { const replaceRow = (id, row) => {
@ -337,7 +301,7 @@ export const deriveStores = context => {
// Refreshes a specific row // Refreshes a specific row
const refreshRow = async id => { const refreshRow = async id => {
const row = await fetchRow(id) const row = await datasource.actions.getRow(id)
replaceRow(id, row) replaceRow(id, row)
} }
@ -347,7 +311,7 @@ export const deriveStores = context => {
} }
// Patches a row with some changes // Patches a row with some changes
const updateRow = async (rowId, changes) => { const updateRow = async (rowId, changes, options = { save: true }) => {
const $rows = get(rows) const $rows = get(rows)
const $rowLookupMap = get(rowLookupMap) const $rowLookupMap = get(rowLookupMap)
const index = $rowLookupMap[rowId] const index = $rowLookupMap[rowId]
@ -377,16 +341,23 @@ export const deriveStores = context => {
}, },
})) }))
// Stop here if we don't want to persist the change
if (!options?.save) {
return
}
// Save change // Save change
try { try {
inProgressChanges.update(state => ({ inProgressChanges.update(state => ({
...state, ...state,
[rowId]: true, [rowId]: true,
})) }))
const saved = await API.saveRow(
{ ...row, ...get(rowChangeCache)[rowId] }, // Update row
SuppressErrors const saved = await datasource.actions.updateRow({
) ...row,
...get(rowChangeCache)[rowId],
})
// Update state after a successful change // Update state after a successful change
if (saved?._id) { if (saved?._id) {
@ -412,8 +383,8 @@ export const deriveStores = context => {
} }
// Updates a value of a row // Updates a value of a row
const updateValue = async (rowId, column, value) => { const updateValue = async ({ rowId, column, value, save = true }) => {
return await updateRow(rowId, { [column]: value }) return await updateRow(rowId, { [column]: value }, { save })
} }
// Deletes an array of rows // Deletes an array of rows
@ -426,10 +397,7 @@ export const deriveStores = context => {
rowsToDelete.forEach(row => { rowsToDelete.forEach(row => {
delete row.__idx delete row.__idx
}) })
await API.deleteRows({ await datasource.actions.deleteRows(rowsToDelete)
tableId: get(tableId),
rows: rowsToDelete,
})
// Update state // Update state
handleRemoveRows(rowsToDelete) handleRemoveRows(rowsToDelete)
@ -473,12 +441,6 @@ export const deriveStores = context => {
get(fetch)?.nextPage() get(fetch)?.nextPage()
} }
// Refreshes the schema of the data fetch subscription
const refreshTableDefinition = async () => {
const definition = await API.fetchTableDefinition(get(tableId))
table.set(definition)
}
// Checks if we have a row with a certain ID // Checks if we have a row with a certain ID
const hasRow = id => { const hasRow = id => {
if (id === NewRowID) { if (id === NewRowID) {
@ -498,7 +460,6 @@ export const deriveStores = context => {
}) })
return { return {
enrichedRows,
rows: { rows: {
...rows, ...rows,
actions: { actions: {
@ -513,7 +474,6 @@ export const deriveStores = context => {
refreshRow, refreshRow,
replaceRow, replaceRow,
refreshData, refreshData,
refreshTableDefinition,
}, },
}, },
} }

View File

@ -1,12 +1,14 @@
import { writable } from "svelte/store" import { derived, get } from "svelte/store"
import { memo } from "../../../utils"
export const createStores = context => { export const createStores = context => {
const { props } = context const { props } = context
const $props = get(props)
// Initialise to default props // Initialise to default props
const sort = writable({ const sort = memo({
column: props.initialSortColumn, column: $props.initialSortColumn,
order: props.initialSortOrder || "ascending", order: $props.initialSortOrder || "ascending",
}) })
return { return {
@ -15,13 +17,34 @@ export const createStores = context => {
} }
export const initialise = context => { export const initialise = context => {
const { sort, initialSortColumn, initialSortOrder } = context const { sort, initialSortColumn, initialSortOrder, definition } = context
// Reset sort when initial sort props change // Reset sort when initial sort props change
initialSortColumn.subscribe(newSortColumn => { initialSortColumn.subscribe(newSortColumn => {
sort.update(state => ({ ...state, column: newSortColumn })) sort.update(state => ({ ...state, column: newSortColumn }))
}) })
initialSortOrder.subscribe(newSortOrder => { initialSortOrder.subscribe(newSortOrder => {
sort.update(state => ({ ...state, order: newSortOrder })) sort.update(state => ({ ...state, order: newSortOrder || "ascending" }))
})
// Derive if the current sort column exists in the schema
const sortColumnExists = derived(
[sort, definition],
([$sort, $definition]) => {
if (!$sort?.column || !$definition) {
return true
}
return $definition.schema?.[$sort.column] != null
}
)
// Clear sort state if our sort column does not exist
sortColumnExists.subscribe(exists => {
if (!exists) {
sort.set({
column: null,
order: "ascending",
})
}
}) })
} }

View File

@ -0,0 +1,129 @@
import { get } from "svelte/store"
const SuppressErrors = true
export const createActions = context => {
const { definition, API, datasource, columns, stickyColumn } = context
const refreshDefinition = async () => {
definition.set(await API.fetchTableDefinition(get(datasource).tableId))
}
const saveDefinition = async newDefinition => {
await API.saveTable(newDefinition)
}
const saveRow = async row => {
row.tableId = get(datasource)?.tableId
return await API.saveRow(row, SuppressErrors)
}
const deleteRows = async rows => {
await API.deleteRows({
tableId: get(datasource).tableId,
rows,
})
}
const isDatasourceValid = datasource => {
return datasource?.type === "table" && datasource?.tableId
}
const getRow = async id => {
const res = await API.searchTable({
tableId: get(datasource).tableId,
limit: 1,
query: {
equal: {
_id: id,
},
},
paginate: false,
})
return res?.rows?.[0]
}
const canUseColumn = name => {
const $columns = get(columns)
const $sticky = get(stickyColumn)
return $columns.some(col => col.name === name) || $sticky?.name === name
}
return {
table: {
actions: {
refreshDefinition,
saveDefinition,
addRow: saveRow,
updateRow: saveRow,
deleteRows,
getRow,
isDatasourceValid,
canUseColumn,
},
},
}
}
export const initialise = context => {
const {
datasource,
fetch,
filter,
sort,
table,
initialFilter,
initialSortColumn,
initialSortOrder,
} = context
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
// Observe datasource changes and apply logic for table datasources
datasource.subscribe($datasource => {
// Clear previous subscriptions
unsubscribers?.forEach(unsubscribe => unsubscribe())
unsubscribers = []
if (!table.actions.isDatasourceValid($datasource)) {
return
}
// Wipe state
filter.set(get(initialFilter))
sort.set({
column: get(initialSortColumn),
order: get(initialSortOrder) || "ascending",
})
// Update fetch when filter changes
unsubscribers.push(
filter.subscribe($filter => {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
return
}
$fetch.update({
filter: $filter,
})
})
)
// Update fetch when sorting changes
unsubscribers.push(
sort.subscribe($sort => {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
return
}
$fetch.update({
sortOrder: $sort.order || "ascending",
sortColumn: $sort.column,
})
})
)
})
}

View File

@ -14,7 +14,7 @@ export const createStores = context => {
const focusedCellAPI = writable(null) const focusedCellAPI = writable(null)
const selectedRows = writable({}) const selectedRows = writable({})
const hoveredRowId = writable(null) const hoveredRowId = writable(null)
const rowHeight = writable(props.fixedRowHeight || DefaultRowHeight) const rowHeight = writable(get(props).fixedRowHeight || DefaultRowHeight)
const previousFocusedRowId = writable(null) const previousFocusedRowId = writable(null)
const gridFocused = writable(false) const gridFocused = writable(false)
const isDragging = writable(false) const isDragging = writable(false)
@ -61,23 +61,13 @@ export const createStores = context => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { const { focusedCellId, rows, rowLookupMap, rowHeight, stickyColumn, width } =
focusedCellId, context
selectedRows,
hoveredRowId,
enrichedRows,
rowLookupMap,
rowHeight,
stickyColumn,
width,
hasNonAutoColumn,
config,
} = context
// Derive the row that contains the selected cell // Derive the row that contains the selected cell
const focusedRow = derived( const focusedRow = derived(
[focusedCellId, rowLookupMap, enrichedRows], [focusedCellId, rowLookupMap, rows],
([$focusedCellId, $rowLookupMap, $enrichedRows]) => { ([$focusedCellId, $rowLookupMap, $rows]) => {
const rowId = $focusedCellId?.split("-")[0] const rowId = $focusedCellId?.split("-")[0]
// Edge case for new rows // Edge case for new rows
@ -87,18 +77,11 @@ export const deriveStores = context => {
// All normal rows // All normal rows
const index = $rowLookupMap[rowId] const index = $rowLookupMap[rowId]
return $enrichedRows[index] return $rows[index]
}, },
null null
) )
// Callback when leaving the grid, deselecting all focussed or selected items
const blur = () => {
focusedCellId.set(null)
selectedRows.set({})
hoveredRowId.set(null)
}
// Derive the amount of content lines to show in cells depending on row height // Derive the amount of content lines to show in cells depending on row height
const contentLines = derived(rowHeight, $rowHeight => { const contentLines = derived(rowHeight, $rowHeight => {
if ($rowHeight >= LargeRowHeight) { if ($rowHeight >= LargeRowHeight) {
@ -114,19 +97,24 @@ export const deriveStores = context => {
return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100 return ($stickyColumn?.width || 0) + $width + GutterWidth < 1100
}) })
// Derive if we're able to add rows
const canAddRows = derived(
[config, hasNonAutoColumn],
([$config, $hasNonAutoColumn]) => {
return $config.allowAddRows && $hasNonAutoColumn
}
)
return { return {
canAddRows,
focusedRow, focusedRow,
contentLines, contentLines,
compact, compact,
}
}
export const createActions = context => {
const { focusedCellId, selectedRows, hoveredRowId } = context
// Callback when leaving the grid, deselecting all focussed or selected items
const blur = () => {
focusedCellId.set(null)
selectedRows.set({})
hoveredRowId.set(null)
}
return {
ui: { ui: {
actions: { actions: {
blur, blur,
@ -143,7 +131,7 @@ export const initialise = context => {
focusedCellId, focusedCellId,
selectedRows, selectedRows,
hoveredRowId, hoveredRowId,
table, definition,
rowHeight, rowHeight,
fixedRowHeight, fixedRowHeight,
} = context } = context
@ -199,9 +187,9 @@ export const initialise = context => {
}) })
// Pull row height from table as long as we don't have a fixed height // Pull row height from table as long as we don't have a fixed height
table.subscribe($table => { definition.subscribe($definition => {
if (!get(fixedRowHeight)) { if (!get(fixedRowHeight)) {
rowHeight.set($table?.rowHeight || DefaultRowHeight) rowHeight.set($definition?.rowHeight || DefaultRowHeight)
} }
}) })
@ -210,7 +198,7 @@ export const initialise = context => {
if (height) { if (height) {
rowHeight.set(height) rowHeight.set(height)
} else { } else {
rowHeight.set(get(table)?.rowHeight || DefaultRowHeight) rowHeight.set(get(definition)?.rowHeight || DefaultRowHeight)
} }
}) })
} }

View File

@ -39,6 +39,14 @@ export const deriveStores = context => {
} }
) )
return {
selectedCellMap,
}
}
export const createActions = context => {
const { users } = context
const updateUser = user => { const updateUser = user => {
const $users = get(users) const $users = get(users)
if (!$users.some(x => x.sessionId === user.sessionId)) { if (!$users.some(x => x.sessionId === user.sessionId)) {
@ -66,6 +74,5 @@ export const deriveStores = context => {
removeUser, removeUser,
}, },
}, },
selectedCellMap,
} }
} }

View File

@ -0,0 +1,212 @@
import { get } from "svelte/store"
const SuppressErrors = true
export const createActions = context => {
const { definition, API, datasource, columns, stickyColumn } = context
const refreshDefinition = async () => {
const $datasource = get(datasource)
if (!$datasource) {
definition.set(null)
return
}
const table = await API.fetchTableDefinition($datasource.tableId)
const view = Object.values(table?.views || {}).find(
view => view.id === $datasource.id
)
definition.set(view)
}
const saveDefinition = async newDefinition => {
await API.viewV2.update(newDefinition)
}
const saveRow = async row => {
const $datasource = get(datasource)
row.tableId = $datasource?.tableId
row._viewId = $datasource?.id
return {
...(await API.saveRow(row, SuppressErrors)),
_viewId: row._viewId,
}
}
const deleteRows = async rows => {
await API.deleteRows({
tableId: get(datasource).id,
rows,
})
}
const getRow = () => {
throw "Views don't support fetching individual rows"
}
const isDatasourceValid = datasource => {
return (
datasource?.type === "viewV2" && datasource?.id && datasource?.tableId
)
}
const canUseColumn = name => {
const $columns = get(columns)
const $sticky = get(stickyColumn)
return (
$columns.some(col => col.name === name && col.visible) ||
$sticky?.name === name
)
}
return {
viewV2: {
actions: {
refreshDefinition,
saveDefinition,
addRow: saveRow,
updateRow: saveRow,
deleteRows,
getRow,
isDatasourceValid,
canUseColumn,
},
},
}
}
export const initialise = context => {
const {
definition,
datasource,
sort,
rows,
filter,
subscribe,
viewV2,
initialFilter,
initialSortColumn,
initialSortOrder,
config,
fetch,
} = context
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
// Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => {
// Clear previous subscriptions
unsubscribers?.forEach(unsubscribe => unsubscribe())
unsubscribers = []
if (!viewV2.actions.isDatasourceValid($datasource)) {
return
}
// Reset state for new view
filter.set(get(initialFilter))
sort.set({
column: get(initialSortColumn),
order: get(initialSortOrder) || "ascending",
})
// Keep sort and filter state in line with the view definition
unsubscribers.push(
definition.subscribe($definition => {
if ($definition?.id !== $datasource.id) {
return
}
// Only override sorting if we don't have an initial sort column
if (!get(initialSortColumn)) {
sort.set({
column: $definition.sort?.field,
order: $definition.sort?.order || "ascending",
})
}
// Only override filter state if we don't have an initial filter
if (!get(initialFilter)) {
filter.set($definition.query)
}
})
)
// When sorting changes, ensure view definition is kept up to date
unsubscribers.push(
sort.subscribe(async $sort => {
// If we can mutate schema then update the view definition
if (get(config).canSaveSchema) {
// Ensure we're updating the correct view
const $view = get(definition)
if ($view?.id !== $datasource.id) {
return
}
if (
$sort?.column !== $view.sort?.field ||
$sort?.order !== $view.sort?.order
) {
await datasource.actions.saveDefinition({
...$view,
sort: {
field: $sort.column,
order: $sort.order || "ascending",
},
})
await rows.actions.refreshData()
}
}
// Otherwise just update the fetch
else {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
return
}
$fetch.update({
sortOrder: $sort.order || "ascending",
sortColumn: $sort.column,
})
}
})
)
// When filters change, ensure view definition is kept up to date
unsubscribers?.push(
filter.subscribe(async $filter => {
// If we can mutate schema then update the view definition
if (get(config).canSaveSchema) {
// Ensure we're updating the correct view
const $view = get(definition)
if ($view?.id !== $datasource.id) {
return
}
if (JSON.stringify($filter) !== JSON.stringify($view.query)) {
await datasource.actions.saveDefinition({
...$view,
query: $filter,
})
await rows.actions.refreshData()
}
}
// Otherwise just update the fetch
else {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if ($fetch?.options?.datasource?.tableId !== $datasource.tableId) {
return
}
$fetch.update({
filter: $filter,
})
}
})
)
// When hidden we show columns, we need to refresh data in order to fetch
// values for those columns
unsubscribers.push(
subscribe("show-column", async () => {
await rows.actions.refreshData()
})
)
})
}

View File

@ -10,7 +10,7 @@ export const deriveStores = context => {
const { const {
rowHeight, rowHeight,
visibleColumns, visibleColumns,
enrichedRows, rows,
scrollTop, scrollTop,
scrollLeft, scrollLeft,
width, width,
@ -35,9 +35,9 @@ export const deriveStores = context => {
0 0
) )
const renderedRows = derived( const renderedRows = derived(
[enrichedRows, scrolledRowCount, visualRowCapacity], [rows, scrolledRowCount, visualRowCapacity],
([$enrichedRows, $scrolledRowCount, $visualRowCapacity]) => { ([$rows, $scrolledRowCount, $visualRowCapacity]) => {
return $enrichedRows.slice( return $rows.slice(
$scrolledRowCount, $scrolledRowCount,
$scrolledRowCount + $visualRowCapacity $scrolledRowCount + $visualRowCapacity
) )

View File

@ -2,6 +2,7 @@
* Operator options for lucene queries * Operator options for lucene queries
*/ */
export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core" export { OperatorOptions, SqlNumberTypeRangeMap } from "@budibase/shared-core"
export { Feature as Features } from "@budibase/types"
// Cookie names // Cookie names
export const Cookies = { export const Cookies = {
@ -74,18 +75,6 @@ export const PlanType = {
*/ */
export const ApiVersion = "1" export const ApiVersion = "1"
export const Features = {
USER_GROUPS: "userGroups",
BACKUPS: "appBackups",
ENVIRONMENT_VARIABLES: "environmentVariables",
AUDIT_LOGS: "auditLogs",
ENFORCEABLE_SSO: "enforceableSSO",
BRANDING: "branding",
SCIM: "scim",
SYNC_AUTOMATIONS: "syncAutomations",
APP_BUILDERS: "appBuilders",
}
// Role IDs // Role IDs
export const Roles = { export const Roles = {
ADMIN: "ADMIN", ADMIN: "ADMIN",

View File

@ -110,14 +110,27 @@ export default class DataFetch {
return this.derivedStore.subscribe return this.derivedStore.subscribe
} }
/**
* Gets the default sort column for this datasource
*/
getDefaultSortColumn(definition, schema) {
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
return definition.primaryDisplay
} else {
return Object.keys(schema)[0]
}
}
/** /**
* Fetches a fresh set of data from the server, resetting pagination * Fetches a fresh set of data from the server, resetting pagination
*/ */
async getInitialData() { async getInitialData() {
const { datasource, filter, paginate } = this.options const { datasource, filter, paginate } = this.options
// Fetch datasource definition and determine feature flags // Fetch datasource definition and extract sort properties if configured
const definition = await this.getDefinition(datasource) const definition = await this.getDefinition(datasource)
// Determine feature flags
const features = this.determineFeatureFlags(definition) const features = this.determineFeatureFlags(definition)
this.features = { this.features = {
supportsSearch: !!features?.supportsSearch, supportsSearch: !!features?.supportsSearch,
@ -132,32 +145,32 @@ export default class DataFetch {
return return
} }
// If no sort order, default to descending // If an invalid sort column is specified, delete it
if (!this.options.sortOrder) { if (this.options.sortColumn && !schema[this.options.sortColumn]) {
this.options.sortColumn = null
}
// If no sort column, get the default column for this datasource
if (!this.options.sortColumn) {
this.options.sortColumn = this.getDefaultSortColumn(definition, schema)
}
// If we don't have a sort column specified then just ensure we don't set
// any sorting params
if (!this.options.sortColumn) {
this.options.sortOrder = "ascending" this.options.sortOrder = "ascending"
} this.options.sortType = null
} else {
// Otherwise determine what sort type to use base on sort column
const type = schema?.[this.options.sortColumn]?.type
this.options.sortType =
type === "number" || type === "bigint" ? "number" : "string"
// If no sort column, or an invalid sort column is provided, use the primary // If no sort order, default to ascending
// display and fallback to first column if (!this.options.sortOrder) {
const sortValid = this.options.sortColumn && schema[this.options.sortColumn] this.options.sortOrder = "ascending"
if (!sortValid) {
let newSortColumn
if (definition?.primaryDisplay && schema[definition.primaryDisplay]) {
newSortColumn = definition.primaryDisplay
} else {
newSortColumn = Object.keys(schema)[0]
} }
this.options.sortColumn = newSortColumn
} }
const { sortOrder, sortColumn } = this.options
// Determine what sort type to use
let sortType = "string"
if (sortColumn) {
const type = schema?.[sortColumn]?.type
sortType = type === "number" || type === "bigint" ? "number" : "string"
}
this.options.sortType = sortType
// Build the lucene query // Build the lucene query
let query = this.options.query let query = this.options.query
@ -174,8 +187,6 @@ export default class DataFetch {
loading: true, loading: true,
cursors: [], cursors: [],
cursor: null, cursor: null,
sortOrder,
sortColumn,
})) }))
// Actually fetch data // Actually fetch data

View File

@ -29,6 +29,10 @@ export default class QueryFetch extends DataFetch {
} }
} }
getDefaultSortColumn() {
return null
}
async getData() { async getData() {
const { datasource, limit, paginate } = this.options const { datasource, limit, paginate } = this.options
const { supportsPagination } = this.features const { supportsPagination } = this.features

View File

@ -0,0 +1,65 @@
import DataFetch from "./DataFetch.js"
import { get } from "svelte/store"
export default class ViewV2Fetch extends DataFetch {
determineFeatureFlags() {
return {
supportsSearch: true,
supportsSort: true,
supportsPagination: true,
}
}
getSchema(datasource, definition) {
return definition?.schema
}
async getDefinition(datasource) {
if (!datasource?.id) {
return null
}
try {
const res = await this.API.viewV2.fetchDefinition(datasource.id)
return res?.data
} catch (error) {
this.store.update(state => ({
...state,
error,
}))
return null
}
}
getDefaultSortColumn() {
return null
}
async getData() {
const { datasource, limit, sortColumn, sortOrder, sortType, paginate } =
this.options
const { cursor, query } = get(this.store)
try {
const res = await this.API.viewV2.fetch({
viewId: datasource.id,
query,
paginate,
limit,
bookmark: cursor,
sort: sortColumn,
sortOrder: sortOrder?.toLowerCase(),
sortType,
})
return {
rows: res?.rows || [],
hasNextPage: res?.hasNextPage || false,
cursor: res?.bookmark || null,
}
} catch (error) {
return {
rows: [],
hasNextPage: false,
error,
}
}
}
}

View File

@ -1,5 +1,6 @@
import TableFetch from "./TableFetch.js" import TableFetch from "./TableFetch.js"
import ViewFetch from "./ViewFetch.js" import ViewFetch from "./ViewFetch.js"
import ViewV2Fetch from "./ViewV2Fetch.js"
import QueryFetch from "./QueryFetch.js" import QueryFetch from "./QueryFetch.js"
import RelationshipFetch from "./RelationshipFetch.js" import RelationshipFetch from "./RelationshipFetch.js"
import NestedProviderFetch from "./NestedProviderFetch.js" import NestedProviderFetch from "./NestedProviderFetch.js"
@ -11,6 +12,7 @@ import GroupUserFetch from "./GroupUserFetch.js"
const DataFetchMap = { const DataFetchMap = {
table: TableFetch, table: TableFetch,
view: ViewFetch, view: ViewFetch,
viewV2: ViewV2Fetch,
query: QueryFetch, query: QueryFetch,
link: RelationshipFetch, link: RelationshipFetch,
user: UserFetch, user: UserFetch,

View File

@ -17,7 +17,6 @@ import {
FetchDatasourceInfoRequest, FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
IntegrationBase, IntegrationBase,
RestConfig,
SourceName, SourceName,
UpdateDatasourceResponse, UpdateDatasourceResponse,
UserCtx, UserCtx,
@ -27,7 +26,6 @@ import {
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets" import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
import { areRESTVariablesValid } from "../../sdk/app/datasources/datasources"
function getErrorTables(errors: any, errorType: string) { function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors) return Object.entries(errors)

View File

@ -159,7 +159,7 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
for (let row of rows) { for (let row of rows) {
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, row)
gridSocket?.emitRowDeletion(ctx, row._id!) gridSocket?.emitRowDeletion(ctx, row)
} }
return rows return rows
@ -175,7 +175,7 @@ async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
await quotas.removeRow() await quotas.removeRow()
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row) ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row)
gridSocket?.emitRowDeletion(ctx, resp.row._id!) gridSocket?.emitRowDeletion(ctx, resp.row)
return resp return resp
} }

Some files were not shown because too many files have changed in this diff Show More