Merge remote-tracking branch 'origin/develop' into feat/per-app-builder-fe
This commit is contained in:
commit
2360e5d8a5
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.9.33-alpha.3",
|
"version": "2.9.33-alpha.6",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 />
|
|
@ -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>
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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 => ({
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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("../")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
$redirect("../")
|
||||||
|
</script>
|
|
@ -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 />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ViewV2DataTable />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
$redirect("../")
|
||||||
|
</script>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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}` })
|
||||||
|
},
|
||||||
|
})
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const createStores = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deriveStores = context => {
|
export const createActions = context => {
|
||||||
const {
|
const {
|
||||||
reorder,
|
reorder,
|
||||||
columns,
|
columns,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue