Merge remote-tracking branch 'origin/master' into feat/automation-store-ts-conversion

This commit is contained in:
Peter Clement 2024-12-23 09:06:30 +00:00
commit 3807568498
28 changed files with 555 additions and 219 deletions

View File

@ -12,6 +12,7 @@
export let name
export let config
export let showModal = () => {}
export let placeholder
const selectComponent = type => {
if (type === "object") {
@ -40,6 +41,7 @@
{name}
{config}
{showModal}
{placeholder}
on:blur
on:change
/>

View File

@ -5,11 +5,12 @@
export let name
export let value
export let error
export let placeholder
</script>
<div class="form-row">
<Label>{name}</Label>
<TextArea on:blur on:change {type} {value} {error} />
<TextArea on:blur on:change {type} {value} {error} {placeholder} />
</div>
<style>

View File

@ -6,6 +6,7 @@
export let value
export let error
export let config
export let placeholder
</script>
<div class="form-row">
@ -17,6 +18,7 @@
{type}
value={value || undefined}
{error}
{placeholder}
/>
</div>

View File

@ -6,6 +6,7 @@
export let name
export let value
export let error
export let placeholder
export let showModal = () => {}
async function handleUpgradePanel() {
@ -22,6 +23,7 @@
type={type === "port" ? "string" : type}
{value}
{error}
{placeholder}
variables={$environment.variables}
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
{showModal}

View File

@ -85,7 +85,7 @@
/>
{/if}
{#each $configStore.validatedConfig as { type, key, value, error, name, hidden, config }}
{#each $configStore.validatedConfig as { type, key, value, error, name, hidden, config, placeholder }}
{#if hidden === undefined || !eval(processStringSync(hidden, $configStore.config))}
<ConfigInput
{type}
@ -93,6 +93,7 @@
{error}
{name}
{config}
{placeholder}
showModal={() =>
showModal(newValue => configStore.updateFieldValue(key, newValue))}
on:blur={() => configStore.markFieldActive(key)}

View File

@ -114,6 +114,7 @@ export const createValidatedConfigStore = (integration, config) => {
value: getValue(),
error: $errorsStore[key],
name: capitalise(properties.display || key),
placeholder: properties.placeholder,
type: properties.type,
hidden: properties.hidden,
config: properties.config,

View File

@ -1,30 +1,64 @@
import { FieldType } from "@budibase/types"
import {
FieldSchema,
FieldType,
SaveTableRequest,
Table,
} from "@budibase/types"
import { SWITCHABLE_TYPES } from "@budibase/shared-core"
import { get, writable, derived } from "svelte/store"
import { get, derived, Writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import { API } from "api"
import { DerivedBudiStore } from "stores/BudiStore"
export function createTablesStore() {
const store = writable({
list: [],
selectedTableId: null,
})
const derivedStore = derived(store, $store => ({
...$store,
selected: $store.list?.find(table => table._id === $store.selectedTableId),
}))
interface BuilderTableStore {
list: Table[]
selectedTableId?: string
}
const fetch = async () => {
interface DerivedTableStore extends BuilderTableStore {
selected?: Table
}
export class TableStore extends DerivedBudiStore<
BuilderTableStore,
DerivedTableStore
> {
constructor() {
const makeDerivedStore = (store: Writable<BuilderTableStore>) => {
return derived(store, $store => ({
...$store,
selected: $store.list?.find(
table => table._id === $store.selectedTableId
),
}))
}
super(
{
list: [],
selectedTableId: undefined,
},
makeDerivedStore
)
this.select = this.select.bind(this)
}
async init() {
return this.fetch()
}
async fetch() {
const tables = await API.getTables()
store.update(state => ({
this.store.update(state => ({
...state,
list: tables,
}))
}
const singleFetch = async tableId => {
private async singleFetch(tableId: string) {
const table = await API.getTable(tableId)
store.update(state => {
this.store.update(state => {
const list = []
// update the list, keep order accurate
for (let tbl of state.list) {
@ -39,16 +73,16 @@ export function createTablesStore() {
})
}
const select = tableId => {
store.update(state => ({
select(tableId: string | undefined) {
this.store.update(state => ({
...state,
selectedTableId: tableId,
}))
}
const save = async table => {
const updatedTable = cloneDeep(table)
const oldTable = get(store).list.filter(t => t._id === table._id)[0]
async save(table: Table) {
const updatedTable: SaveTableRequest = cloneDeep(table)
const oldTable = get(this.store).list.filter(t => t._id === table._id)[0]
const fieldNames = []
// Update any renamed schema keys to reflect their names
@ -79,8 +113,8 @@ export function createTablesStore() {
}
const savedTable = await API.saveTable(updatedTable)
replaceTable(savedTable._id, savedTable)
select(savedTable._id)
this.replaceTable(savedTable._id, savedTable)
this.select(savedTable._id)
// make sure tables up to date (related)
let newTableIds = []
for (let column of Object.values(updatedTable?.schema || {})) {
@ -99,28 +133,30 @@ export function createTablesStore() {
const tableIdsToFetch = [...new Set([...newTableIds, ...oldTableIds])]
// too many tables to fetch, just get all
if (tableIdsToFetch.length > 3) {
await fetch()
await this.fetch()
} else {
await Promise.all(tableIdsToFetch.map(id => singleFetch(id)))
await Promise.all(tableIdsToFetch.map(id => this.singleFetch(id)))
}
return savedTable
}
const deleteTable = async table => {
if (!table?._id) {
return
}
await API.deleteTable(table._id, table._rev || "rev")
replaceTable(table._id, null)
async delete(table: { _id: string; _rev: string }) {
await API.deleteTable(table._id, table._rev)
this.replaceTable(table._id, null)
}
const saveField = async ({
async saveField({
originalName,
field,
primaryDisplay = false,
indexes,
}) => {
let draft = cloneDeep(get(derivedStore).selected)
}: {
originalName: string
field: FieldSchema
primaryDisplay: boolean
indexes: Record<string, any>
}) {
const draft: SaveTableRequest = cloneDeep(get(this.derivedStore).selected!)
// delete the original if renaming
// need to handle if the column had no name, empty string
@ -139,7 +175,7 @@ export function createTablesStore() {
const fields = Object.keys(draft.schema)
// pick another display column randomly if unselecting
draft.primaryDisplay = fields.filter(
name => name !== originalName || name !== field
name => name !== originalName || name !== field.name
)[0]
}
if (indexes) {
@ -150,24 +186,24 @@ export function createTablesStore() {
[field.name]: cloneDeep(field),
}
await save(draft)
await this.save(draft)
}
const deleteField = async field => {
let draft = cloneDeep(get(derivedStore).selected)
async deleteField(field: { name: string | number }) {
let draft = cloneDeep(get(this.derivedStore).selected!)
delete draft.schema[field.name]
await save(draft)
await this.save(draft)
}
// Handles external updates of tables
const replaceTable = (tableId, table) => {
replaceTable(tableId: string | undefined, table: Table | null) {
if (!tableId) {
return
}
// Handle deletion
if (!table) {
store.update(state => ({
this.store.update(state => ({
...state,
list: state.list.filter(x => x._id !== tableId),
}))
@ -175,9 +211,9 @@ export function createTablesStore() {
}
// Add new table
const index = get(store).list.findIndex(x => x._id === table._id)
const index = get(this.store).list.findIndex(x => x._id === table._id)
if (index === -1) {
store.update(state => ({
this.store.update(state => ({
...state,
list: [...state.list, table],
}))
@ -188,7 +224,7 @@ export function createTablesStore() {
// This function has to merge state as there discrepancies with the table
// API endpoints. The table list endpoint and get table endpoint use the
// "type" property to mean different things.
store.update(state => {
this.store.update(state => {
state.list[index] = {
...table,
type: state.list[index].type,
@ -198,26 +234,12 @@ export function createTablesStore() {
}
}
const removeDatasourceTables = datasourceId => {
store.update(state => ({
removeDatasourceTables(datasourceId: string) {
this.store.update(state => ({
...state,
list: state.list.filter(table => table.sourceId !== datasourceId),
}))
}
return {
...store,
subscribe: derivedStore.subscribe,
fetch,
init: fetch,
select,
save,
delete: deleteTable,
saveField,
deleteField,
replaceTable,
removeDatasourceTables,
}
}
export const tables = createTablesStore()
export const tables = new TableStore()

View File

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

View File

@ -0,0 +1,122 @@
import { derived, get, Writable } from "svelte/store"
import { tables } from "./tables"
import { API } from "api"
import { DerivedBudiStore } from "stores/BudiStore"
import { CreateViewRequest, UpdateViewRequest, ViewV2 } from "@budibase/types"
import { helpers } from "@budibase/shared-core"
interface BuilderViewV2Store {
selectedViewId: string | null
}
interface DerivedViewV2Store extends BuilderViewV2Store {
list: ViewV2[]
selected?: ViewV2
}
export class ViewV2Store extends DerivedBudiStore<
BuilderViewV2Store,
DerivedViewV2Store
> {
constructor() {
const makeDerivedStore = (store: Writable<BuilderViewV2Store>) => {
return derived(
[store, tables],
([$store, $tables]): DerivedViewV2Store => {
let list: ViewV2[] = []
$tables.list?.forEach(table => {
const views = Object.values(table?.views || {}).filter(
helpers.views.isV2
)
list = list.concat(views)
})
return {
...$store,
list,
selected: list.find(view => view.id === $store.selectedViewId),
}
}
)
}
super(
{
selectedViewId: null,
},
makeDerivedStore
)
this.select = this.select.bind(this)
}
select(id: string) {
this.store.update(state => ({
...state,
selectedViewId: id,
}))
}
async delete(view: { id: string }) {
await API.viewV2.delete(view.id)
this.replaceView(view.id, null)
}
async create(view: CreateViewRequest) {
const savedViewResponse = await API.viewV2.create(view)
const savedView = savedViewResponse.data
this.replaceView(savedView.id, savedView)
return savedView
}
async save(view: UpdateViewRequest) {
const res = await API.viewV2.update(view)
const savedView = res?.data
this.replaceView(view.id, savedView)
}
// Handles external updates of tables
replaceView(viewId: string, view: ViewV2 | null) {
const existingView = get(this.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 && existingView) {
tables.update(state => {
delete state.list[tableIndex].views![existingView.name]
return state
})
return
}
// Add new view
else if (!existingView && view) {
tables.update(state => {
state.list[tableIndex].views ??= {}
state.list[tableIndex].views[view.name] = view
return state
})
}
// Update existing view
else if (existingView && view) {
tables.update(state => {
// Remove old view
state.list[tableIndex].views ??= {}
delete state.list[tableIndex].views[existingView.name]
// Add new view
state.list[tableIndex].views[view.name] = view
return state
})
}
}
}
export const viewsV2 = new ViewV2Store()

View File

@ -1,6 +1,7 @@
{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"assets/*": ["./assets/*"],

View File

@ -1,8 +1,25 @@
import { derived, get, writable } from "svelte/store"
import { derived, get, Readable, Writable, writable } from "svelte/store"
import { DefaultColumnWidth, GutterWidth } from "../lib/constants"
import { UIColumn } from "@budibase/types"
import { Store as StoreContext } from "."
export const createStores = () => {
const columns = writable([])
interface ColumnStore {
columns: Writable<UIColumn[]>
}
interface DerivedColumnStore {
tableColumns: Readable<UIColumn[]>
displayColumn: Readable<UIColumn | undefined>
columnLookupMap: Readable<Record<string, UIColumn>>
visibleColumns: Readable<UIColumn[]>
scrollableColumns: Readable<UIColumn[]>
hasNonAutoColumn: Readable<boolean>
}
export type Store = ColumnStore & DerivedColumnStore
export const createStores = (): ColumnStore => {
const columns = writable<UIColumn[]>([])
// Enrich columns with metadata about their display position
const enrichedColumns = derived(columns, $columns => {
@ -16,7 +33,7 @@ export const createStores = () => {
}
if (col.visible) {
idx++
offset += col.width
offset += col.width ?? 0
}
return enriched
})
@ -30,12 +47,12 @@ export const createStores = () => {
}
}
export const deriveStores = context => {
export const deriveStores = (context: StoreContext): DerivedColumnStore => {
const { columns } = context
// Derive a lookup map for all columns by name
const columnLookupMap = derived(columns, $columns => {
let map = {}
let map: Record<string, UIColumn> = {}
$columns.forEach(column => {
map[column.name] = column
})
@ -78,11 +95,11 @@ export const deriveStores = context => {
}
}
export const createActions = context => {
export const createActions = (context: StoreContext) => {
const { columns, datasource } = context
// Updates the width of all columns
const changeAllColumnWidths = async width => {
const changeAllColumnWidths = async (width: number) => {
const $columns = get(columns)
$columns.forEach(column => {
const { related } = column
@ -101,7 +118,7 @@ export const createActions = context => {
}
// Checks if a column is readonly
const isReadonly = column => {
const isReadonly = (column: UIColumn) => {
if (!column?.schema) {
return false
}
@ -125,11 +142,11 @@ export const createActions = context => {
}
}
export const initialise = context => {
export const initialise = (context: StoreContext) => {
const { definition, columns, displayColumn, enrichedSchema } = context
// Merge new schema fields with existing schema in order to preserve widths
const processColumns = $enrichedSchema => {
const processColumns = ($enrichedSchema: any) => {
if (!$enrichedSchema) {
columns.set([])
return
@ -139,7 +156,7 @@ export const initialise = context => {
const $displayColumn = get(displayColumn)
// Find primary display
let primaryDisplay
let primaryDisplay: string
const candidatePD = $definition.primaryDisplay || $displayColumn?.name
if (candidatePD && $enrichedSchema[candidatePD]) {
primaryDisplay = candidatePD
@ -151,7 +168,8 @@ export const initialise = context => {
.map(field => {
const fieldSchema = $enrichedSchema[field]
const oldColumn = $columns?.find(col => col.name === field)
const column = {
const column: UIColumn = {
type: fieldSchema.type,
name: field,
label: fieldSchema.displayName || field,
schema: fieldSchema,

View File

@ -1,7 +1,24 @@
import { SortOrder } from "@budibase/types"
import { SortOrder, UIDatasource } from "@budibase/types"
import { get } from "svelte/store"
import { Store as StoreContext } from ".."
export const createActions = context => {
interface NonPlusActions {
nonPlus: {
actions: {
saveDefinition: () => Promise<void>
addRow: () => Promise<void>
updateRow: () => Promise<void>
deleteRows: () => Promise<void>
getRow: () => Promise<void>
isDatasourceValid: (datasource: UIDatasource) => boolean
canUseColumn: (name: string) => boolean
}
}
}
export type Store = NonPlusActions
export const createActions = (context: StoreContext): NonPlusActions => {
const { columns, table, viewV2 } = context
const saveDefinition = async () => {
@ -20,7 +37,7 @@ export const createActions = context => {
throw "This datasource does not support fetching individual rows"
}
const isDatasourceValid = datasource => {
const isDatasourceValid = (datasource: UIDatasource) => {
// There are many different types and shapes of datasource, so we only
// check that we aren't null
return (
@ -30,7 +47,7 @@ export const createActions = context => {
)
}
const canUseColumn = name => {
const canUseColumn = (name: string) => {
return get(columns).some(col => col.name === name)
}
@ -50,11 +67,11 @@ export const createActions = context => {
}
// Small util to compare datasource definitions
const isSameDatasource = (a, b) => {
const isSameDatasource = (a: any, b: any) => {
return JSON.stringify(a) === JSON.stringify(b)
}
export const initialise = context => {
export const initialise = (context: StoreContext) => {
const {
datasource,
sort,
@ -69,7 +86,7 @@ export const initialise = context => {
} = context
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
let unsubscribers: any[] = []
// Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => {

View File

@ -1,16 +1,40 @@
import { SortOrder } from "@budibase/types"
import {
Row,
SaveRowRequest,
SaveRowResponse,
SaveTableRequest,
SortOrder,
UIDatasource,
} from "@budibase/types"
import { get } from "svelte/store"
import { Store as StoreContext } from ".."
const SuppressErrors = true
export const createActions = context => {
interface TableActions {
table: {
actions: {
saveDefinition: (newDefinition: SaveTableRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<SaveRowResponse>
updateRow: (row: SaveRowRequest) => Promise<SaveRowResponse>
deleteRows: (rows: (string | Row)[]) => Promise<void>
getRow: (id: string) => Promise<Row>
isDatasourceValid: (datasource: UIDatasource) => boolean
canUseColumn: (name: string) => boolean
}
}
}
export type Store = TableActions
export const createActions = (context: StoreContext): TableActions => {
const { API, datasource, columns } = context
const saveDefinition = async newDefinition => {
const saveDefinition = async (newDefinition: SaveTableRequest) => {
await API.saveTable(newDefinition)
}
const saveRow = async row => {
const saveRow = async (row: SaveRowRequest) => {
row = {
...row,
tableId: get(datasource)?.tableId,
@ -18,15 +42,15 @@ export const createActions = context => {
return await API.saveRow(row, SuppressErrors)
}
const deleteRows = async rows => {
const deleteRows = async (rows: (string | Row)[]) => {
await API.deleteRows(get(datasource).tableId, rows)
}
const isDatasourceValid = datasource => {
return datasource?.type === "table" && datasource?.tableId
const isDatasourceValid = (datasource: UIDatasource) => {
return datasource?.type === "table" && !!datasource?.tableId
}
const getRow = async id => {
const getRow = async (id: any) => {
const res = await API.searchTable(get(datasource).tableId, {
limit: 1,
query: {
@ -39,7 +63,7 @@ export const createActions = context => {
return res?.rows?.[0]
}
const canUseColumn = name => {
const canUseColumn = (name: string) => {
return get(columns).some(col => col.name === name)
}
@ -58,7 +82,7 @@ export const createActions = context => {
}
}
export const initialise = context => {
export const initialise = (context: StoreContext) => {
const {
datasource,
fetch,
@ -74,7 +98,7 @@ export const initialise = context => {
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
let unsubscribers: any[] = []
// Observe datasource changes and apply logic for table datasources
datasource.subscribe($datasource => {

View File

@ -1,16 +1,39 @@
import { get } from "svelte/store"
import { SortOrder } from "@budibase/types"
import {
Row,
SaveRowRequest,
SortOrder,
UIDatasource,
UpdateViewRequest,
} from "@budibase/types"
import { Store as StoreContext } from ".."
const SuppressErrors = true
export const createActions = context => {
interface ViewActions {
viewV2: {
actions: {
saveDefinition: (newDefinition: UpdateViewRequest) => Promise<void>
addRow: (row: SaveRowRequest) => Promise<Row>
updateRow: (row: SaveRowRequest) => Promise<Row>
deleteRows: (rows: (string | Row)[]) => Promise<void>
getRow: (id: string) => Promise<Row>
isDatasourceValid: (datasource: UIDatasource) => boolean
canUseColumn: (name: string) => boolean
}
}
}
export type Store = ViewActions
export const createActions = (context: StoreContext): ViewActions => {
const { API, datasource, columns } = context
const saveDefinition = async newDefinition => {
const saveDefinition = async (newDefinition: UpdateViewRequest) => {
await API.viewV2.update(newDefinition)
}
const saveRow = async row => {
const saveRow = async (row: SaveRowRequest) => {
const $datasource = get(datasource)
row = {
...row,
@ -23,11 +46,11 @@ export const createActions = context => {
}
}
const deleteRows = async rows => {
const deleteRows = async (rows: (string | Row)[]) => {
await API.deleteRows(get(datasource).id, rows)
}
const getRow = async id => {
const getRow = async (id: string) => {
const res = await API.viewV2.fetch(get(datasource).id, {
limit: 1,
query: {
@ -40,13 +63,13 @@ export const createActions = context => {
return res?.rows?.[0]
}
const isDatasourceValid = datasource => {
const isDatasourceValid = (datasource: UIDatasource) => {
return (
datasource?.type === "viewV2" && datasource?.id && datasource?.tableId
datasource?.type === "viewV2" && !!datasource?.id && !!datasource?.tableId
)
}
const canUseColumn = name => {
const canUseColumn = (name: string) => {
return get(columns).some(col => col.name === name && col.visible)
}
@ -65,7 +88,7 @@ export const createActions = context => {
}
}
export const initialise = context => {
export const initialise = (context: StoreContext) => {
const {
definition,
datasource,
@ -85,7 +108,7 @@ export const initialise = context => {
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
let unsubscribers: any[] = []
// Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => {

View File

@ -1,3 +1,6 @@
import { Writable } from "svelte/store"
import type { APIClient } from "../../../api/types"
import * as Bounds from "./bounds"
import * as Columns from "./columns"
import * as Menu from "./menu"
@ -42,31 +45,65 @@ const DependencyOrderedStores = [
Users,
Menu,
Pagination,
Config,
Config as any,
Clipboard,
Notifications,
Cache,
]
export const attachStores = context => {
export interface BaseStore {
API: APIClient
}
export type Store = BaseStore &
Columns.Store &
Table.Store &
ViewV2.Store &
NonPlus.Store & {
// TODO while typing the rest of stores
datasource: Writable<any> & { actions: any }
definition: Writable<any>
enrichedSchema: any
fetch: Writable<any>
filter: Writable<any>
inlineFilters: Writable<any>
allFilters: Writable<any>
sort: Writable<any>
initialFilter: Writable<any>
initialSortColumn: Writable<any>
initialSortOrder: Writable<any>
rows: Writable<any> & { actions: any }
subscribe: any
config: Writable<any>
}
export const attachStores = (context: Store): Store => {
// Atomic store creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.createStores?.(context) }
if ("createStores" in store) {
context = { ...context, ...store.createStores?.(context) }
}
}
// Derived store creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.deriveStores?.(context) }
if ("deriveStores" in store) {
context = { ...context, ...store.deriveStores?.(context) }
}
}
// Action creation
for (let store of DependencyOrderedStores) {
context = { ...context, ...store.createActions?.(context) }
if ("createActions" in store) {
context = { ...context, ...store.createActions?.(context) }
}
}
// Initialise any store logic
for (let store of DependencyOrderedStores) {
store.initialise?.(context)
if ("initialise" in store) {
store.initialise?.(context)
}
}
return context

@ -1 +1 @@
Subproject commit 7fc699463b3957eb050351b983edef0d25a531ae
Subproject commit ae786121d923449b0ad5fcbd123d0a9fec28f65e

View File

@ -37,6 +37,7 @@ import { jsonFromCsvString } from "../../../utilities/csv"
import { builderSocket } from "../../../websockets"
import { cloneDeep } from "lodash"
import {
canBeDisplayColumn,
helpers,
PROTECTED_EXTERNAL_COLUMNS,
PROTECTED_INTERNAL_COLUMNS,
@ -67,6 +68,27 @@ function checkDefaultFields(table: Table) {
}
}
async function guardTable(table: Table, isCreate: boolean) {
checkDefaultFields(table)
if (
table.primaryDisplay &&
!canBeDisplayColumn(table.schema[table.primaryDisplay]?.type)
) {
// Prevent throwing errors from existing badly configured tables. Only throw for new tables or if this setting is being updated
if (
isCreate ||
(await sdk.tables.getTable(table._id!)).primaryDisplay !==
table.primaryDisplay
) {
throw new HTTPError(
`Column "${table.primaryDisplay}" cannot be used as a display type.`,
400
)
}
}
}
// covers both internal and external
export async function fetch(ctx: UserCtx<void, FetchTablesResponse>) {
const internal = await sdk.tables.getAllInternalTables()
@ -111,7 +133,7 @@ export async function save(ctx: UserCtx<SaveTableRequest, SaveTableResponse>) {
const isCreate = !table._id
checkDefaultFields(table)
await guardTable(table, isCreate)
let savedTable: Table
if (isCreate) {

View File

@ -3399,7 +3399,7 @@ if (descriptions.length) {
type: FieldType.LINK,
relationshipType: RelationshipType.MANY_TO_ONE,
tableId: toRelateTableId,
fieldName: "link",
fieldName: "main",
},
})
@ -3408,7 +3408,7 @@ if (descriptions.length) {
)
await config.api.table.save({
...toRelateTable,
primaryDisplay: "link",
primaryDisplay: "name",
})
const relatedRows = await Promise.all([
config.api.row.save(toRelateTable._id!, {

View File

@ -185,6 +185,62 @@ if (descriptions.length) {
)
}
)
it("can set primary display", async () => {
const columnName = generator.word()
const table = await config.api.table.save(
tableForDatasource(datasource, {
primaryDisplay: columnName,
schema: {
[columnName]: {
name: columnName,
type: FieldType.STRING,
},
},
})
)
expect(table.primaryDisplay).toEqual(columnName)
const res = await config.api.table.get(table._id!)
expect(res.primaryDisplay).toEqual(columnName)
})
it("cannot use unexisting columns as primary display", async () => {
const columnName = generator.word()
await config.api.table.save(
tableForDatasource(datasource, {
primaryDisplay: columnName,
}),
{
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
}
)
})
it("cannot use invalid column types as display name", async () => {
const columnName = generator.word()
await config.api.table.save(
tableForDatasource(datasource, {
primaryDisplay: columnName,
schema: {
[columnName]: {
name: columnName,
type: FieldType.BOOLEAN,
},
},
}),
{
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
}
)
})
})
describe("permissions", () => {
@ -603,6 +659,49 @@ if (descriptions.length) {
}
expect(response).toEqual(expectedResponse)
})
it("cannot use unexisting columns as primary display", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource)
)
const columnName = generator.word()
const tableRequest = {
...table,
primaryDisplay: columnName,
}
await config.api.table.save(tableRequest, {
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
})
})
it("cannot use invalid column types as display name", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource)
)
const columnName = generator.word()
const tableRequest: SaveTableRequest = {
...table,
primaryDisplay: columnName,
schema: {
...table.schema,
[columnName]: {
name: columnName,
type: FieldType.BOOLEAN,
},
},
}
await config.api.table.save(tableRequest, {
status: 400,
body: {
message: `Column "${columnName}" cannot be used as a display type.`,
},
})
})
})
describe("import", () => {

View File

@ -12,6 +12,7 @@ interface FirebaseConfig {
email: string
privateKey: string
projectId: string
databaseId?: string
}
const SCHEMA: Integration = {
@ -30,12 +31,21 @@ const SCHEMA: Integration = {
},
privateKey: {
type: DatasourceFieldType.STRING,
display: "Private Key",
required: true,
},
projectId: {
type: DatasourceFieldType.STRING,
display: "Project ID",
required: true,
},
databaseId: {
type: DatasourceFieldType.STRING,
display: "Database ID",
required: false,
default: "(default)",
placeholder: "(default)",
},
},
query: {
create: {
@ -97,6 +107,7 @@ class FirebaseIntegration implements IntegrationBase {
this.config = config
this.client = new Firestore({
projectId: config.projectId,
databaseId: config.databaseId || "(default)",
credentials: {
client_email: config.email,
private_key: config.privateKey?.replace(/\\n/g, "\n"),

View File

@ -324,8 +324,8 @@ export async function update(
return pickApi(tableId).update(tableId, view)
}
export function isV2(view: View | ViewV2): view is ViewV2 {
return (view as ViewV2).version === 2
export function isV2(view: View | ViewV2) {
return helpers.views.isV2(view)
}
export async function remove(viewId: string): Promise<ViewV2> {

View File

@ -1,5 +1,6 @@
import {
BasicViewFieldMetadata,
View,
ViewCalculationFieldMetadata,
ViewFieldMetadata,
ViewV2,
@ -43,3 +44,7 @@ export function basicFields(view: UnsavedViewV2, opts?: { visible?: boolean }) {
return !isCalculationField(field) && (!visible || isVisible(field))
})
}
export function isV2(view: View | ViewV2): view is ViewV2 {
return (view as ViewV2).version === 2
}

View File

@ -12,8 +12,8 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
[FieldType.AUTO]: true,
[FieldType.INTERNAL]: true,
[FieldType.BARCODEQR]: true,
[FieldType.BIGINT]: true,
[FieldType.BOOLEAN]: false,
[FieldType.ARRAY]: false,
[FieldType.ATTACHMENTS]: false,

View File

@ -119,6 +119,7 @@ interface DatasourceBasicFieldConfig {
default?: any
deprecated?: boolean
hidden?: string
placeholder?: string
}
interface DatasourceSelectFieldConfig extends DatasourceBasicFieldConfig {

View File

@ -0,0 +1,19 @@
import { CalculationType, FieldSchema, FieldType } from "@budibase/types"
export type UIColumn = FieldSchema & {
label: string
readonly: boolean
conditions: any
related?: {
field: string
subField: string
}
primaryDisplay?: boolean
schema?: {
disabled: boolean
type: FieldType
readonly: boolean
autocolumn: boolean
}
calculationType: CalculationType
}

View File

@ -0,0 +1,5 @@
export interface UIDatasource {
type: string
id: string
tableId: string
}

View File

@ -0,0 +1,2 @@
export * from "./columns"
export * from "./datasource"

View File

@ -1,2 +1,3 @@
export * from "./integration"
export * from "./automations"
export * from "./grid"