Merge develop into editable text lab day
This commit is contained in:
commit
2adbaea026
|
@ -3,6 +3,7 @@ builder/*
|
|||
.temp/
|
||||
packages/server/runtime_apps/
|
||||
.idea/
|
||||
bb-airgapped.tar.gz
|
||||
|
||||
# Logs
|
||||
logs
|
||||
|
|
|
@ -45,6 +45,7 @@ services:
|
|||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
MINIO_URL: http://minio-service:9000
|
||||
APPS_URL: http://app-service:4002
|
||||
COUCH_DB_USERNAME: ${COUCH_DB_USER}
|
||||
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
|
||||
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
|
||||
|
|
|
@ -113,6 +113,8 @@ spec:
|
|||
value: {{ .Values.globals.smtp.port | quote }}
|
||||
- name: SMTP_FROM_ADDRESS
|
||||
value: {{ .Values.globals.smtp.from | quote }}
|
||||
- name: APPS_URL
|
||||
value: http://app-service:{{ .Values.services.apps.port }}
|
||||
image: budibase/worker
|
||||
imagePullPolicy: Always
|
||||
name: bbworker
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
const fs = require("fs")
|
||||
const { execSync } = require("child_process")
|
||||
const path = require("path")
|
||||
|
||||
const IMAGES = {
|
||||
worker: "budibase/worker",
|
||||
apps: "budibase/apps",
|
||||
proxy: "envoyproxy/envoy:v1.16-latest",
|
||||
minio: "minio/minio",
|
||||
couch: "ibmcom/couchdb3",
|
||||
curl: "curlimages/curl",
|
||||
redis: "redis",
|
||||
watchtower: "containrrr/watchtower"
|
||||
}
|
||||
|
||||
const FILES = {
|
||||
COMPOSE: "docker-compose.yaml",
|
||||
ENVOY: "envoy.yaml",
|
||||
PROPERTIES: "hosting.properties"
|
||||
}
|
||||
|
||||
const OUTPUT_DIR = path.join(__dirname, "../", "bb-airgapped")
|
||||
|
||||
function copyFile(file) {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, "../", "../", file),
|
||||
path.join(OUTPUT_DIR, file)
|
||||
)
|
||||
}
|
||||
|
||||
// create output dir
|
||||
console.log(`Creating ${OUTPUT_DIR} for build..`)
|
||||
fs.rmdirSync(OUTPUT_DIR, { recursive: true })
|
||||
fs.mkdirSync(OUTPUT_DIR)
|
||||
|
||||
// package images into tar files
|
||||
for (let image in IMAGES) {
|
||||
console.log(`Creating tar for ${image}..`)
|
||||
execSync(`docker save ${IMAGES[image]} -o ${OUTPUT_DIR}/${image}.tar`)
|
||||
}
|
||||
|
||||
// copy config files
|
||||
copyFile(FILES.COMPOSE)
|
||||
copyFile(FILES.ENVOY)
|
||||
copyFile(FILES.PROPERTIES)
|
||||
|
||||
// compress
|
||||
execSync(`tar -czf bb-airgapped.tar.gz hosting/scripts/bb-airgapped`)
|
||||
|
||||
// clean up
|
||||
fs.rmdirSync(OUTPUT_DIR, { recursive: true })
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "0.9.173-alpha.5",
|
||||
"version": "0.9.185-alpha.2",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"build:docker": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION && cd -",
|
||||
"build:docker:production": "lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh $BUDIBASE_RELEASE_VERSION release && cd -",
|
||||
"build:docker:develop": "node scripts/pinVersions && lerna run build:docker && cd hosting/scripts/linux/ && ./release-to-docker-hub.sh develop && cd -",
|
||||
"build:docker:airgap": "node hosting/scripts/airgapped/airgappedDockerBuild",
|
||||
"release:helm": "./scripts/release_helm_chart.sh",
|
||||
"env:multi:enable": "lerna run env:multi:enable",
|
||||
"env:multi:disable": "lerna run env:multi:disable",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "0.9.173-alpha.5",
|
||||
"version": "0.9.185-alpha.2",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -6,6 +6,7 @@ exports.UserStatus = {
|
|||
exports.Cookies = {
|
||||
CurrentApp: "budibase:currentapp",
|
||||
Auth: "budibase:auth",
|
||||
Init: "budibase:init",
|
||||
OIDC_CONFIG: "budibase:oidc:config",
|
||||
}
|
||||
|
||||
|
|
|
@ -152,6 +152,17 @@ exports.getDeployedAppID = appId => {
|
|||
return appId
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a deployed app ID to a development app ID.
|
||||
*/
|
||||
exports.getDevelopmentAppID = appId => {
|
||||
if (!appId.startsWith(exports.APP_DEV_PREFIX)) {
|
||||
const id = appId.split(exports.APP_PREFIX)[1]
|
||||
return `${exports.APP_DEV_PREFIX}${id}`
|
||||
}
|
||||
return appId
|
||||
}
|
||||
|
||||
exports.getCouchUrl = () => {
|
||||
if (!env.COUCH_DB_URL) return
|
||||
|
||||
|
@ -248,6 +259,24 @@ exports.getAllApps = async (CouchDB, { dev, all, idsOnly } = {}) => {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for getAllApps but filters to production apps only.
|
||||
*/
|
||||
exports.getDeployedAppIDs = async CouchDB => {
|
||||
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(
|
||||
id => !exports.isDevAppID(id)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for the inverse of above.
|
||||
*/
|
||||
exports.getDevAppIDs = async CouchDB => {
|
||||
return (await exports.getAllApps(CouchDB, { idsOnly: true })).filter(id =>
|
||||
exports.isDevAppID(id)
|
||||
)
|
||||
}
|
||||
|
||||
exports.dbExists = async (CouchDB, dbName) => {
|
||||
let exists = false
|
||||
try {
|
||||
|
|
|
@ -9,6 +9,8 @@ const { createASession } = require("../../security/sessions")
|
|||
const { getTenantId } = require("../../tenancy")
|
||||
|
||||
const INVALID_ERR = "Invalid Credentials"
|
||||
const SSO_NO_PASSWORD = "SSO user does not have a password set"
|
||||
const EXPIRED = "This account has expired. Please reset your password"
|
||||
|
||||
exports.options = {
|
||||
passReqToCallback: true,
|
||||
|
@ -36,6 +38,19 @@ exports.authenticate = async function (ctx, email, password, done) {
|
|||
return authError(done, INVALID_ERR)
|
||||
}
|
||||
|
||||
// check that the user has a stored password before proceeding
|
||||
if (!dbUser.password) {
|
||||
if (
|
||||
(dbUser.account && dbUser.account.authType === "sso") || // root account sso
|
||||
dbUser.thirdPartyProfile // internal sso
|
||||
) {
|
||||
return authError(done, SSO_NO_PASSWORD)
|
||||
}
|
||||
|
||||
console.error("Non SSO usser has no password set", dbUser)
|
||||
return authError(done, EXPIRED)
|
||||
}
|
||||
|
||||
// authenticate
|
||||
if (await compare(password, dbUser.password)) {
|
||||
const sessionId = newid()
|
||||
|
|
|
@ -181,8 +181,8 @@ exports.saveUser = async (
|
|||
|
||||
// check budibase users in other tenants
|
||||
if (env.MULTI_TENANCY) {
|
||||
dbUser = await getTenantUser(email)
|
||||
if (dbUser != null && dbUser.tenantId !== tenantId) {
|
||||
const tenantUser = await getTenantUser(email)
|
||||
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
||||
throw `Email address ${email} already in use.`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "0.9.173-alpha.5",
|
||||
"version": "0.9.185-alpha.2",
|
||||
"license": "AGPL-3.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -36,5 +36,7 @@
|
|||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
on:change={onChange}
|
||||
on:pick
|
||||
on:type
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
<label
|
||||
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
||||
class:is-invalid={!!error}
|
||||
class:checked={value}
|
||||
>
|
||||
<input
|
||||
checked={value}
|
||||
|
@ -50,6 +51,16 @@
|
|||
</label>
|
||||
|
||||
<style>
|
||||
.spectrum-Checkbox--sizeL .spectrum-Checkbox-checkmark {
|
||||
transform: scale(1.1);
|
||||
left: 55%;
|
||||
top: 55%;
|
||||
}
|
||||
.spectrum-Checkbox--sizeXL .spectrum-Checkbox-checkmark {
|
||||
transform: scale(1.2);
|
||||
left: 60%;
|
||||
top: 60%;
|
||||
}
|
||||
.spectrum-Checkbox-input {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -40,8 +40,15 @@
|
|||
open = false
|
||||
}
|
||||
|
||||
const onChange = e => {
|
||||
selectOption(e.target.value)
|
||||
const onType = e => {
|
||||
const value = e.target.value
|
||||
dispatch("type", value)
|
||||
selectOption(value)
|
||||
}
|
||||
|
||||
const onPick = value => {
|
||||
dispatch("pick", value)
|
||||
selectOption(value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -62,7 +69,7 @@
|
|||
type="text"
|
||||
on:focus={() => (focus = true)}
|
||||
on:blur={() => (focus = false)}
|
||||
on:change={onChange}
|
||||
on:change={onType}
|
||||
value={value || ""}
|
||||
placeholder={placeholder || ""}
|
||||
{disabled}
|
||||
|
@ -99,7 +106,7 @@
|
|||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => selectOption(getOptionValue(option))}
|
||||
on:click={() => onPick(getOptionValue(option))}
|
||||
>
|
||||
<span class="spectrum-Menu-itemLabel"
|
||||
>{getOptionLabel(option)}</span
|
||||
|
|
|
@ -47,5 +47,6 @@
|
|||
--spectrum-semantic-positive-border-color: #2d9d78;
|
||||
--spectrum-semantic-positive-icon-color: #2d9d78;
|
||||
--spectrum-semantic-negative-icon-color: #e34850;
|
||||
min-width: 100px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,8 @@ it("should rename an unpublished application", () => {
|
|||
renameApp(appRename)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
|
||||
})
|
||||
cy.deleteApp(appRename)
|
||||
})
|
||||
|
||||
xit("Should rename a published application", () => {
|
||||
// It is not possible to rename a published application
|
||||
|
|
|
@ -43,24 +43,26 @@ Cypress.Commands.add("createApp", name => {
|
|||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("deleteApp", () => {
|
||||
Cypress.Commands.add("deleteApp", appName => {
|
||||
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
|
||||
cy.wait(1000)
|
||||
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
console.log(val)
|
||||
if (val.length > 0) {
|
||||
cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
|
||||
cy.contains("Delete").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("input").type(appName)
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createTestApp", () => {
|
||||
const appName = "Cypress Tests"
|
||||
cy.deleteApp()
|
||||
cy.deleteApp(appName)
|
||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "0.9.173-alpha.5",
|
||||
"version": "0.9.185-alpha.2",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.173-alpha.5",
|
||||
"@budibase/client": "^0.9.173-alpha.5",
|
||||
"@budibase/bbui": "^0.9.185-alpha.2",
|
||||
"@budibase/client": "^0.9.185-alpha.2",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^0.9.173-alpha.5",
|
||||
"@budibase/string-templates": "^0.9.185-alpha.2",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -12,7 +12,7 @@ export default class PosthogClient {
|
|||
|
||||
posthog.init(this.token, {
|
||||
autocapture: false,
|
||||
capture_pageview: false,
|
||||
capture_pageview: true,
|
||||
api_host: this.url,
|
||||
})
|
||||
posthog.set_config({ persistence: "cookie" })
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
findComponentPath,
|
||||
getComponentSettings,
|
||||
} from "./storeUtils"
|
||||
import { store } from "builderStore"
|
||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||
|
@ -30,14 +31,33 @@ export const getBindableProperties = (asset, componentId) => {
|
|||
const deviceBindings = getDeviceBindings()
|
||||
const stateBindings = getStateBindings()
|
||||
return [
|
||||
...stateBindings,
|
||||
...deviceBindings,
|
||||
...urlBindings,
|
||||
...contextBindings,
|
||||
...urlBindings,
|
||||
...stateBindings,
|
||||
...userBindings,
|
||||
...deviceBindings,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the bindable properties exposed by a certain component.
|
||||
*/
|
||||
export const getComponentBindableProperties = (asset, componentId) => {
|
||||
if (!asset || !componentId) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Ensure that the component exists and exposes context
|
||||
const component = findComponent(asset.props, componentId)
|
||||
const def = store.actions.components.getDefinition(component?._component)
|
||||
if (!def?.context) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get the bindings for the component
|
||||
return getProviderContextBindings(asset, component)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all data provider components above a component.
|
||||
*/
|
||||
|
@ -82,13 +102,10 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
|
|||
* Gets a datasource object for a certain data provider component
|
||||
*/
|
||||
export const getDatasourceForProvider = (asset, component) => {
|
||||
const def = store.actions.components.getDefinition(component?._component)
|
||||
if (!def) {
|
||||
return null
|
||||
}
|
||||
const settings = getComponentSettings(component?._component)
|
||||
|
||||
// If this component has a dataProvider setting, go up the stack and use it
|
||||
const dataProviderSetting = def.settings.find(setting => {
|
||||
const dataProviderSetting = settings.find(setting => {
|
||||
return setting.type === "dataProvider"
|
||||
})
|
||||
if (dataProviderSetting) {
|
||||
|
@ -100,7 +117,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
|
||||
// Extract datasource from component instance
|
||||
const validSettingTypes = ["dataSource", "table", "schema"]
|
||||
const datasourceSetting = def.settings.find(setting => {
|
||||
const datasourceSetting = settings.find(setting => {
|
||||
return validSettingTypes.includes(setting.type)
|
||||
})
|
||||
if (!datasourceSetting) {
|
||||
|
@ -127,9 +144,26 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
const getContextBindings = (asset, componentId) => {
|
||||
// Extract any components which provide data contexts
|
||||
const dataProviders = getDataProviderComponents(asset, componentId)
|
||||
let bindings = []
|
||||
|
||||
// Generate bindings for all matching components
|
||||
return getProviderContextBindings(asset, dataProviders)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the context bindings exposed by a set of data provider components.
|
||||
*/
|
||||
const getProviderContextBindings = (asset, dataProviders) => {
|
||||
if (!asset || !dataProviders) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Ensure providers is an array
|
||||
if (!Array.isArray(dataProviders)) {
|
||||
dataProviders = [dataProviders]
|
||||
}
|
||||
|
||||
// Create bindings for each data provider
|
||||
let bindings = []
|
||||
dataProviders.forEach(component => {
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||
|
@ -142,6 +176,7 @@ const getContextBindings = (asset, componentId) => {
|
|||
|
||||
let schema
|
||||
let readablePrefix
|
||||
let runtimeSuffix = context.suffix
|
||||
|
||||
if (context.type === "form") {
|
||||
// Forms do not need table schemas
|
||||
|
@ -171,22 +206,19 @@ const getContextBindings = (asset, componentId) => {
|
|||
|
||||
const keys = Object.keys(schema).sort()
|
||||
|
||||
// Generate safe unique runtime prefix
|
||||
let providerId = component._id
|
||||
if (runtimeSuffix) {
|
||||
providerId += `-${runtimeSuffix}`
|
||||
}
|
||||
const safeComponentId = makePropSafe(providerId)
|
||||
|
||||
// Create bindable properties for each schema field
|
||||
const safeComponentId = makePropSafe(component._id)
|
||||
keys.forEach(key => {
|
||||
const fieldSchema = schema[key]
|
||||
|
||||
// Make safe runtime binding and replace certain bindings with a
|
||||
// new property to help display components
|
||||
let runtimeBoundKey = key
|
||||
if (fieldSchema.type === "link") {
|
||||
runtimeBoundKey = `${key}_text`
|
||||
} else if (fieldSchema.type === "attachment") {
|
||||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
const runtimeBinding = `${safeComponentId}.${makePropSafe(
|
||||
runtimeBoundKey
|
||||
)}`
|
||||
// Make safe runtime binding
|
||||
const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}`
|
||||
|
||||
// Optionally use a prefix with readable bindings
|
||||
let readableBinding = component._instanceName
|
||||
|
@ -203,7 +235,7 @@ const getContextBindings = (asset, componentId) => {
|
|||
// Field schema and provider are required to construct relationship
|
||||
// datasource options, based on bindable properties
|
||||
fieldSchema,
|
||||
providerId: component._id,
|
||||
providerId,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -225,17 +257,9 @@ const getUserBindings = () => {
|
|||
const safeUser = makePropSafe("user")
|
||||
keys.forEach(key => {
|
||||
const fieldSchema = schema[key]
|
||||
// Replace certain bindings with a new property to help display components
|
||||
let runtimeBoundKey = key
|
||||
if (fieldSchema.type === "link") {
|
||||
runtimeBoundKey = `${key}_text`
|
||||
} else if (fieldSchema.type === "attachment") {
|
||||
runtimeBoundKey = `${key}_first`
|
||||
}
|
||||
|
||||
bindings.push({
|
||||
type: "context",
|
||||
runtimeBinding: `${safeUser}.${makePropSafe(runtimeBoundKey)}`,
|
||||
runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
|
||||
readableBinding: `Current User.${key}`,
|
||||
// Field schema and provider are required to construct relationship
|
||||
// datasource options, based on bindable properties
|
||||
|
@ -309,8 +333,11 @@ const getUrlBindings = asset => {
|
|||
*/
|
||||
export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
||||
let schema, table
|
||||
|
||||
if (datasource) {
|
||||
const { type } = datasource
|
||||
|
||||
// Determine the source table from the datasource type
|
||||
if (type === "provider") {
|
||||
const component = findComponent(asset.props, datasource.providerId)
|
||||
const source = getDatasourceForProvider(asset, component)
|
||||
|
@ -318,11 +345,32 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
|||
} else if (type === "query") {
|
||||
const queries = get(queriesStores).list
|
||||
table = queries.find(query => query._id === datasource._id)
|
||||
} else if (type === "field") {
|
||||
table = { name: datasource.fieldName }
|
||||
const { fieldType } = datasource
|
||||
if (fieldType === "attachment") {
|
||||
schema = {
|
||||
url: {
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
} else if (fieldType === "array") {
|
||||
schema = {
|
||||
value: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const tables = get(tablesStore).list
|
||||
table = tables.find(table => table._id === datasource.tableId)
|
||||
}
|
||||
if (table) {
|
||||
|
||||
// Determine the schema from the table if not already determined
|
||||
if (table && !schema) {
|
||||
if (type === "view") {
|
||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||
} else if (type === "query" && isForm) {
|
||||
|
@ -374,8 +422,8 @@ const buildFormSchema = component => {
|
|||
if (!component) {
|
||||
return schema
|
||||
}
|
||||
const def = store.actions.components.getDefinition(component._component)
|
||||
const fieldSetting = def?.settings?.find(
|
||||
const settings = getComponentSettings(component._component)
|
||||
const fieldSetting = settings.find(
|
||||
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||
)
|
||||
if (fieldSetting && component.field) {
|
||||
|
@ -501,7 +549,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
|
|||
* {{ literal [componentId] }}
|
||||
*/
|
||||
function extractLiteralHandlebarsID(value) {
|
||||
return value?.match(/{{\s*literal[\s[]+([a-fA-F0-9]+)[\s\]]*}}/)?.[1]
|
||||
return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
findClosestMatchingComponent,
|
||||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
getComponentSettings,
|
||||
} from "../storeUtils"
|
||||
import { uuid } from "../uuid"
|
||||
import { removeBindings } from "../dataBinding"
|
||||
|
@ -44,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
state: false,
|
||||
customThemes: false,
|
||||
devicePreview: false,
|
||||
messagePassing: false,
|
||||
},
|
||||
currentFrontEndType: "none",
|
||||
selectedScreenId: "",
|
||||
|
@ -368,14 +370,13 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
|
||||
// Generate default props
|
||||
const settings = getComponentSettings(componentName)
|
||||
let props = { ...presetProps }
|
||||
if (definition.settings) {
|
||||
definition.settings.forEach(setting => {
|
||||
settings.forEach(setting => {
|
||||
if (setting.defaultValue !== undefined) {
|
||||
props[setting.key] = setting.defaultValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Add any extra properties the component needs
|
||||
let extras = {}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { store } from "./index"
|
||||
|
||||
/**
|
||||
* Recursively searches for a specific component ID
|
||||
*/
|
||||
|
@ -123,3 +125,20 @@ const searchComponentTree = (rootComponent, matchComponent) => {
|
|||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a component's definition for a setting matching a certin predicate.
|
||||
*/
|
||||
export const getComponentSettings = componentType => {
|
||||
const def = store.actions.components.getDefinition(componentType)
|
||||
if (!def) {
|
||||
return []
|
||||
}
|
||||
let settings = def.settings?.filter(setting => !setting.section) ?? []
|
||||
def.settings
|
||||
?.filter(setting => setting.section)
|
||||
.forEach(section => {
|
||||
settings = settings.concat(section.settings || [])
|
||||
})
|
||||
return settings
|
||||
}
|
||||
|
|
|
@ -202,7 +202,7 @@
|
|||
display: inline-block;
|
||||
}
|
||||
.block {
|
||||
width: 360px;
|
||||
width: 480px;
|
||||
font-size: 16px;
|
||||
background-color: var(--background);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
|
|
|
@ -8,10 +8,13 @@
|
|||
|
||||
let name
|
||||
let selectedTrigger
|
||||
let nameTouched = false
|
||||
let triggerVal
|
||||
export let webhookModal
|
||||
|
||||
$: instanceId = $database._id
|
||||
$: nameError =
|
||||
nameTouched && !name ? "Please specify a name for the automation." : null
|
||||
|
||||
async function createAutomation() {
|
||||
await automationStore.actions.create({
|
||||
|
@ -51,13 +54,18 @@
|
|||
confirmText="Save"
|
||||
size="M"
|
||||
onConfirm={createAutomation}
|
||||
disabled={!selectedTrigger}
|
||||
disabled={!selectedTrigger || !name}
|
||||
>
|
||||
<Body size="XS"
|
||||
>Please name your automation, then select a trigger. Every automation must
|
||||
start with a trigger.
|
||||
</Body>
|
||||
<Input bind:value={name} label="Name" />
|
||||
<Input
|
||||
bind:value={name}
|
||||
on:change={() => (nameTouched = true)}
|
||||
bind:error={nameError}
|
||||
label="Name"
|
||||
/>
|
||||
|
||||
<Layout noPadding>
|
||||
<Body size="S">Triggers</Body>
|
||||
|
|
|
@ -234,7 +234,8 @@
|
|||
<Editor
|
||||
mode="javascript"
|
||||
on:change={e => {
|
||||
onChange(e, key)
|
||||
// need to pass without the value inside
|
||||
onChange({ detail: e.detail.value }, key)
|
||||
inputData[key] = e.detail.value
|
||||
}}
|
||||
value={inputData[key]}
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||
import ExistingRelationshipButton from "./buttons/ExistingRelationshipButton.svelte"
|
||||
import ExportButton from "./buttons/ExportButton.svelte"
|
||||
import ImportButton from "./buttons/ImportButton.svelte"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
|
@ -98,9 +100,7 @@
|
|||
on:updatecolumns={onUpdateColumns}
|
||||
on:updaterows={onUpdateRows}
|
||||
>
|
||||
{#if isInternal}
|
||||
<CreateColumnButton on:updatecolumns={onUpdateColumns} />
|
||||
{/if}
|
||||
{#if schema && Object.keys(schema).length > 0}
|
||||
{#if !isUsersTable}
|
||||
<CreateRowButton
|
||||
|
@ -116,9 +116,19 @@
|
|||
{#if isUsersTable}
|
||||
<EditRolesButton />
|
||||
{/if}
|
||||
{#if !isInternal}
|
||||
<ExistingRelationshipButton
|
||||
table={$tables.selected}
|
||||
on:updatecolumns={onUpdateColumns}
|
||||
/>
|
||||
{/if}
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<!-- always have the export last -->
|
||||
<ExportButton view={$tables.selected?._id} />
|
||||
<ImportButton
|
||||
tableId={$tables.selected?._id}
|
||||
on:updaterows={onUpdateRows}
|
||||
/>
|
||||
{#key id}
|
||||
<TableFilterButton {schema} on:change={onFilter} />
|
||||
{/key}
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
export let value = defaultValue || (meta.type === "boolean" ? false : "")
|
||||
export let readonly
|
||||
|
||||
$: type = meta.type
|
||||
$: label = capitalise(meta.name)
|
||||
$: type = meta?.type
|
||||
$: label = meta.name ? capitalise(meta.name) : ""
|
||||
</script>
|
||||
|
||||
{#if type === "options"}
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import {
|
||||
TableNames,
|
||||
UNEDITABLE_USER_FIELDS,
|
||||
UNSORTABLE_TYPES,
|
||||
} from "constants"
|
||||
import RoleCell from "./cells/RoleCell.svelte"
|
||||
|
||||
export let schema = {}
|
||||
|
@ -33,6 +37,15 @@
|
|||
$: isUsersTable = tableId === TableNames.USERS
|
||||
$: data && resetSelectedRows()
|
||||
$: editRowComponent = isUsersTable ? CreateEditUser : CreateEditRow
|
||||
$: {
|
||||
UNSORTABLE_TYPES.forEach(type => {
|
||||
Object.values(schema).forEach(col => {
|
||||
if (col.type === type) {
|
||||
col.sortable = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
$: {
|
||||
if (isUsersTable) {
|
||||
customRenderers = [
|
||||
|
@ -129,7 +142,7 @@
|
|||
bind:selectedRows
|
||||
allowSelectRows={allowEditing && !isUsersTable}
|
||||
allowEditRows={allowEditing}
|
||||
allowEditColumns={allowEditing && isInternal}
|
||||
allowEditColumns={allowEditing}
|
||||
showAutoColumns={!hideAutocolumns}
|
||||
on:editcolumn={e => editColumn(e.detail)}
|
||||
on:editrow={e => editRow(e.detail)}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
import { ActionButton, Modal, notifications } from "@budibase/bbui"
|
||||
import CreateEditRelationship from "../../Datasources/CreateEditRelationship.svelte"
|
||||
import { datasources, tables } from "../../../../stores/backend"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let table
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: plusTables = datasource?.plus
|
||||
? Object.values(datasource?.entities || {})
|
||||
: []
|
||||
$: datasource = $datasources.list.find(
|
||||
source => source._id === table?.sourceId
|
||||
)
|
||||
|
||||
let modal
|
||||
|
||||
async function saveRelationship() {
|
||||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
notifications.success(`Relationship information saved.`)
|
||||
const tableList = await tables.fetch()
|
||||
await tables.select(tableList.find(tbl => tbl._id === table._id))
|
||||
dispatch("updatecolumns")
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving relationship info: ${err}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if table.sourceId}
|
||||
<div>
|
||||
<ActionButton
|
||||
icon="DataCorrelated"
|
||||
primary
|
||||
size="S"
|
||||
quiet
|
||||
on:click={modal.show}
|
||||
>
|
||||
Define existing relationship
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditRelationship
|
||||
{datasource}
|
||||
save={saveRelationship}
|
||||
close={modal.hide}
|
||||
{plusTables}
|
||||
selectedFromTable={table}
|
||||
/>
|
||||
</Modal>
|
||||
{/if}
|
|
@ -7,7 +7,7 @@
|
|||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="Download" size="S" quiet on:click={modal.show}>
|
||||
<ActionButton icon="DataDownload" size="S" quiet on:click={modal.show}>
|
||||
Export
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { ActionButton, Modal } from "@budibase/bbui"
|
||||
import ImportModal from "../modals/ImportModal.svelte"
|
||||
|
||||
export let tableId
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
||||
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show}>
|
||||
Import
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ImportModal {tableId} on:updaterows />
|
||||
</Modal>
|
|
@ -18,6 +18,11 @@
|
|||
FIELDS,
|
||||
AUTO_COLUMN_SUB_TYPES,
|
||||
RelationshipTypes,
|
||||
ALLOWABLE_STRING_OPTIONS,
|
||||
ALLOWABLE_NUMBER_OPTIONS,
|
||||
ALLOWABLE_STRING_TYPES,
|
||||
ALLOWABLE_NUMBER_TYPES,
|
||||
SWITCHABLE_TYPES,
|
||||
} from "constants/backend"
|
||||
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
@ -31,6 +36,9 @@
|
|||
const AUTO_TYPE = "auto"
|
||||
const FORMULA_TYPE = FIELDS.FORMULA.type
|
||||
const LINK_TYPE = FIELDS.LINK.type
|
||||
const STRING_TYPE = FIELDS.STRING.type
|
||||
const NUMBER_TYPE = FIELDS.NUMBER.type
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"]
|
||||
const { hide } = getContext(Context.Modal)
|
||||
|
@ -55,9 +63,7 @@
|
|||
let confirmDeleteDialog
|
||||
let deletion
|
||||
|
||||
$: tableOptions = $tables.list.filter(
|
||||
table => table._id !== $tables.draft._id && table.type !== "external"
|
||||
)
|
||||
$: checkConstraints(field)
|
||||
$: required = !!field?.constraints?.presence || primaryDisplay
|
||||
$: uneditable =
|
||||
$tables.selected?._id === TableNames.USERS &&
|
||||
|
@ -83,6 +89,17 @@
|
|||
$: canBeRequired =
|
||||
field.type !== LINK_TYPE && !uneditable && field.type !== AUTO_TYPE
|
||||
$: relationshipOptions = getRelationshipOptions(field)
|
||||
$: external = table.type === "external"
|
||||
// in the case of internal tables the sourceId will just be undefined
|
||||
$: tableOptions = $tables.list.filter(
|
||||
opt =>
|
||||
opt._id !== $tables.draft._id &&
|
||||
opt.type === table.type &&
|
||||
table.sourceId === opt.sourceId
|
||||
)
|
||||
$: typeEnabled =
|
||||
!originalName ||
|
||||
(originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1)
|
||||
|
||||
async function saveColumn() {
|
||||
if (field.type === AUTO_TYPE) {
|
||||
|
@ -169,7 +186,7 @@
|
|||
if (!field || !field.tableId) {
|
||||
return null
|
||||
}
|
||||
const linkTable = tableOptions.find(table => table._id === field.tableId)
|
||||
const linkTable = tableOptions?.find(table => table._id === field.tableId)
|
||||
if (!linkTable) {
|
||||
return null
|
||||
}
|
||||
|
@ -193,6 +210,52 @@
|
|||
},
|
||||
]
|
||||
}
|
||||
|
||||
function getAllowedTypes() {
|
||||
if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) {
|
||||
return ALLOWABLE_STRING_OPTIONS
|
||||
} else if (
|
||||
originalName &&
|
||||
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
|
||||
) {
|
||||
return ALLOWABLE_NUMBER_OPTIONS
|
||||
} else if (!external) {
|
||||
return [
|
||||
...Object.values(fieldDefinitions),
|
||||
{ name: "Auto Column", type: AUTO_TYPE },
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
FIELDS.STRING,
|
||||
FIELDS.LONGFORM,
|
||||
FIELDS.OPTIONS,
|
||||
FIELDS.DATETIME,
|
||||
FIELDS.NUMBER,
|
||||
FIELDS.BOOLEAN,
|
||||
FIELDS.ARRAY,
|
||||
FIELDS.FORMULA,
|
||||
FIELDS.LINK,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function checkConstraints(fieldToCheck) {
|
||||
// most types need this, just make sure its always present
|
||||
if (fieldToCheck && !fieldToCheck.constraints) {
|
||||
fieldToCheck.constraints = {}
|
||||
}
|
||||
// some string types may have been built by server, may not always have constraints
|
||||
if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) {
|
||||
fieldToCheck.constraints.length = {}
|
||||
}
|
||||
// some number types made server-side will be missing constraints
|
||||
if (
|
||||
fieldToCheck.type === NUMBER_TYPE &&
|
||||
!fieldToCheck.constraints.numericality
|
||||
) {
|
||||
fieldToCheck.constraints.numericality = {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
@ -211,14 +274,11 @@
|
|||
/>
|
||||
|
||||
<Select
|
||||
disabled={originalName}
|
||||
disabled={!typeEnabled}
|
||||
label="Type"
|
||||
bind:value={field.type}
|
||||
on:change={handleTypeChange}
|
||||
options={[
|
||||
...Object.values(fieldDefinitions),
|
||||
{ name: "Auto Column", type: AUTO_TYPE },
|
||||
]}
|
||||
options={getAllowedTypes()}
|
||||
getOptionLabel={field => field.name}
|
||||
getOptionValue={field => field.type}
|
||||
/>
|
||||
|
@ -245,7 +305,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canBeSearched}
|
||||
{#if canBeSearched && !external}
|
||||
<div>
|
||||
<Label grey small>Search Indexes</Label>
|
||||
<Toggle
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import { ModalContent, Label, notifications, Body } from "@budibase/bbui"
|
||||
import TableDataImport from "../../TableNavigator/TableDataImport.svelte"
|
||||
import api from "builderStore/api"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let tableId
|
||||
let dataImport
|
||||
|
||||
$: valid = dataImport?.csvString != null && dataImport?.valid
|
||||
|
||||
async function importData() {
|
||||
const response = await api.post(`/api/tables/${tableId}/import`, {
|
||||
dataImport,
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
const error = await response.text()
|
||||
notifications.error(`Unable to import data - ${error}`)
|
||||
} else {
|
||||
notifications.success("Rows successfully imported.")
|
||||
}
|
||||
dispatch("updaterows")
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Import Data"
|
||||
confirmText="Import"
|
||||
onConfirm={importData}
|
||||
disabled={!valid}
|
||||
>
|
||||
<Body
|
||||
>Import rows to an existing table from a CSV. Only columns from the CSV
|
||||
which exist in the table will be imported.</Body
|
||||
>
|
||||
<Label grey extraSmall>CSV to import</Label>
|
||||
<TableDataImport bind:dataImport bind:existingTableId={tableId} />
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -18,10 +18,19 @@
|
|||
export let fromRelationship = {}
|
||||
export let toRelationship = {}
|
||||
export let close
|
||||
export let selectedFromTable
|
||||
|
||||
let originalFromName = fromRelationship.name,
|
||||
originalToName = toRelationship.name
|
||||
|
||||
if (fromRelationship && !fromRelationship.relationshipType) {
|
||||
fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
|
||||
}
|
||||
|
||||
if (toRelationship && selectedFromTable) {
|
||||
toRelationship.tableId = selectedFromTable._id
|
||||
}
|
||||
|
||||
function inSchema(table, prop, ogName) {
|
||||
if (!table || !prop || prop === ogName) {
|
||||
return false
|
||||
|
@ -114,6 +123,7 @@
|
|||
},
|
||||
]
|
||||
$: updateRelationshipType(fromRelationship?.relationshipType)
|
||||
$: tableChanged(fromTable, toTable)
|
||||
|
||||
function updateRelationshipType(fromType) {
|
||||
if (fromType === RelationshipTypes.MANY_TO_MANY) {
|
||||
|
@ -205,7 +215,6 @@
|
|||
originalToName = toRelationship.name
|
||||
originalFromName = fromRelationship.name
|
||||
await save()
|
||||
await tables.fetch()
|
||||
}
|
||||
|
||||
async function deleteRelationship() {
|
||||
|
@ -215,10 +224,26 @@
|
|||
await tables.fetch()
|
||||
close()
|
||||
}
|
||||
|
||||
function tableChanged(fromTbl, toTbl) {
|
||||
fromRelationship.name = toTbl?.name || ""
|
||||
errors.fromCol = ""
|
||||
toRelationship.name = fromTbl?.name || ""
|
||||
errors.toCol = ""
|
||||
if (toTbl || fromTbl) {
|
||||
checkForErrors(
|
||||
fromTable,
|
||||
toTable,
|
||||
through,
|
||||
fromRelationship,
|
||||
toRelationship
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create Relationship"
|
||||
title="Define Relationship"
|
||||
confirmText="Save"
|
||||
onConfirm={saveRelationship}
|
||||
disabled={!valid}
|
||||
|
@ -234,6 +259,7 @@
|
|||
<Select
|
||||
label="Select from table"
|
||||
options={tableOptions}
|
||||
disabled={!!selectedFromTable}
|
||||
on:change={() => ($touched.from = true)}
|
||||
bind:error={errors.from}
|
||||
bind:value={toRelationship.tableId}
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { Select, InlineAlert, notifications } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import api from "builderStore/api"
|
||||
|
||||
|
@ -12,11 +11,13 @@
|
|||
valid: true,
|
||||
schema: {},
|
||||
}
|
||||
export let existingTableId
|
||||
|
||||
let csvString
|
||||
let primaryDisplay
|
||||
let csvString = undefined
|
||||
let primaryDisplay = undefined
|
||||
let schema = {}
|
||||
let fields = []
|
||||
let hasValidated = false
|
||||
|
||||
$: valid = !schema || fields.every(column => schema[column].success)
|
||||
$: dataImport = {
|
||||
|
@ -25,6 +26,9 @@
|
|||
csvString,
|
||||
primaryDisplay,
|
||||
}
|
||||
$: noFieldsError = existingTableId
|
||||
? "No columns in CSV match existing table schema"
|
||||
: "Could not find any columns to import"
|
||||
|
||||
function buildTableSchema(schema) {
|
||||
const tableSchema = {}
|
||||
|
@ -46,6 +50,7 @@
|
|||
const response = await api.post("/api/tables/csv/validate", {
|
||||
csvString,
|
||||
schema: schema || {},
|
||||
tableId: existingTableId,
|
||||
})
|
||||
|
||||
const parseResult = await response.json()
|
||||
|
@ -63,6 +68,7 @@
|
|||
notifications.error("CSV Invalid, please try another CSV file")
|
||||
return []
|
||||
}
|
||||
hasValidated = true
|
||||
}
|
||||
|
||||
async function handleFile(evt) {
|
||||
|
@ -138,6 +144,7 @@
|
|||
placeholder={null}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.value}
|
||||
disabled={!!existingTableId}
|
||||
/>
|
||||
<span class="field-status" class:error={!schema[columnName].success}>
|
||||
{schema[columnName].success ? "Success" : "Failure"}
|
||||
|
@ -149,9 +156,7 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if fields.length}
|
||||
{#if !existingTableId}
|
||||
<div class="display-column">
|
||||
<Select
|
||||
label="Display Column"
|
||||
|
@ -160,6 +165,15 @@
|
|||
sort
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if hasValidated}
|
||||
<div>
|
||||
<InlineAlert
|
||||
header="Invalid CSV"
|
||||
bind:message={noFieldsError}
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { allScreens, store } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import {
|
||||
ActionMenu,
|
||||
Icon,
|
||||
|
@ -40,7 +40,10 @@
|
|||
store.actions.screens.delete(templateScreens)
|
||||
await tables.fetch()
|
||||
notifications.success("Table deleted")
|
||||
if (wasSelectedTable._id === table._id) {
|
||||
if (table.type === "external") {
|
||||
await datasources.fetch()
|
||||
}
|
||||
if (wasSelectedTable && wasSelectedTable._id === table._id) {
|
||||
$goto("./table")
|
||||
}
|
||||
}
|
||||
|
@ -64,9 +67,7 @@
|
|||
<Icon s hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Edit" on:click={editorModal.show}>Edit</MenuItem>
|
||||
{#if !external}
|
||||
<MenuItem icon="Delete" on:click={showDeleteModal}>Delete</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
|
||||
<Modal bind:this={editorModal}>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let onOk = undefined
|
||||
export let onCancel = undefined
|
||||
export let warning = true
|
||||
export let disabled
|
||||
|
||||
let modal
|
||||
|
||||
|
@ -26,6 +27,7 @@
|
|||
confirmText={okText}
|
||||
{cancelText}
|
||||
{warning}
|
||||
{disabled}
|
||||
>
|
||||
<Body size="S">
|
||||
{body}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
export let disabled = false
|
||||
export let options
|
||||
export let allowJS = true
|
||||
export let appendBindingsAsOptions = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
@ -24,15 +25,30 @@
|
|||
$: readableValue = runtimeToReadableBinding(bindings, value)
|
||||
$: tempValue = readableValue
|
||||
$: isJS = isJSBinding(value)
|
||||
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
|
||||
|
||||
const handleClose = () => {
|
||||
onChange(tempValue)
|
||||
bindingDrawer.hide()
|
||||
}
|
||||
|
||||
const onChange = value => {
|
||||
const onChange = (value, optionPicked) => {
|
||||
// Add HBS braces if picking binding
|
||||
if (optionPicked && !options?.includes(value)) {
|
||||
value = `{{ ${value} }}`
|
||||
}
|
||||
|
||||
dispatch("change", readableToRuntimeBinding(bindings, value))
|
||||
}
|
||||
|
||||
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
|
||||
if (!appendBindingsAsOptions) {
|
||||
return options
|
||||
}
|
||||
return []
|
||||
.concat(options || [])
|
||||
.concat(bindings?.map(binding => binding.readableBinding) || [])
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="control">
|
||||
|
@ -40,12 +56,17 @@
|
|||
{label}
|
||||
{disabled}
|
||||
value={isJS ? "(JavaScript function)" : readableValue}
|
||||
on:change={event => onChange(event.detail)}
|
||||
on:type={e => onChange(e.detail, false)}
|
||||
on:pick={e => onChange(e.detail, true)}
|
||||
{placeholder}
|
||||
{options}
|
||||
options={allOptions}
|
||||
/>
|
||||
{#if !disabled}
|
||||
<div class="icon" on:click={bindingDrawer.show}>
|
||||
<div
|
||||
class="icon"
|
||||
on:click={bindingDrawer.show}
|
||||
data-cy="text-binding-button"
|
||||
>
|
||||
<Icon size="S" name="FlashOn" />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
<script>
|
||||
import { Icon, Modal, notifications, ModalContent } from "@budibase/bbui"
|
||||
import {
|
||||
Icon,
|
||||
Input,
|
||||
Modal,
|
||||
notifications,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
|
||||
let revertModal
|
||||
let appName
|
||||
|
||||
$: appId = $store.appId
|
||||
|
||||
|
@ -33,10 +40,17 @@
|
|||
|
||||
<Icon name="Revert" hoverable on:click={revertModal.show} />
|
||||
<Modal bind:this={revertModal}>
|
||||
<ModalContent title="Revert Changes" confirmText="Revert" onConfirm={revert}>
|
||||
<ModalContent
|
||||
title="Revert Changes"
|
||||
confirmText="Revert"
|
||||
onConfirm={revert}
|
||||
disabled={appName !== $store.name}
|
||||
>
|
||||
<span
|
||||
>The changes you have made will be deleted and the application reverted
|
||||
back to its production state.</span
|
||||
>
|
||||
<span>Please enter your app name to continue.</span>
|
||||
<Input bind:value={appName} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
|
|
@ -33,6 +33,15 @@
|
|||
.instanceName("Content Placeholder")
|
||||
.json()
|
||||
|
||||
// Messages that can be sent from the iframe preview to the builder
|
||||
// Budibase events are and initalisation events
|
||||
const MessageTypes = {
|
||||
IFRAME_LOADED: "iframe-loaded",
|
||||
READY: "ready",
|
||||
ERROR: "error",
|
||||
BUDIBASE: "type",
|
||||
}
|
||||
|
||||
// Construct iframe template
|
||||
$: template = iframeTemplate.replace(
|
||||
/\{\{ CLIENT_LIB_PATH }}/,
|
||||
|
@ -59,6 +68,7 @@
|
|||
theme: $store.theme,
|
||||
customTheme: $store.customTheme,
|
||||
previewDevice: $store.previewDevice,
|
||||
messagePassing: $store.clientFeatures.messagePassing,
|
||||
}
|
||||
|
||||
// Saving pages and screens to the DB causes them to have _revs.
|
||||
|
@ -80,11 +90,14 @@
|
|||
// Refresh the preview when required
|
||||
$: refreshContent(strippedJson)
|
||||
|
||||
onMount(() => {
|
||||
function receiveMessage(message) {
|
||||
const handlers = {
|
||||
[MessageTypes.READY]: () => {
|
||||
// Initialise the app when mounted
|
||||
iframe.contentWindow.addEventListener(
|
||||
"ready",
|
||||
() => {
|
||||
if ($store.clientFeatures.messagePassing) {
|
||||
if (!loading) return
|
||||
}
|
||||
|
||||
// Display preview immediately if the intelligent loading feature
|
||||
// is not supported
|
||||
if (!$store.clientFeatures.intelligentLoading) {
|
||||
|
@ -92,32 +105,58 @@
|
|||
}
|
||||
refreshContent(strippedJson)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
[MessageTypes.ERROR]: event => {
|
||||
// Catch any app errors
|
||||
iframe.contentWindow.addEventListener(
|
||||
"error",
|
||||
event => {
|
||||
loading = false
|
||||
error = event.detail || "An unknown error occurred"
|
||||
error = event.error || "An unknown error occurred"
|
||||
},
|
||||
}
|
||||
|
||||
const messageHandler = handlers[message.data.type] || handleBudibaseEvent
|
||||
messageHandler(message)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener("message", receiveMessage)
|
||||
if (!$store.clientFeatures.messagePassing) {
|
||||
// Legacy - remove in later versions of BB
|
||||
iframe.contentWindow.addEventListener(
|
||||
"ready",
|
||||
() => {
|
||||
receiveMessage({ data: { type: MessageTypes.READY } })
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
iframe.contentWindow.addEventListener(
|
||||
"error",
|
||||
event => {
|
||||
receiveMessage({
|
||||
data: { type: MessageTypes.ERROR, error: event.detail },
|
||||
})
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
// Add listener for events sent by client library in preview
|
||||
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove all iframe event listeners on component destroy
|
||||
onDestroy(() => {
|
||||
if (iframe.contentWindow) {
|
||||
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
||||
window.removeEventListener("message", receiveMessage)
|
||||
if (!$store.clientFeatures.messagePassing) {
|
||||
// Legacy - remove in later versions of BB
|
||||
iframe.contentWindow.removeEventListener(
|
||||
"bb-event",
|
||||
handleBudibaseEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleBudibaseEvent = event => {
|
||||
const { type, data } = event.detail
|
||||
const { type, data } = event.data || event.detail
|
||||
if (type === "select-component" && data.id) {
|
||||
store.actions.components.select({ _id: data.id })
|
||||
} else if (type === "update-prop") {
|
||||
|
@ -149,7 +188,7 @@
|
|||
store.actions.components.paste(destination, data.mode)
|
||||
}
|
||||
} else {
|
||||
console.warning(`Client sent unknown event type: ${type}`)
|
||||
console.warn(`Client sent unknown event type: ${type}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
[
|
||||
{
|
||||
"name": "Blocks",
|
||||
"icon": "Article",
|
||||
"children": [
|
||||
"tableblock",
|
||||
"cardsblock",
|
||||
"repeaterblock"
|
||||
]
|
||||
},
|
||||
"section",
|
||||
"container",
|
||||
"dataprovider",
|
||||
|
|
|
@ -84,17 +84,21 @@ export default `
|
|||
if (window.loadBudibase) {
|
||||
window.loadBudibase()
|
||||
document.documentElement.classList.add("loaded")
|
||||
window.dispatchEvent(new Event("iframe-loaded"))
|
||||
window.parent.postMessage({ type: "iframe-loaded" })
|
||||
} else {
|
||||
throw "The client library couldn't be loaded"
|
||||
}
|
||||
} catch (error) {
|
||||
window.dispatchEvent(new CustomEvent("error", { detail: error }))
|
||||
window.parent.postMessage({ type: "error", error })
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", receiveMessage)
|
||||
window.dispatchEvent(new Event("ready"))
|
||||
window.addEventListener("keydown", evt => {
|
||||
window.parent.postMessage({ type: "keydown", key: event.key })
|
||||
})
|
||||
|
||||
window.parent.postMessage({ type: "ready" })
|
||||
</script>
|
||||
</head>
|
||||
<body/>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let componentInstance
|
||||
export let assetInstance
|
||||
export let bindings
|
||||
export let componentBindings
|
||||
|
||||
const layoutDefinition = []
|
||||
const screenDefinition = [
|
||||
|
@ -21,10 +22,24 @@
|
|||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||
]
|
||||
|
||||
$: settings = componentDefinition?.settings ?? []
|
||||
$: sections = getSections(componentDefinition)
|
||||
$: isLayout = assetInstance && assetInstance.favicon
|
||||
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
|
||||
|
||||
const getSections = definition => {
|
||||
const settings = definition?.settings ?? []
|
||||
const generalSettings = settings.filter(setting => !setting.section)
|
||||
const customSections = settings.filter(setting => setting.section)
|
||||
return [
|
||||
{
|
||||
name: "General",
|
||||
info: componentDefinition?.info,
|
||||
settings: generalSettings,
|
||||
},
|
||||
...(customSections || []),
|
||||
]
|
||||
}
|
||||
|
||||
const updateProp = store.actions.components.updateProp
|
||||
|
||||
const canRenderControl = setting => {
|
||||
|
@ -59,10 +74,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<DetailSummary name="General" collapsible={false}>
|
||||
{#if !componentInstance._component.endsWith("/layout")}
|
||||
{#each sections as section, idx (section.name)}
|
||||
<DetailSummary name={section.name} collapsible={false}>
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout")}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
|
@ -70,8 +85,7 @@
|
|||
onChange={val => updateProp("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{#if settings && settings.length > 0}
|
||||
{#each settings as setting (setting.key)}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if canRenderControl(setting)}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
|
@ -80,7 +94,7 @@
|
|||
key={setting.key}
|
||||
value={componentInstance[setting.key] ??
|
||||
componentInstance[setting.key]?.defaultValue}
|
||||
{componentInstance}
|
||||
nested={setting.nested}
|
||||
onChange={val => updateProp(setting.key, val)}
|
||||
props={{
|
||||
options: setting.options || [],
|
||||
|
@ -89,20 +103,22 @@
|
|||
max: setting.max || null,
|
||||
}}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if componentDefinition?.info}
|
||||
{#if section?.info}
|
||||
<div class="text">
|
||||
{@html componentDefinition?.info}
|
||||
{@html section.info}
|
||||
</div>
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
</DetailSummary>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.text {
|
||||
|
|
|
@ -6,13 +6,20 @@
|
|||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import {
|
||||
getBindableProperties,
|
||||
getComponentBindableProperties,
|
||||
} from "builderStore/dataBinding"
|
||||
|
||||
$: componentInstance = $selectedComponent
|
||||
$: componentDefinition = store.actions.components.getDefinition(
|
||||
$selectedComponent?._component
|
||||
)
|
||||
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
|
||||
$: componentBindings = getComponentBindableProperties(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
</script>
|
||||
|
||||
<Tabs selected="Settings" noPadding>
|
||||
|
@ -28,6 +35,7 @@
|
|||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
/>
|
||||
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
||||
<CustomStylesSection
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
import { generate } from "shortid"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
|
||||
import { selectedComponent, store } from "builderStore"
|
||||
import { selectedComponent } from "builderStore"
|
||||
import { getComponentForSettingType } from "./componentSettings"
|
||||
import PropertyControl from "./PropertyControl.svelte"
|
||||
import { getComponentSettings } from "builderStore/storeUtils"
|
||||
|
||||
export let conditions = []
|
||||
export let bindings = []
|
||||
|
@ -55,15 +56,11 @@
|
|||
]
|
||||
|
||||
let dragDisabled = true
|
||||
$: definition = store.actions.components.getDefinition(
|
||||
$selectedComponent?._component
|
||||
)
|
||||
$: settings = (definition?.settings ?? []).map(setting => {
|
||||
return {
|
||||
$: settings = getComponentSettings($selectedComponent?._component)
|
||||
$: settingOptions = settings.map(setting => ({
|
||||
label: setting.label,
|
||||
value: setting.key,
|
||||
}
|
||||
})
|
||||
}))
|
||||
$: conditions.forEach(link => {
|
||||
if (!link.id) {
|
||||
link.id = generate()
|
||||
|
@ -71,9 +68,7 @@
|
|||
})
|
||||
|
||||
const getSettingDefinition = key => {
|
||||
return definition?.settings?.find(setting => {
|
||||
return setting.key === key
|
||||
})
|
||||
return settings.find(setting => setting.key === key)
|
||||
}
|
||||
|
||||
const getComponentForSetting = key => {
|
||||
|
@ -175,7 +170,10 @@
|
|||
bind:value={condition.action}
|
||||
/>
|
||||
{#if condition.action === "update"}
|
||||
<Select options={settings} bind:value={condition.setting} />
|
||||
<Select
|
||||
options={settingOptions}
|
||||
bind:value={condition.setting}
|
||||
/>
|
||||
<div>TO</div>
|
||||
{#if getSettingDefinition(condition.setting)}
|
||||
<PropertyControl
|
||||
|
|
|
@ -11,10 +11,7 @@
|
|||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||
|
||||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
||||
$: providers = path.filter(
|
||||
component =>
|
||||
component._component === "@budibase/standard-components/dataprovider"
|
||||
)
|
||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||
|
||||
// Set initial value to closest data provider
|
||||
onMount(() => {
|
||||
|
|
|
@ -20,16 +20,18 @@
|
|||
import { notifications } from "@budibase/bbui"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let anchorRight, dropdownRight
|
||||
let drawer
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
||||
export let value = {}
|
||||
export let otherSources
|
||||
export let showAllQueries
|
||||
export let bindings = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const arrayTypes = ["attachment", "array"]
|
||||
let anchorRight, dropdownRight
|
||||
let drawer
|
||||
|
||||
$: text = value?.label ?? "Choose an option"
|
||||
$: tables = $tablesStore.list.map(m => ({
|
||||
label: m.name,
|
||||
|
@ -54,8 +56,6 @@
|
|||
name: query.name,
|
||||
tableId: query._id,
|
||||
...query,
|
||||
schema: query.schema,
|
||||
parameters: query.parameters,
|
||||
type: "query",
|
||||
}))
|
||||
$: dataProviders = getDataProviderComponents(
|
||||
|
@ -65,29 +65,40 @@
|
|||
label: provider._instanceName,
|
||||
name: provider._instanceName,
|
||||
providerId: provider._id,
|
||||
value: `{{ literal [${provider._id}] }}`,
|
||||
value: `{{ literal ${safe(provider._id)} }}`,
|
||||
type: "provider",
|
||||
schema: provider.schema,
|
||||
}))
|
||||
$: queryBindableProperties = bindings.map(property => ({
|
||||
...property,
|
||||
category: property.type === "instance" ? "Component" : "Table",
|
||||
label: property.readableBinding,
|
||||
path: property.readableBinding,
|
||||
}))
|
||||
$: links = bindings
|
||||
.filter(x => x.fieldSchema?.type === "link")
|
||||
.map(property => {
|
||||
.map(binding => {
|
||||
const { providerId, readableBinding, fieldSchema } = binding || {}
|
||||
const { name, tableId } = fieldSchema || {}
|
||||
const safeProviderId = safe(providerId)
|
||||
return {
|
||||
providerId: property.providerId,
|
||||
label: property.readableBinding,
|
||||
fieldName: property.fieldSchema.name,
|
||||
tableId: property.fieldSchema.tableId,
|
||||
providerId,
|
||||
label: readableBinding,
|
||||
fieldName: name,
|
||||
tableId,
|
||||
type: "link",
|
||||
// These properties will be enriched by the client library and provide
|
||||
// details of the parent row of the relationship field, from context
|
||||
rowId: `{{ ${property.providerId}._id }}`,
|
||||
rowTableId: `{{ ${property.providerId}.tableId }}`,
|
||||
rowId: `{{ ${safeProviderId}.${safe("_id")} }}`,
|
||||
rowTableId: `{{ ${safeProviderId}.${safe("tableId")} }}`,
|
||||
}
|
||||
})
|
||||
$: fields = bindings
|
||||
.filter(x => arrayTypes.includes(x.fieldSchema?.type))
|
||||
.map(binding => {
|
||||
const { providerId, readableBinding, runtimeBinding } = binding
|
||||
const { name, type, tableId } = binding.fieldSchema
|
||||
return {
|
||||
providerId,
|
||||
label: readableBinding,
|
||||
fieldName: name,
|
||||
fieldType: type,
|
||||
tableId,
|
||||
type: "field",
|
||||
value: `{{ literal ${runtimeBinding} }}`,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -102,6 +113,14 @@
|
|||
).source
|
||||
return $integrations[source].query[query.queryVerb]
|
||||
}
|
||||
|
||||
const getQueryParams = query => {
|
||||
return $queriesStore.list.find(q => q._id === query?._id)?.parameters || []
|
||||
}
|
||||
|
||||
const getQueryDatasource = query => {
|
||||
return $datasources.list.find(ds => ds._id === query?.datasourceId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" bind:this={anchorRight}>
|
||||
|
@ -127,11 +146,10 @@
|
|||
</Button>
|
||||
<DrawerContent slot="body">
|
||||
<Layout noPadding>
|
||||
{#if value.parameters.length > 0}
|
||||
{#if getQueryParams(value._id).length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={value.queryParams}
|
||||
parameters={queries.find(query => query._id === value._id)
|
||||
.parameters}
|
||||
parameters={getQueryParams(value)}
|
||||
{bindings}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -139,9 +157,7 @@
|
|||
height={200}
|
||||
query={value}
|
||||
schema={fetchQueryDefinition(value)}
|
||||
datasource={$datasources.list.find(
|
||||
ds => ds._id === value.datasourceId
|
||||
)}
|
||||
datasource={getQueryDatasource(value)}
|
||||
editable={false}
|
||||
/>
|
||||
</Layout>
|
||||
|
@ -159,6 +175,7 @@
|
|||
<li on:click={() => handleSelected(table)}>{table.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{#if views?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Views</Heading>
|
||||
|
@ -168,15 +185,8 @@
|
|||
<li on:click={() => handleSelected(view)}>{view.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Relationships</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
{#each links as link}
|
||||
<li on:click={() => handleSelected(link)}>{link.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if queries?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Queries</Heading>
|
||||
|
@ -191,6 +201,30 @@
|
|||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if links?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Relationships</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
{#each links as link}
|
||||
<li on:click={() => handleSelected(link)}>{link.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if fields?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Fields</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
{#each fields as field}
|
||||
<li on:click={() => handleSelected(field)}>{field.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if dataProviders?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
<Heading size="XS">Data Providers</Heading>
|
||||
|
@ -205,6 +239,7 @@
|
|||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if otherSources?.length}
|
||||
<Divider size="S" />
|
||||
<div class="title">
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { Input } from "@budibase/bbui"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let value
|
||||
|
||||
$: isJS = isJSBinding(value)
|
||||
</script>
|
||||
|
||||
<Input
|
||||
{...$$props}
|
||||
value={isJS ? "(JavaScript function)" : value}
|
||||
readonly={isJS}
|
||||
on:change
|
||||
/>
|
|
@ -1,14 +1,11 @@
|
|||
<script>
|
||||
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
|
||||
import { Label } from "@budibase/bbui"
|
||||
import {
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let label = ""
|
||||
export let bindable = true
|
||||
export let componentInstance = {}
|
||||
export let control = null
|
||||
export let key = ""
|
||||
|
@ -17,18 +14,19 @@
|
|||
export let props = {}
|
||||
export let onChange = () => {}
|
||||
export let bindings = []
|
||||
export let componentBindings = []
|
||||
export let nested = false
|
||||
|
||||
let bindingDrawer
|
||||
let anchor
|
||||
let valid
|
||||
|
||||
$: safeValue = getSafeValue(value, props.defaultValue, bindings)
|
||||
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||
$: safeValue = getSafeValue(value, props.defaultValue, allBindings)
|
||||
$: tempValue = safeValue
|
||||
$: replaceBindings = val => readableToRuntimeBinding(bindings, val)
|
||||
$: replaceBindings = val => readableToRuntimeBinding(allBindings, val)
|
||||
|
||||
const handleClose = () => {
|
||||
handleChange(tempValue)
|
||||
bindingDrawer.hide()
|
||||
const getAllBindings = (bindings, componentBindings, nested) => {
|
||||
if (!nested) {
|
||||
return bindings
|
||||
}
|
||||
return [...(componentBindings || []), ...(bindings || [])]
|
||||
}
|
||||
|
||||
// Handle a value change of any type
|
||||
|
@ -64,7 +62,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
|
||||
<div class="property-control" data-cy={`setting-${key}`}>
|
||||
{#if type !== "boolean" && label}
|
||||
<div class="label">
|
||||
<Label>{label}</Label>
|
||||
|
@ -78,37 +76,12 @@
|
|||
updateOnChange={false}
|
||||
on:change={handleChange}
|
||||
onChange={handleChange}
|
||||
{bindings}
|
||||
bindings={allBindings}
|
||||
name={key}
|
||||
text={label}
|
||||
{type}
|
||||
{...props}
|
||||
/>
|
||||
{#if bindable && !key.startsWith("_") && type === "text"}
|
||||
<div
|
||||
class="icon"
|
||||
data-cy={`${key}-binding-button`}
|
||||
on:click={bindingDrawer.show}
|
||||
>
|
||||
<Icon size="S" name="FlashOn" />
|
||||
</div>
|
||||
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
|
||||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
|
||||
Save
|
||||
</Button>
|
||||
<BindingPanel
|
||||
slot="body"
|
||||
bind:valid
|
||||
value={safeValue}
|
||||
on:change={e => (tempValue = e.detail)}
|
||||
bindableProperties={bindings}
|
||||
allowJS
|
||||
/>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -120,41 +93,10 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.label {
|
||||
text-transform: capitalize;
|
||||
padding-bottom: var(--spectrum-global-dimension-size-65);
|
||||
}
|
||||
|
||||
.control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon {
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
bottom: 1px;
|
||||
position: absolute;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
border-left: 1px solid var(--spectrum-alias-border-color);
|
||||
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
|
||||
width: 31px;
|
||||
color: var(--spectrum-alias-text-color);
|
||||
background-color: var(--spectrum-global-color-gray-75);
|
||||
transition: background-color
|
||||
var(--spectrum-global-animation-duration-100, 130ms),
|
||||
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
|
||||
border-color var(--spectrum-global-animation-duration-100, 130ms);
|
||||
}
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
color: var(--spectrum-alias-text-color-hover);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
border-color: var(--spectrum-alias-border-color-hover);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,4 +10,10 @@
|
|||
.filter(x => x != null)
|
||||
</script>
|
||||
|
||||
<DrawerBindableCombobox {value} {bindings} on:change options={urlOptions} />
|
||||
<DrawerBindableCombobox
|
||||
{value}
|
||||
{bindings}
|
||||
on:change
|
||||
options={urlOptions}
|
||||
appendBindingsAsOptions={false}
|
||||
/>
|
||||
|
|
|
@ -15,10 +15,10 @@ import URLSelect from "./URLSelect.svelte"
|
|||
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
|
||||
import FormFieldSelect from "./FormFieldSelect.svelte"
|
||||
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
|
||||
import Input from "./Input.svelte"
|
||||
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
|
||||
|
||||
const componentMap = {
|
||||
text: Input,
|
||||
text: DrawerBindableCombobox,
|
||||
select: Select,
|
||||
dataSource: DataSourceSelect,
|
||||
dataProvider: DataProviderSelect,
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
<DetailSummary name="Screen" collapsible={false}>
|
||||
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
control={def.control}
|
||||
label={def.label}
|
||||
key={def.key}
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
|
||||
<div style="grid-column: {prop.column || 'auto'}">
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
|
||||
control={prop.control}
|
||||
key={prop.key}
|
||||
|
|
|
@ -9,7 +9,6 @@ export const margin = {
|
|||
label: "Top",
|
||||
key: "margin-top",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -30,7 +29,6 @@ export const margin = {
|
|||
label: "Right",
|
||||
key: "margin-right",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -51,7 +49,6 @@ export const margin = {
|
|||
label: "Bottom",
|
||||
key: "margin-bottom",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -72,7 +69,6 @@ export const margin = {
|
|||
label: "Left",
|
||||
key: "margin-left",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -100,7 +96,6 @@ export const padding = {
|
|||
label: "Top",
|
||||
key: "padding-top",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -121,7 +116,6 @@ export const padding = {
|
|||
label: "Right",
|
||||
key: "padding-right",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -142,7 +136,6 @@ export const padding = {
|
|||
label: "Bottom",
|
||||
key: "padding-bottom",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
@ -163,7 +156,6 @@ export const padding = {
|
|||
label: "Left",
|
||||
key: "padding-left",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "4px", value: "4px" },
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
Checkbox,
|
||||
} from "@budibase/bbui"
|
||||
import { store, automationStore, hostingStore } from "builderStore"
|
||||
import { admin } from "stores/portal"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { string, mixed, object } from "yup"
|
||||
import api, { get, post } from "builderStore/api"
|
||||
import analytics, { Events } from "analytics"
|
||||
|
@ -139,6 +139,7 @@
|
|||
}
|
||||
const userResp = await api.post(`/api/users/metadata/self`, user)
|
||||
await userResp.json()
|
||||
await auth.setInitInfo({})
|
||||
$goto(`/builder/app/${appJson.instance._id}`)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -146,6 +147,21 @@
|
|||
submitting = false
|
||||
}
|
||||
}
|
||||
|
||||
function getModalTitle() {
|
||||
let title = "Create App"
|
||||
if (template.fromFile) {
|
||||
title = "Import App"
|
||||
} else if (template.key) {
|
||||
title = "Create app from template"
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
async function onCancel() {
|
||||
template = null
|
||||
await auth.setInitInfo({})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showTemplateSelection}
|
||||
|
@ -172,10 +188,10 @@
|
|||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent
|
||||
title={template?.fromFile ? "Import app" : "Create app"}
|
||||
title={getModalTitle()}
|
||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||
onConfirm={createNewApp}
|
||||
onCancel={inline ? () => (template = null) : null}
|
||||
onCancel={inline ? onCancel : null}
|
||||
cancelText={inline ? "Back" : undefined}
|
||||
showCloseIcon={!inline}
|
||||
disabled={!valid}
|
||||
|
|
|
@ -37,6 +37,10 @@
|
|||
<p class="detail">{template?.category?.toUpperCase()}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:catch err}
|
||||
<h1 style="color:red">{err}</h1>
|
||||
{/await}
|
||||
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
|
||||
<div
|
||||
class="background-icon"
|
||||
|
@ -60,10 +64,6 @@
|
|||
<Heading size="XS">Import an app</Heading>
|
||||
<p class="detail">BLANK</p>
|
||||
</div>
|
||||
</div>
|
||||
{:catch err}
|
||||
<h1 style="color:red">{err}</h1>
|
||||
{/await}
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -138,3 +138,19 @@ export const RelationshipTypes = {
|
|||
ONE_TO_MANY: "one-to-many",
|
||||
MANY_TO_ONE: "many-to-one",
|
||||
}
|
||||
|
||||
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
|
||||
|
||||
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
|
||||
|
||||
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
|
||||
opt => opt.type
|
||||
)
|
||||
|
||||
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
|
||||
ALLOWABLE_STRING_TYPES
|
||||
)
|
||||
|
|
|
@ -39,6 +39,8 @@ export const UNEDITABLE_USER_FIELDS = [
|
|||
"lastName",
|
||||
]
|
||||
|
||||
export const UNSORTABLE_TYPES = ["formula", "attachment", "array", "link"]
|
||||
|
||||
export const LAYOUT_NAMES = {
|
||||
MASTER: {
|
||||
PRIVATE: "layout_private_master",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { isActive, redirect } from "@roxi/routify"
|
||||
import { isActive, redirect, params } from "@roxi/routify"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
|
@ -28,9 +28,13 @@
|
|||
}
|
||||
|
||||
if (user && user.tenantId) {
|
||||
// no tenant in the url - send to account portal to fix this
|
||||
if (!urlTenantId) {
|
||||
window.location.href = $admin.accountPortalUrl
|
||||
// redirect to correct tenantId subdomain
|
||||
if (!window.location.host.includes("localhost")) {
|
||||
let redirectUrl = window.location.href
|
||||
redirectUrl = redirectUrl.replace("://", `://${user.tenantId}.`)
|
||||
window.location.href = redirectUrl
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -47,6 +51,10 @@
|
|||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($params["?template"]) {
|
||||
await auth.setInitInfo({ init_template: $params["?template"] })
|
||||
}
|
||||
|
||||
await auth.checkAuth()
|
||||
await admin.init()
|
||||
|
||||
|
|
|
@ -8,17 +8,18 @@
|
|||
Layout,
|
||||
Modal,
|
||||
InlineAlert,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import CreateEditRelationship from "./CreateEditRelationship/CreateEditRelationship.svelte"
|
||||
import DisplayColumnModal from "./modals/EditDisplayColumnsModal.svelte"
|
||||
import CreateEditRelationship from "components/backend/Datasources/CreateEditRelationship.svelte"
|
||||
import CreateExternalTableModal from "./modals/CreateExternalTableModal.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
let relationshipModal
|
||||
let displayColumnModal
|
||||
let createExternalTableModal
|
||||
let selectedFromRelationship, selectedToRelationship
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === $datasources.selected)
|
||||
|
@ -78,6 +79,10 @@
|
|||
try {
|
||||
// Create datasource
|
||||
await datasources.save(datasource)
|
||||
if (datasource?.plus) {
|
||||
await tables.fetch()
|
||||
}
|
||||
await datasources.fetch()
|
||||
notifications.success(`Datasource ${name} updated successfully.`)
|
||||
} catch (err) {
|
||||
notifications.error(`Error saving datasource: ${err}`)
|
||||
|
@ -110,8 +115,8 @@
|
|||
relationshipModal.show()
|
||||
}
|
||||
|
||||
function openDisplayColumnModal() {
|
||||
displayColumnModal.show()
|
||||
function createNewTable() {
|
||||
createExternalTableModal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -126,8 +131,8 @@
|
|||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={displayColumnModal}>
|
||||
<DisplayColumnModal {datasource} {plusTables} save={saveDatasource} />
|
||||
<Modal bind:this={createExternalTableModal}>
|
||||
<CreateExternalTableModal {datasource} />
|
||||
</Modal>
|
||||
|
||||
{#if datasource && integration}
|
||||
|
@ -163,15 +168,15 @@
|
|||
<div class="query-header">
|
||||
<Heading size="S">Tables</Heading>
|
||||
<div class="table-buttons">
|
||||
{#if plusTables && plusTables.length !== 0}
|
||||
<Button primary on:click={openDisplayColumnModal}>
|
||||
Update display columns
|
||||
</Button>
|
||||
{/if}
|
||||
<div>
|
||||
<Button primary on:click={updateDatasourceSchema}>
|
||||
<ActionButton
|
||||
size="S"
|
||||
quiet
|
||||
icon="DataRefresh"
|
||||
on:click={updateDatasourceSchema}
|
||||
>
|
||||
Fetch tables from database
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -196,14 +201,23 @@
|
|||
<p>→</p>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="add-table">
|
||||
<Button cta on:click={createNewTable}>Create new table</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if plusTables?.length !== 0}
|
||||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Relationships</Heading>
|
||||
<Button primary on:click={() => openRelationshipModal()}
|
||||
>Create relationship</Button
|
||||
<ActionButton
|
||||
icon="DataCorrelated"
|
||||
primary
|
||||
size="S"
|
||||
quiet
|
||||
on:click={openRelationshipModal}
|
||||
>
|
||||
Define existing relationship
|
||||
</ActionButton>
|
||||
</div>
|
||||
<Body>
|
||||
Tell budibase how your tables are related to get even more smart
|
||||
|
@ -318,11 +332,14 @@
|
|||
|
||||
.table-buttons {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-l);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.table-buttons div {
|
||||
grid-column-end: -1;
|
||||
}
|
||||
|
||||
.add-table {
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { ModalContent, Body, Input } from "@budibase/bbui"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
export let datasource
|
||||
|
||||
let name = ""
|
||||
$: valid = name && name.length > 0 && !datasource?.entities[name]
|
||||
$: error =
|
||||
name && datasource?.entities[name] ? "Table name already in use." : null
|
||||
|
||||
function buildDefaultTable(tableName, datasourceId) {
|
||||
return {
|
||||
name: tableName,
|
||||
type: "external",
|
||||
primary: ["id"],
|
||||
sourceId: datasourceId,
|
||||
schema: {
|
||||
id: {
|
||||
autocolumn: true,
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTable() {
|
||||
const table = await tables.save(buildDefaultTable(name, datasource._id))
|
||||
await datasources.fetch()
|
||||
$goto(`../../table/${table._id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create new table"
|
||||
confirmText="Create"
|
||||
onConfirm={saveTable}
|
||||
disabled={!valid}
|
||||
>
|
||||
<Body
|
||||
>Provide a name for your new table; you can add columns once it is created.</Body
|
||||
>
|
||||
<Input label="Table Name" bind:error bind:value={name} />
|
||||
</ModalContent>
|
|
@ -1,43 +0,0 @@
|
|||
<script>
|
||||
import { ModalContent, Select, Body } from "@budibase/bbui"
|
||||
import { tables } from "stores/backend"
|
||||
|
||||
export let datasource
|
||||
export let plusTables
|
||||
export let save
|
||||
|
||||
async function saveDisplayColumns() {
|
||||
// be explicit about copying over
|
||||
for (let table of plusTables) {
|
||||
datasource.entities[table.name].primaryDisplay = table.primaryDisplay
|
||||
}
|
||||
save()
|
||||
await tables.fetch()
|
||||
}
|
||||
|
||||
function getColumnOptions(table) {
|
||||
if (!table || !table.schema) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(table.schema)
|
||||
.filter(field => field[1].type !== "link")
|
||||
.map(([fieldName]) => fieldName)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Edit display columns"
|
||||
confirmText="Save"
|
||||
onConfirm={saveDisplayColumns}
|
||||
>
|
||||
<Body
|
||||
>Select the columns that will be shown when displaying relationships.</Body
|
||||
>
|
||||
{#each plusTables as table}
|
||||
<Select
|
||||
label={table.name}
|
||||
options={getColumnOptions(table)}
|
||||
bind:value={table.primaryDisplay}
|
||||
/>
|
||||
{/each}
|
||||
</ModalContent>
|
|
@ -44,7 +44,7 @@
|
|||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
notifications.error("Invalid credentials")
|
||||
notifications.error(err.message ? err.message : "Invalid Credentials")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
ActionButton,
|
||||
ActionGroup,
|
||||
ButtonGroup,
|
||||
Input,
|
||||
Select,
|
||||
Modal,
|
||||
Page,
|
||||
|
@ -36,6 +37,7 @@
|
|||
let loaded = false
|
||||
let searchTerm = ""
|
||||
let cloud = $admin.cloud
|
||||
let appName = ""
|
||||
|
||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||
$: filteredApps = enrichedApps.filter(app =>
|
||||
|
@ -163,6 +165,7 @@
|
|||
notifications.error(`Error deleting app: ${err}`)
|
||||
}
|
||||
selectedApp = null
|
||||
appName = null
|
||||
}
|
||||
|
||||
const updateApp = async app => {
|
||||
|
@ -184,9 +187,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
function createAppFromTemplateUrl(templateKey) {
|
||||
// validate the template key just to make sure
|
||||
const templateParts = templateKey.split("/")
|
||||
if (templateParts.length === 2 && templateParts[0] === "app") {
|
||||
template = {
|
||||
key: templateKey,
|
||||
}
|
||||
initiateAppCreation()
|
||||
} else {
|
||||
notifications.error("Your Template URL is invalid. Please try another.")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await apps.load()
|
||||
loaded = true
|
||||
// if the portal is loaded from an external URL with a template param
|
||||
const initInfo = await auth.getInitInfo()
|
||||
if (initInfo.init_template) {
|
||||
createAppFromTemplateUrl(initInfo.init_template)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -278,8 +299,13 @@
|
|||
title="Confirm deletion"
|
||||
okText="Delete app"
|
||||
onOk={confirmDeleteApp}
|
||||
onCancel={() => (appName = null)}
|
||||
disabled={appName !== selectedApp?.name}
|
||||
>
|
||||
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
|
||||
|
||||
<p>Please enter the app name below to confirm.</p>
|
||||
<Input bind:value={appName} data-cy="delete-app-confirmation" />
|
||||
</ConfirmDialog>
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { writable, get } from "svelte/store"
|
|||
import { views, queries, datasources } from "./"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import api from "builderStore/api"
|
||||
import { SWITCHABLE_TYPES } from "../../constants/backend"
|
||||
|
||||
export function createTablesStore() {
|
||||
const store = writable({})
|
||||
|
@ -11,6 +12,7 @@ export function createTablesStore() {
|
|||
const tablesResponse = await api.get(`/api/tables`)
|
||||
const tables = await tablesResponse.json()
|
||||
update(state => ({ ...state, list: tables }))
|
||||
return tables
|
||||
}
|
||||
|
||||
async function select(table) {
|
||||
|
@ -46,7 +48,11 @@ export function createTablesStore() {
|
|||
const field = updatedTable.schema[key]
|
||||
const oldField = oldTable?.schema[key]
|
||||
// if the type has changed then revert back to the old field
|
||||
if (oldField != null && oldField?.type !== field.type) {
|
||||
if (
|
||||
oldField != null &&
|
||||
oldField?.type !== field.type &&
|
||||
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
|
||||
) {
|
||||
updatedTable.schema[key] = oldField
|
||||
}
|
||||
// field has been renamed
|
||||
|
@ -62,6 +68,9 @@ export function createTablesStore() {
|
|||
const response = await api.post(`/api/tables`, updatedTable)
|
||||
const savedTable = await response.json()
|
||||
await fetch()
|
||||
if (table.type === "external") {
|
||||
await datasources.fetch()
|
||||
}
|
||||
await select(savedTable)
|
||||
return savedTable
|
||||
}
|
||||
|
|
|
@ -57,11 +57,11 @@ export function createAuthStore() {
|
|||
analytics.showChat({
|
||||
email: user.email,
|
||||
created_at: (user.createdAt || Date.now()) / 1000,
|
||||
name: user.name,
|
||||
name: user.account?.name,
|
||||
user_id: user._id,
|
||||
tenant: user.tenantId,
|
||||
"Company size": user.size,
|
||||
"Job role": user.profession,
|
||||
"Company size": user.account?.size,
|
||||
"Job role": user.account?.profession,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -80,9 +80,30 @@ export function createAuthStore() {
|
|||
}
|
||||
}
|
||||
|
||||
async function setInitInfo(info) {
|
||||
await api.post(`/api/global/auth/init`, info)
|
||||
auth.update(store => {
|
||||
store.initInfo = info
|
||||
return store
|
||||
})
|
||||
return info
|
||||
}
|
||||
|
||||
async function getInitInfo() {
|
||||
const response = await api.get(`/api/global/auth/init`)
|
||||
const json = response.json()
|
||||
auth.update(store => {
|
||||
store.initInfo = json
|
||||
return store
|
||||
})
|
||||
return json
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
setOrganisation: setOrganisation,
|
||||
setOrganisation,
|
||||
getInitInfo,
|
||||
setInitInfo,
|
||||
checkQueryString: async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.has("tenantId")) {
|
||||
|
@ -112,7 +133,7 @@ export function createAuthStore() {
|
|||
if (response.status === 200) {
|
||||
setUser(json.user)
|
||||
} else {
|
||||
throw "Invalid credentials"
|
||||
throw new Error(json.message ? json.message : "Invalid credentials")
|
||||
}
|
||||
return json
|
||||
},
|
||||
|
@ -122,6 +143,7 @@ export function createAuthStore() {
|
|||
throw "Unable to create logout"
|
||||
}
|
||||
await response.json()
|
||||
await setInitInfo({})
|
||||
setUser(null)
|
||||
},
|
||||
updateSelf: async fields => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "0.9.173-alpha.5",
|
||||
"version": "0.9.185-alpha.2",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -19,7 +19,7 @@ The object key is the name of the component, as exported by `index.js`.
|
|||
- **bindable** - whether the components provides a bindable value or not
|
||||
- **settings** - array of settings displayed in the builder
|
||||
|
||||
###Settings Definitions
|
||||
### Settings Definitions
|
||||
|
||||
The `type` field in each setting is used by the builder to know which component to use to display
|
||||
the setting, so it's important that this field is correct. The valid options are:
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"deviceAwareness": true,
|
||||
"state": true,
|
||||
"customThemes": true,
|
||||
"devicePreview": true
|
||||
"devicePreview": true,
|
||||
"messagePassing": true
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
|
@ -2481,12 +2482,12 @@
|
|||
"settings": [
|
||||
{
|
||||
"type": "dataProvider",
|
||||
"label": "Provider",
|
||||
"label": "Data provider",
|
||||
"key": "dataProvider"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Row Count",
|
||||
"label": "Row count",
|
||||
"key": "rowCount",
|
||||
"defaultValue": 8
|
||||
},
|
||||
|
@ -2520,9 +2521,36 @@
|
|||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Auto Columns",
|
||||
"label": "Show auto columns",
|
||||
"key": "showAutoColumns",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Link table rows",
|
||||
"key": "linkRows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link screens in modal",
|
||||
"key": "linkPeek"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Link screen",
|
||||
"key": "linkURL"
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Advanced",
|
||||
"settings": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "ID column for linking (appended to URL)",
|
||||
"key": "linkColumn",
|
||||
"placeholder": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
|
@ -2592,10 +2620,539 @@
|
|||
"key": "linkURL",
|
||||
"label": "Link URL"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "linkPeek",
|
||||
"label": "Open link in modal"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "horizontal",
|
||||
"label": "Horizontal"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show button",
|
||||
"key": "showButton"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "buttonText",
|
||||
"label": "Button text"
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"label": "Button action",
|
||||
"key": "buttonOnClick"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tableblock": {
|
||||
"block": true,
|
||||
"name": "Table block",
|
||||
"icon": "Table",
|
||||
"styles": ["size"],
|
||||
"info": "Only the first 3 search columns will be used.",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title"
|
||||
},
|
||||
{
|
||||
"type": "dataSource",
|
||||
"label": "Data",
|
||||
"key": "dataSource"
|
||||
},
|
||||
{
|
||||
"type": "multifield",
|
||||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Sort Column",
|
||||
"key": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort Order",
|
||||
"key": "sortOrder",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Descending"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"defaultValue": "spectrum--medium",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Paginate",
|
||||
"key": "paginate",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Table",
|
||||
"settings": [
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Row Count",
|
||||
"key": "rowCount",
|
||||
"defaultValue": 8
|
||||
},
|
||||
{
|
||||
"type": "multifield",
|
||||
"label": "Table Columns",
|
||||
"key": "tableColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"placeholder": "All columns"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Quiet table variant",
|
||||
"key": "quiet"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show auto columns",
|
||||
"key": "showAutoColumns"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Link table rows",
|
||||
"key": "linkRows"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link in modal",
|
||||
"key": "linkPeek"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Link screen",
|
||||
"key": "linkURL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Title button",
|
||||
"settings": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "showTitleButton",
|
||||
"label": "Show link button",
|
||||
"defaultValue": false
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link in modal",
|
||||
"key": "titleButtonPeek"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "titleButtonText",
|
||||
"label": "Button text"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Button link",
|
||||
"key": "titleButtonURL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Advanced",
|
||||
"settings": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "ID column for linking (appended to URL)",
|
||||
"key": "linkColumn",
|
||||
"placeholder": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"cardsblock": {
|
||||
"block": true,
|
||||
"name": "Cards block",
|
||||
"icon": "Table",
|
||||
"styles": ["size"],
|
||||
"info": "Only the first 3 search columns will be used.",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Title",
|
||||
"key": "title"
|
||||
},
|
||||
{
|
||||
"type": "dataSource",
|
||||
"label": "Data",
|
||||
"key": "dataSource"
|
||||
},
|
||||
{
|
||||
"type": "multifield",
|
||||
"label": "Search Columns",
|
||||
"key": "searchColumns",
|
||||
"placeholder": "Choose search columns"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Sort Column",
|
||||
"key": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort Order",
|
||||
"key": "sortOrder",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Descending"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Limit",
|
||||
"key": "limit",
|
||||
"defaultValue": 8
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Paginate",
|
||||
"key": "paginate"
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Cards",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardTitle",
|
||||
"label": "Title",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardSubtitle",
|
||||
"label": "Subtitle",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardDescription",
|
||||
"label": "Description",
|
||||
"nested": true
|
||||
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardImageURL",
|
||||
"label": "Image URL",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "linkCardTitle",
|
||||
"label": "Link card title"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "cardPeek",
|
||||
"label": "Open link in modal"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Link screen",
|
||||
"key": "cardURL",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "cardHorizontal",
|
||||
"label": "Horizontal"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Show button",
|
||||
"key": "showCardButton"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "cardButtonText",
|
||||
"label": "Button text",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "event",
|
||||
"label": "Button action",
|
||||
"key": "cardButtonOnClick",
|
||||
"nested": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Title button",
|
||||
"settings": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "showTitleButton",
|
||||
"label": "Show link button"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Open link in modal",
|
||||
"key": "titleButtonPeek"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"key": "titleButtonText",
|
||||
"label": "Button text"
|
||||
},
|
||||
{
|
||||
"type": "url",
|
||||
"label": "Button link",
|
||||
"key": "titleButtonURL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Advanced",
|
||||
"settings": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "ID column for linking (appended to URL)",
|
||||
"key": "linkColumn",
|
||||
"placeholder": "Default"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": {
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
},
|
||||
"repeaterblock": {
|
||||
"name": "Repeater block",
|
||||
"icon": "ViewList",
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
"showSettingsBar": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "dataSource",
|
||||
"label": "Data",
|
||||
"key": "dataSource"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Sort Column",
|
||||
"key": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort Order",
|
||||
"key": "sortOrder",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Descending"
|
||||
},
|
||||
{
|
||||
"type": "number",
|
||||
"label": "Limit",
|
||||
"key": "limit",
|
||||
"defaultValue": 10
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Paginate",
|
||||
"key": "paginate"
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Layout settings",
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Empty Text",
|
||||
"key": "noRowsMessage",
|
||||
"defaultValue": "No rows found"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Direction",
|
||||
"key": "direction",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Column",
|
||||
"value": "column",
|
||||
"barIcon": "ViewRow",
|
||||
"barTitle": "Column layout"
|
||||
},
|
||||
{
|
||||
"label": "Row",
|
||||
"value": "row",
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "Row layout"
|
||||
}
|
||||
],
|
||||
"defaultValue": "column"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Horiz. Align",
|
||||
"key": "hAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left",
|
||||
"barIcon": "AlignLeft",
|
||||
"barTitle": "Align left"
|
||||
},
|
||||
{
|
||||
"label": "Center",
|
||||
"value": "center",
|
||||
"barIcon": "AlignCenter",
|
||||
"barTitle": "Align center"
|
||||
},
|
||||
{
|
||||
"label": "Right",
|
||||
"value": "right",
|
||||
"barIcon": "AlignRight",
|
||||
"barTitle": "Align right"
|
||||
},
|
||||
{
|
||||
"label": "Stretch",
|
||||
"value": "stretch",
|
||||
"barIcon": "MoveLeftRight",
|
||||
"barTitle": "Align stretched horizontally"
|
||||
}
|
||||
],
|
||||
"defaultValue": "stretch"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Vert. Align",
|
||||
"key": "vAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Top",
|
||||
"value": "top",
|
||||
"barIcon": "AlignTop",
|
||||
"barTitle": "Align top"
|
||||
},
|
||||
{
|
||||
"label": "Middle",
|
||||
"value": "middle",
|
||||
"barIcon": "AlignMiddle",
|
||||
"barTitle": "Align middle"
|
||||
},
|
||||
{
|
||||
"label": "Bottom",
|
||||
"value": "bottom",
|
||||
"barIcon": "AlignBottom",
|
||||
"barTitle": "Align bottom"
|
||||
},
|
||||
{
|
||||
"label": "Stretch",
|
||||
"value": "stretch",
|
||||
"barIcon": "MoveUpDown",
|
||||
"barTitle": "Align stretched vertically"
|
||||
}
|
||||
],
|
||||
"defaultValue": "top"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Gap",
|
||||
"key": "gap",
|
||||
"showInBar": true,
|
||||
"barStyle": "picker",
|
||||
"options": [
|
||||
{
|
||||
"label": "None",
|
||||
"value": "N"
|
||||
},
|
||||
{
|
||||
"label": "Small",
|
||||
"value": "S"
|
||||
},
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "M"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "L"
|
||||
}
|
||||
],
|
||||
"defaultValue": "M"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"type": "static",
|
||||
"suffix": "provider",
|
||||
"values": [
|
||||
{
|
||||
"label": "Rows",
|
||||
"key": "rows"
|
||||
},
|
||||
{
|
||||
"label": "Rows Length",
|
||||
"key": "rowsLength"
|
||||
},
|
||||
{
|
||||
"label": "Schema",
|
||||
"key": "schema"
|
||||
},
|
||||
{
|
||||
"label": "Page Number",
|
||||
"key": "pageNumber"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "0.9.173-alpha.5",
|
||||
"version": "0.9.185-alpha.2",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^0.9.173-alpha.5",
|
||||
"@budibase/bbui": "^0.9.185-alpha.2",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^0.9.173-alpha.5",
|
||||
"@budibase/string-templates": "^0.9.185-alpha.2",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { cloneDeep } from "lodash/fp"
|
||||
import { fetchTableData } from "./tables"
|
||||
import { fetchTableData, fetchTableDefinition } from "./tables"
|
||||
import { fetchViewData } from "./views"
|
||||
import { fetchRelationshipData } from "./relationships"
|
||||
import { executeQuery } from "./queries"
|
||||
import { FieldTypes } from "../constants"
|
||||
import { executeQuery, fetchQueryDefinition } from "./queries"
|
||||
|
||||
/**
|
||||
* Fetches all rows for a particular Budibase data source.
|
||||
|
@ -28,7 +29,7 @@ export const fetchDatasource = async dataSource => {
|
|||
}
|
||||
}
|
||||
rows = await executeQuery({ queryId: dataSource._id, parameters })
|
||||
} else if (type === "link") {
|
||||
} else if (type === FieldTypes.LINK) {
|
||||
rows = await fetchRelationshipData({
|
||||
rowId: dataSource.rowId,
|
||||
tableId: dataSource.rowTableId,
|
||||
|
@ -39,3 +40,55 @@ export const fetchDatasource = async dataSource => {
|
|||
// Enrich the result is always an array
|
||||
return Array.isArray(rows) ? rows : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the schema of any kind of datasource.
|
||||
*/
|
||||
export const fetchDatasourceSchema = async dataSource => {
|
||||
if (!dataSource) {
|
||||
return null
|
||||
}
|
||||
const { type } = dataSource
|
||||
|
||||
// Nested providers should already have exposed their own schema
|
||||
if (type === "provider") {
|
||||
return dataSource.value?.schema
|
||||
}
|
||||
|
||||
// Field sources have their schema statically defined
|
||||
if (type === "field") {
|
||||
if (dataSource.fieldType === "attachment") {
|
||||
return {
|
||||
url: {
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
} else if (dataSource.fieldType === "array") {
|
||||
return {
|
||||
value: {
|
||||
type: "string",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tables, views and links can be fetched by table ID
|
||||
if (
|
||||
(type === "table" || type === "view" || type === "link") &&
|
||||
dataSource.tableId
|
||||
) {
|
||||
const table = await fetchTableDefinition(dataSource.tableId)
|
||||
return table?.schema
|
||||
}
|
||||
|
||||
// Queries can be fetched by query ID
|
||||
if (type === "query" && dataSource._id) {
|
||||
const definition = await fetchQueryDefinition(dataSource._id)
|
||||
return definition?.schema
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import API from "./api"
|
|||
* Executes a query against an external data connector.
|
||||
*/
|
||||
export const executeQuery = async ({ queryId, parameters }) => {
|
||||
const query = await API.get({ url: `/api/queries/${queryId}` })
|
||||
const query = await fetchQueryDefinition(queryId)
|
||||
if (query?.datasourceId == null) {
|
||||
notificationStore.actions.error("That query couldn't be found")
|
||||
return
|
||||
|
@ -24,3 +24,10 @@ export const executeQuery = async ({ queryId, parameters }) => {
|
|||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the definition of an external query.
|
||||
*/
|
||||
export const fetchQueryDefinition = async queryId => {
|
||||
return await API.get({ url: `/api/queries/${queryId}`, cache: true })
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { notificationStore, dataSourceStore } from "stores"
|
||||
import API from "./api"
|
||||
import { fetchTableDefinition } from "./tables"
|
||||
import { FieldTypes } from "../constants"
|
||||
|
||||
/**
|
||||
* Fetches data about a certain row in a table.
|
||||
|
@ -107,6 +108,8 @@ export const deleteRows = async ({ tableId, rows }) => {
|
|||
/**
|
||||
* Enriches rows which contain certain field types so that they can
|
||||
* be properly displayed.
|
||||
* The ability to create these bindings has been removed, but they will still
|
||||
* exist in client apps to support backwards compatibility.
|
||||
*/
|
||||
export const enrichRows = async (rows, tableId) => {
|
||||
if (!Array.isArray(rows)) {
|
||||
|
@ -129,7 +132,7 @@ export const enrichRows = async (rows, tableId) => {
|
|||
const keys = Object.keys(schema)
|
||||
for (let key of keys) {
|
||||
const type = schema[key].type
|
||||
if (type === "link" && Array.isArray(row[key])) {
|
||||
if (type === FieldTypes.LINK && Array.isArray(row[key])) {
|
||||
// Enrich row a string join of relationship fields
|
||||
row[`${key}_text`] =
|
||||
row[key]
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
|
||||
const component = getContext("component")
|
||||
|
||||
// We need to set a block context to know we're inside a block, but also
|
||||
// to be able to reference the actual component ID of the block from
|
||||
// any depth
|
||||
setContext("block", { id: $component.id })
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import Component from "components/Component.svelte"
|
||||
|
||||
export let type
|
||||
export let props
|
||||
export let styles
|
||||
export let context
|
||||
|
||||
// ID is only exposed as a prop so that it can be bound to from parent
|
||||
// block components
|
||||
export let id
|
||||
|
||||
const block = getContext("block")
|
||||
const rand = generate()
|
||||
|
||||
// Create a fake component instance so that we can use the core Component
|
||||
// to render this part of the block, taking advantage of binding enrichment
|
||||
$: id = `${block.id}-${context ?? rand}`
|
||||
$: instance = {
|
||||
_component: `@budibase/standard-components/${type}`,
|
||||
_id: id,
|
||||
_styles: {
|
||||
normal: {
|
||||
...styles,
|
||||
},
|
||||
},
|
||||
...props,
|
||||
}
|
||||
</script>
|
||||
|
||||
<Component {instance} isBlock>
|
||||
<slot />
|
||||
</Component>
|
|
@ -1,3 +1,7 @@
|
|||
<script context="module">
|
||||
let SettingsDefinitionCache = {}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { writable, get } from "svelte/store"
|
||||
|
@ -13,6 +17,7 @@
|
|||
export let instance = {}
|
||||
export let isLayout = false
|
||||
export let isScreen = false
|
||||
export let isBlock = false
|
||||
|
||||
// The enriched component settings
|
||||
let enrichedSettings
|
||||
|
@ -20,13 +25,13 @@
|
|||
// Any prop overrides that need to be applied due to conditional UI
|
||||
let conditionalSettings
|
||||
|
||||
// Props are hashed when inside the builder preview and used as a key, so that
|
||||
// components fully remount whenever any props change
|
||||
let propsHash = 0
|
||||
// Settings are hashed when inside the builder preview and used as a key,
|
||||
// so that components fully remount whenever any settings change
|
||||
let hash = 0
|
||||
|
||||
// Latest timestamp that we started a props update.
|
||||
// Due to enrichment now being async, we need to avoid overwriting newer
|
||||
// props with old ones, depending on how long enrichment takes.
|
||||
// settings with old ones, depending on how long enrichment takes.
|
||||
let latestUpdateTime
|
||||
|
||||
// Keep track of stringified representations of context and instance
|
||||
|
@ -48,28 +53,61 @@
|
|||
// Extract component instance info
|
||||
$: constructor = getComponentConstructor(instance._component)
|
||||
$: definition = getComponentDefinition(instance._component)
|
||||
$: settingsDefinition = getSettingsDefinition(definition)
|
||||
$: children = instance._children || []
|
||||
$: id = instance._id
|
||||
$: name = instance._instanceName
|
||||
$: interactive =
|
||||
$builderStore.inBuilder &&
|
||||
($builderStore.previewType === "layout" || insideScreenslot)
|
||||
$: empty = interactive && !children.length && definition?.hasChildren
|
||||
$: emptyState = empty && definition?.showEmptyState !== false
|
||||
$: rawProps = getRawProps(instance)
|
||||
$: instanceKey = JSON.stringify(rawProps)
|
||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||
|
||||
// Determine if the component is selected or is part of the critical path
|
||||
// leading to the selected component
|
||||
$: selected =
|
||||
$builderStore.inBuilder && $builderStore.selectedComponentId === id
|
||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||
$: evaluateConditions(enrichedSettings?._conditions)
|
||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||
$: renderKey = `${propsHash}-${emptyState}`
|
||||
|
||||
// Interactive components can be selected, dragged and highlighted inside
|
||||
// the builder preview
|
||||
$: interactive =
|
||||
$builderStore.inBuilder &&
|
||||
($builderStore.previewType === "layout" || insideScreenslot) &&
|
||||
!isBlock
|
||||
$: draggable = interactive && !isLayout && !isScreen
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
$: editable = definition.editable
|
||||
$: editing = editable && selected && $builderStore.editMode
|
||||
$: draggable =
|
||||
interactive && !isLayout && !isScreen && !$builderStore.editMode
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
|
||||
// Empty components are those which accept children but do not have any.
|
||||
// Empty states can be shown for these components, but can be disabled
|
||||
// in the component manifest.
|
||||
$: empty = interactive && !children.length && definition?.hasChildren
|
||||
$: emptyState = empty && definition?.showEmptyState !== false
|
||||
|
||||
// Raw props are all props excluding internal props and children
|
||||
$: rawSettings = getRawSettings(instance)
|
||||
$: instanceKey = hashString(JSON.stringify(rawSettings))
|
||||
|
||||
// Component settings are those which are intended for this component and
|
||||
// which need to be enriched
|
||||
$: componentSettings = getComponentSettings(rawSettings, settingsDefinition)
|
||||
$: enrichComponentSettings(rawSettings, instanceKey, $context)
|
||||
|
||||
// Nested settings are those which are intended for child components inside
|
||||
// blocks and which should not be enriched at this level
|
||||
$: nestedSettings = getNestedSettings(rawSettings, settingsDefinition)
|
||||
|
||||
// Evaluate conditional UI settings and store any component setting changes
|
||||
// which need to be made
|
||||
$: evaluateConditions(enrichedSettings?._conditions)
|
||||
|
||||
// Build up the final settings object to be passed to the component
|
||||
$: settings = {
|
||||
...enrichedSettings,
|
||||
...nestedSettings,
|
||||
...conditionalSettings,
|
||||
}
|
||||
|
||||
// Render key is used when in the builder preview to fully remount
|
||||
// components when settings are changed
|
||||
$: renderKey = `${hash}-${emptyState}`
|
||||
|
||||
// Update component context
|
||||
$: componentStore.set({
|
||||
|
@ -89,14 +127,14 @@
|
|||
editing,
|
||||
})
|
||||
|
||||
const getRawProps = instance => {
|
||||
let validProps = {}
|
||||
const getRawSettings = instance => {
|
||||
let validSettings = {}
|
||||
Object.entries(instance)
|
||||
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
||||
.forEach(([key, value]) => {
|
||||
validProps[key] = value
|
||||
validSettings[key] = value
|
||||
})
|
||||
return validProps
|
||||
return validSettings
|
||||
}
|
||||
|
||||
// Gets the component constructor for the specified component
|
||||
|
@ -115,8 +153,47 @@
|
|||
return type ? Manifest[type] : null
|
||||
}
|
||||
|
||||
const getSettingsDefinition = definition => {
|
||||
if (!definition) {
|
||||
return []
|
||||
}
|
||||
if (SettingsDefinitionCache[definition.name]) {
|
||||
return SettingsDefinitionCache[definition.name]
|
||||
}
|
||||
let settings = []
|
||||
definition.settings?.forEach(setting => {
|
||||
if (setting.section) {
|
||||
settings = settings.concat(setting.settings || [])
|
||||
} else {
|
||||
settings.push(setting)
|
||||
}
|
||||
})
|
||||
SettingsDefinitionCache[definition] = settings
|
||||
return settings
|
||||
}
|
||||
|
||||
const getComponentSettings = (rawSettings, settingsDefinition) => {
|
||||
let clone = { ...rawSettings }
|
||||
settingsDefinition?.forEach(setting => {
|
||||
if (setting.nested) {
|
||||
delete clone[setting.key]
|
||||
}
|
||||
})
|
||||
return clone
|
||||
}
|
||||
|
||||
const getNestedSettings = (rawSettings, settingsDefinition) => {
|
||||
let clone = { ...rawSettings }
|
||||
settingsDefinition?.forEach(setting => {
|
||||
if (!setting.nested) {
|
||||
delete clone[setting.key]
|
||||
}
|
||||
})
|
||||
return clone
|
||||
}
|
||||
|
||||
// Enriches any string component props using handlebars
|
||||
const updateComponentProps = (rawProps, instanceKey, context) => {
|
||||
const enrichComponentSettings = (rawSettings, instanceKey, context) => {
|
||||
const instanceSame = instanceKey === lastInstanceKey
|
||||
const contextSame = context.key === lastContextKey
|
||||
|
||||
|
@ -131,8 +208,8 @@
|
|||
latestUpdateTime = Date.now()
|
||||
const enrichmentTime = latestUpdateTime
|
||||
|
||||
// Enrich props with context
|
||||
const enrichedProps = enrichProps(rawProps, context)
|
||||
// Enrich settings with context
|
||||
const newEnrichedSettings = enrichProps(rawSettings, context)
|
||||
|
||||
// Abandon this update if a newer update has started
|
||||
if (enrichmentTime !== latestUpdateTime) {
|
||||
|
@ -142,7 +219,7 @@
|
|||
// Update the component props.
|
||||
// Most props are deeply compared so that svelte will only trigger reactive
|
||||
// statements on props that have actually changed.
|
||||
if (!enrichedProps) {
|
||||
if (!newEnrichedSettings) {
|
||||
return
|
||||
}
|
||||
let propsChanged = false
|
||||
|
@ -150,17 +227,17 @@
|
|||
enrichedSettings = {}
|
||||
propsChanged = true
|
||||
}
|
||||
Object.keys(enrichedProps).forEach(key => {
|
||||
if (!propsAreSame(enrichedProps[key], enrichedSettings[key])) {
|
||||
Object.keys(newEnrichedSettings).forEach(key => {
|
||||
if (!propsAreSame(newEnrichedSettings[key], enrichedSettings[key])) {
|
||||
propsChanged = true
|
||||
enrichedSettings[key] = enrichedProps[key]
|
||||
enrichedSettings[key] = newEnrichedSettings[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Update the hash if we're in the builder so we can fully remount this
|
||||
// component
|
||||
if (get(builderStore).inBuilder && propsChanged) {
|
||||
propsHash = hashString(JSON.stringify(enrichedSettings))
|
||||
hash = hashString(JSON.stringify(enrichedSettings))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,7 +263,7 @@
|
|||
</script>
|
||||
|
||||
{#key renderKey}
|
||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||
{#if constructor && settings && (visible || inSelectedPath)}
|
||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||
<!-- and the performance matters for the selection indicators -->
|
||||
<div
|
||||
|
@ -196,16 +273,19 @@
|
|||
class:empty
|
||||
class:interactive
|
||||
class:editing
|
||||
class:block={isBlock}
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
>
|
||||
<svelte:component this={constructor} {...componentSettings}>
|
||||
<svelte:component this={constructor} {...settings}>
|
||||
{#if children.length}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self instance={child} />
|
||||
{/each}
|
||||
{:else if emptyState}
|
||||
<Placeholder />
|
||||
{:else if isBlock}
|
||||
<slot />
|
||||
{/if}
|
||||
</svelte:component>
|
||||
</div>
|
||||
|
|
|
@ -46,6 +46,9 @@
|
|||
button {
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.spectrum-Button--overBackground:hover {
|
||||
color: #555;
|
||||
|
|
|
@ -35,28 +35,36 @@
|
|||
let pageNumber = 0
|
||||
let query = null
|
||||
|
||||
// Sorting can be overridden at run time, so we can't use the prop directly
|
||||
let currentSortColumn = sortColumn
|
||||
let currentSortOrder = sortOrder
|
||||
|
||||
$: query = buildLuceneQuery(filter)
|
||||
$: internalTable = dataSource?.type === "table"
|
||||
$: nestedProvider = dataSource?.type === "provider"
|
||||
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
||||
$: hasPrevPage = pageNumber > 0
|
||||
$: getSchema(dataSource)
|
||||
$: sortType = getSortType(schema, sortColumn)
|
||||
$: {
|
||||
$: sortType = getSortType(schema, currentSortColumn)
|
||||
|
||||
// Wait until schema loads before loading data, so that we can determine
|
||||
// the correct sort type first time
|
||||
$: {
|
||||
if (schemaLoaded) {
|
||||
fetchData(
|
||||
dataSource,
|
||||
schema,
|
||||
query,
|
||||
limit,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
currentSortColumn,
|
||||
currentSortOrder,
|
||||
sortType,
|
||||
paginate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Reactively filter and sort rows if required
|
||||
$: {
|
||||
if (internalTable) {
|
||||
// Internal tables are already processed server-side
|
||||
|
@ -65,10 +73,17 @@
|
|||
// For anything else we use client-side implementations to filter, sort
|
||||
// and limit
|
||||
const filtered = luceneQuery(allRows, query)
|
||||
const sorted = luceneSort(filtered, sortColumn, sortOrder, sortType)
|
||||
const sorted = luceneSort(
|
||||
filtered,
|
||||
currentSortColumn,
|
||||
currentSortOrder,
|
||||
sortType
|
||||
)
|
||||
rows = luceneLimit(sorted, limit)
|
||||
}
|
||||
}
|
||||
|
||||
// Build our action context
|
||||
$: actions = [
|
||||
{
|
||||
type: ActionTypes.RefreshDatasource,
|
||||
|
@ -79,7 +94,20 @@
|
|||
type: ActionTypes.SetDataProviderQuery,
|
||||
callback: newQuery => (query = newQuery),
|
||||
},
|
||||
{
|
||||
type: ActionTypes.SetDataProviderSorting,
|
||||
callback: ({ column, order }) => {
|
||||
if (column) {
|
||||
currentSortColumn = column
|
||||
}
|
||||
if (order) {
|
||||
currentSortOrder = order
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Build our data context
|
||||
$: dataContext = {
|
||||
rows,
|
||||
schema,
|
||||
|
@ -88,7 +116,11 @@
|
|||
// Undocumented properties. These aren't supposed to be used in builder
|
||||
// bindings, but are used internally by other components
|
||||
id: $component?.id,
|
||||
state: { query },
|
||||
state: {
|
||||
query,
|
||||
sortColumn: currentSortColumn,
|
||||
sortOrder: currentSortOrder,
|
||||
},
|
||||
loaded,
|
||||
}
|
||||
|
||||
|
@ -104,10 +136,11 @@
|
|||
if (schemaLoaded && !nestedProvider) {
|
||||
fetchData(
|
||||
dataSource,
|
||||
schema,
|
||||
query,
|
||||
limit,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
currentSortColumn,
|
||||
currentSortOrder,
|
||||
sortType,
|
||||
paginate
|
||||
)
|
||||
|
@ -116,6 +149,7 @@
|
|||
|
||||
const fetchData = async (
|
||||
dataSource,
|
||||
schema,
|
||||
query,
|
||||
limit,
|
||||
sortColumn,
|
||||
|
@ -125,12 +159,16 @@
|
|||
) => {
|
||||
loading = true
|
||||
if (dataSource?.type === "table") {
|
||||
// Sanity check sort column, as using a non-existant column will prevent
|
||||
// results coming back at all
|
||||
const sort = schema?.[sortColumn] ? sortColumn : undefined
|
||||
|
||||
// For internal tables we use server-side processing
|
||||
const res = await API.searchTable({
|
||||
tableId: dataSource.tableId,
|
||||
query,
|
||||
limit,
|
||||
sort: sortColumn,
|
||||
sort,
|
||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||
sortType,
|
||||
paginate,
|
||||
|
@ -145,7 +183,16 @@
|
|||
} else if (dataSource?.type === "provider") {
|
||||
// For providers referencing another provider, just use the rows it
|
||||
// provides
|
||||
allRows = dataSource?.value?.rows ?? []
|
||||
allRows = dataSource?.value?.rows || []
|
||||
} else if (dataSource?.type === "field") {
|
||||
// Field sources will be available from context.
|
||||
// Enrich non object elements into object to ensure a valid schema.
|
||||
const data = dataSource?.value || []
|
||||
if (Array.isArray(data) && data[0] && typeof data[0] !== "object") {
|
||||
allRows = data.map(value => ({ value }))
|
||||
} else {
|
||||
allRows = data
|
||||
}
|
||||
} else {
|
||||
// For other data sources like queries or views, fetch all rows from the
|
||||
// server
|
||||
|
@ -156,34 +203,24 @@
|
|||
}
|
||||
|
||||
const getSchema = async dataSource => {
|
||||
if (dataSource?.schema) {
|
||||
schema = dataSource.schema
|
||||
} else if (dataSource?.tableId) {
|
||||
const definition = await API.fetchTableDefinition(dataSource.tableId)
|
||||
schema = definition?.schema ?? {}
|
||||
} else if (dataSource?.type === "provider") {
|
||||
schema = dataSource.value?.schema ?? {}
|
||||
} else {
|
||||
schema = {}
|
||||
}
|
||||
let newSchema = (await API.fetchDatasourceSchema(dataSource)) || {}
|
||||
|
||||
// Ensure there are "name" properties for all fields and that field schema
|
||||
// are objects
|
||||
let fixedSchema = {}
|
||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
||||
Object.entries(newSchema).forEach(([fieldName, fieldSchema]) => {
|
||||
if (typeof fieldSchema === "string") {
|
||||
fixedSchema[fieldName] = {
|
||||
newSchema[fieldName] = {
|
||||
type: fieldSchema,
|
||||
name: fieldName,
|
||||
}
|
||||
} else {
|
||||
fixedSchema[fieldName] = {
|
||||
newSchema[fieldName] = {
|
||||
...fieldSchema,
|
||||
name: fieldName,
|
||||
}
|
||||
}
|
||||
})
|
||||
schema = fixedSchema
|
||||
schema = newSchema
|
||||
schemaLoaded = true
|
||||
}
|
||||
|
||||
|
@ -191,13 +228,14 @@
|
|||
if (!hasNextPage || !internalTable) {
|
||||
return
|
||||
}
|
||||
const sort = schema?.[currentSortColumn] ? currentSortColumn : undefined
|
||||
const res = await API.searchTable({
|
||||
tableId: dataSource?.tableId,
|
||||
query,
|
||||
bookmark: bookmarks[pageNumber + 1],
|
||||
limit,
|
||||
sort: sortColumn,
|
||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||
sort,
|
||||
sortOrder: currentSortOrder?.toLowerCase() ?? "ascending",
|
||||
sortType,
|
||||
paginate: true,
|
||||
})
|
||||
|
@ -212,13 +250,14 @@
|
|||
if (!hasPrevPage || !internalTable) {
|
||||
return
|
||||
}
|
||||
const sort = schema?.[currentSortColumn] ? currentSortColumn : undefined
|
||||
const res = await API.searchTable({
|
||||
tableId: dataSource?.tableId,
|
||||
query,
|
||||
bookmark: bookmarks[pageNumber - 1],
|
||||
limit,
|
||||
sort: sortColumn,
|
||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||
sort,
|
||||
sortOrder: currentSortOrder?.toLowerCase() ?? "ascending",
|
||||
sortType,
|
||||
paginate: true,
|
||||
})
|
||||
|
@ -234,7 +273,7 @@
|
|||
<ProgressCircle />
|
||||
</div>
|
||||
{:else}
|
||||
{#if !$component.children}
|
||||
{#if $component.emptyState}
|
||||
<Placeholder />
|
||||
{:else}
|
||||
<slot />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Heading, Icon } from "@budibase/bbui"
|
||||
import { FieldTypes } from "../../constants"
|
||||
import active from "svelte-spa-router/active"
|
||||
|
||||
const { routeStore, styleable, linkable, builderStore } = getContext("sdk")
|
||||
|
@ -108,7 +109,7 @@
|
|||
{#each validLinks as { text, url }}
|
||||
{#if isInternal(url)}
|
||||
<a
|
||||
class="link"
|
||||
class={FieldTypes.LINK}
|
||||
href={url}
|
||||
use:linkable
|
||||
on:click={close}
|
||||
|
@ -117,7 +118,11 @@
|
|||
{text}
|
||||
</a>
|
||||
{:else}
|
||||
<a class="link" href={ensureExternal(url)} on:click={close}>
|
||||
<a
|
||||
class={FieldTypes.LINK}
|
||||
href={ensureExternal(url)}
|
||||
on:click={close}
|
||||
>
|
||||
{text}
|
||||
</a>
|
||||
{/if}
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
padding: var(--spacing-l);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.noRows i {
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
|
|
@ -1,18 +1,29 @@
|
|||
<script>
|
||||
import "@spectrum-css/card/dist/index-vars.css"
|
||||
import { getContext } from "svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let subtitle
|
||||
export let description
|
||||
export let imageURL
|
||||
export let linkURL
|
||||
export let linkPeek
|
||||
export let horizontal
|
||||
export let showButton
|
||||
export let buttonText
|
||||
export let buttonOnClick
|
||||
|
||||
const { styleable, linkable } = getContext("sdk")
|
||||
const { styleable, routeStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
$: external = linkURL && !linkURL.startsWith("/")
|
||||
const handleLink = e => {
|
||||
if (!linkURL) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
routeStore.actions.navigate(linkURL, linkPeek)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -33,16 +44,10 @@
|
|||
<div class="spectrum-Card-header">
|
||||
<div
|
||||
class="spectrum-Card-title spectrum-Heading spectrum-Heading--sizeXS"
|
||||
on:click={handleLink}
|
||||
class:link={linkURL}
|
||||
>
|
||||
{#if linkURL}
|
||||
{#if external}
|
||||
<a href={linkURL}>{title || "Card Title"}</a>
|
||||
{:else}
|
||||
<a use:linkable href={linkURL}>{title || "Card Title"}</a>
|
||||
{/if}
|
||||
{:else}
|
||||
{title || "Card Title"}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if subtitle}
|
||||
|
@ -60,12 +65,17 @@
|
|||
{description}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showButton}
|
||||
<div class="spectrum-Card-footer button-container">
|
||||
<Button on:click={buttonOnClick} secondary>{buttonText || ""}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spectrum-Card {
|
||||
width: 240px;
|
||||
width: 300px;
|
||||
border-color: var(--spectrum-global-color-gray-300) !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -76,11 +86,15 @@
|
|||
flex-direction: row;
|
||||
width: 420px;
|
||||
}
|
||||
.spectrum-Card-title :global(a) {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
.spectrum-Card-container {
|
||||
padding: var(--spectrum-global-dimension-size-50) 0;
|
||||
}
|
||||
.spectrum-Card-title.link {
|
||||
transition: color 130ms ease-in-out;
|
||||
}
|
||||
.spectrum-Card-title.link:hover {
|
||||
cursor: pointer;
|
||||
color: var(--spectrum-link-primary-m-text-color-hover);
|
||||
}
|
||||
.spectrum-Card-subtitle {
|
||||
text-overflow: ellipsis;
|
||||
|
@ -91,14 +105,6 @@
|
|||
word-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
a {
|
||||
transition: color 130ms ease-in-out;
|
||||
color: var(--spectrum-alias-text-color);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--spectrum-link-primary-m-text-color-hover);
|
||||
}
|
||||
|
||||
.horizontal .spectrum-Card-coverPhoto {
|
||||
flex: 0 0 160px;
|
||||
height: auto;
|
||||
|
@ -114,6 +120,19 @@
|
|||
.spectrum-Card-footer {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: -8px;
|
||||
margin-bottom: var(
|
||||
--spectrum-card-body-padding-bottom,
|
||||
var(--spectrum-global-dimension-size-300)
|
||||
);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-bottom: var(--spectrum-global-dimension-size-300);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,273 @@
|
|||
<script>
|
||||
import { onMount, getContext } from "svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { Heading } from "@budibase/bbui"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
||||
export let title
|
||||
export let dataSource
|
||||
export let searchColumns
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
export let paginate
|
||||
export let limit
|
||||
export let showTitleButton
|
||||
export let titleButtonText
|
||||
export let titleButtonURL
|
||||
export let titleButtonPeek
|
||||
export let cardTitle
|
||||
export let cardSubtitle
|
||||
export let cardDescription
|
||||
export let cardImageURL
|
||||
export let linkCardTitle
|
||||
export let cardURL
|
||||
export let cardPeek
|
||||
export let cardHorizontal
|
||||
export let showCardButton
|
||||
export let cardButtonText
|
||||
export let cardButtonOnClick
|
||||
export let linkColumn
|
||||
|
||||
const { API, styleable } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const schemaComponentMap = {
|
||||
string: "stringfield",
|
||||
options: "optionsfield",
|
||||
number: "numberfield",
|
||||
datetime: "datetimefield",
|
||||
boolean: "booleanfield",
|
||||
}
|
||||
|
||||
let formId
|
||||
let dataProviderId
|
||||
let repeaterId
|
||||
let schema
|
||||
|
||||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
||||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
||||
$: cardWidth = cardHorizontal ? 420 : 300
|
||||
$: fullCardURL = buildFullCardUrl(
|
||||
linkCardTitle,
|
||||
cardURL,
|
||||
repeaterId,
|
||||
linkColumn
|
||||
)
|
||||
$: titleButtonAction = [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
peek: titleButtonPeek,
|
||||
url: titleButtonURL,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Enrich the default filter with the specified search fields
|
||||
const enrichFilter = (filter, columns, formId) => {
|
||||
let enrichedFilter = [...(filter || [])]
|
||||
columns?.forEach(column => {
|
||||
enrichedFilter.push({
|
||||
field: column.name,
|
||||
operator: column.type === "string" ? "string" : "equal",
|
||||
type: "string",
|
||||
valueType: "Binding",
|
||||
value: `{{ [${formId}].[${column.name}] }}`,
|
||||
})
|
||||
})
|
||||
return enrichedFilter
|
||||
}
|
||||
|
||||
// Determine data types for search fields and only use those that are valid
|
||||
const enrichSearchColumns = (searchColumns, schema) => {
|
||||
let enrichedColumns = []
|
||||
searchColumns?.forEach(column => {
|
||||
const schemaType = schema?.[column]?.type
|
||||
const componentType = schemaComponentMap[schemaType]
|
||||
if (componentType) {
|
||||
enrichedColumns.push({
|
||||
name: column,
|
||||
componentType,
|
||||
type: schemaType,
|
||||
})
|
||||
}
|
||||
})
|
||||
return enrichedColumns.slice(0, 3)
|
||||
}
|
||||
|
||||
// Builds a full details page URL for the card title
|
||||
const buildFullCardUrl = (link, url, repeaterId, linkColumn) => {
|
||||
if (!link || !url || !repeaterId) {
|
||||
return null
|
||||
}
|
||||
const col = linkColumn || "_id"
|
||||
const split = url.split("/:")
|
||||
return `${split[0]}/{{ ${safe(repeaterId)}.${safe(col)} }}`
|
||||
}
|
||||
|
||||
// Load the datasource schema on mount so we can determine column types
|
||||
onMount(async () => {
|
||||
if (dataSource) {
|
||||
schema = await API.fetchDatasourceSchema(dataSource)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<div class="card-list" use:styleable={$component.styles}>
|
||||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}>
|
||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||
<div class="header" class:mobile={$context.device.mobile}>
|
||||
<div class="title">
|
||||
<Heading>{title || ""}</Heading>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{#if enrichedSearchColumns?.length}
|
||||
<div
|
||||
class="search"
|
||||
style="--cols:{enrichedSearchColumns?.length}"
|
||||
>
|
||||
{#each enrichedSearchColumns as column}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
field: column.name,
|
||||
placeholder: column.name,
|
||||
text: column.name,
|
||||
autoWidth: true,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTitleButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
onClick: titleButtonAction,
|
||||
text: titleButtonText,
|
||||
type: "cta",
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
bind:id={dataProviderId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter: enrichedFilter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
paginate,
|
||||
limit,
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
bind:id={repeaterId}
|
||||
context="repeater"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
gap: "M",
|
||||
noRowsMessage: "No rows found",
|
||||
}}
|
||||
styles={{
|
||||
display: "grid",
|
||||
"grid-template-columns": `repeat(auto-fill, minmax(${cardWidth}px, 1fr))`,
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="spectrumcard"
|
||||
props={{
|
||||
title: cardTitle,
|
||||
subtitle: cardSubtitle,
|
||||
description: cardDescription,
|
||||
imageURL: cardImageURL,
|
||||
horizontal: cardHorizontal,
|
||||
showButton: showCardButton,
|
||||
buttonText: cardButtonText,
|
||||
buttonOnClick: cardButtonOnClick,
|
||||
linkURL: fullCardURL,
|
||||
linkPeek: cardPeek,
|
||||
}}
|
||||
styles={{
|
||||
width: "auto",
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
}
|
||||
.title :global(.spectrum-Heading) {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 0 1 auto;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
|
||||
}
|
||||
.search :global(.spectrum-InputGroup) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
.mobile {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.mobile .controls {
|
||||
flex-direction: column-reverse;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.mobile .search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,61 @@
|
|||
<script>
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import Placeholder from "components/app/Placeholder.svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
||||
export let dataSource
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
export let limit
|
||||
export let paginate
|
||||
export let noRowsMessage
|
||||
export let direction
|
||||
export let hAlign
|
||||
export let vAlign
|
||||
export let gap
|
||||
|
||||
let providerId
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<div use:styleable={$component.styles}>
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
context="provider"
|
||||
bind:id={providerId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
limit,
|
||||
paginate,
|
||||
}}
|
||||
>
|
||||
{#if $component.empty}
|
||||
<Placeholder text={$component.name} />
|
||||
{:else}
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
context="repeater"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(providerId)} }}`,
|
||||
noRowsMessage,
|
||||
direction,
|
||||
hAlign,
|
||||
vAlign,
|
||||
gap,
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
</div>
|
||||
</Block>
|
|
@ -0,0 +1,230 @@
|
|||
<script>
|
||||
import { onMount, getContext } from "svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { Heading } from "@budibase/bbui"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
||||
export let title
|
||||
export let dataSource
|
||||
export let searchColumns
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
export let paginate
|
||||
export let tableColumns
|
||||
export let showAutoColumns
|
||||
export let rowCount
|
||||
export let quiet
|
||||
export let size
|
||||
export let linkRows
|
||||
export let linkURL
|
||||
export let linkColumn
|
||||
export let linkPeek
|
||||
export let showTitleButton
|
||||
export let titleButtonText
|
||||
export let titleButtonURL
|
||||
export let titleButtonPeek
|
||||
|
||||
const { API, styleable } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const schemaComponentMap = {
|
||||
string: "stringfield",
|
||||
options: "optionsfield",
|
||||
number: "numberfield",
|
||||
datetime: "datetimefield",
|
||||
boolean: "booleanfield",
|
||||
}
|
||||
|
||||
let formId
|
||||
let dataProviderId
|
||||
let schema
|
||||
|
||||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
||||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
||||
$: titleButtonAction = [
|
||||
{
|
||||
"##eventHandlerType": "Navigate To",
|
||||
parameters: {
|
||||
peek: titleButtonPeek,
|
||||
url: titleButtonURL,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// Enrich the default filter with the specified search fields
|
||||
const enrichFilter = (filter, columns, formId) => {
|
||||
let enrichedFilter = [...(filter || [])]
|
||||
columns?.forEach(column => {
|
||||
enrichedFilter.push({
|
||||
field: column.name,
|
||||
operator: column.type === "string" ? "string" : "equal",
|
||||
type: "string",
|
||||
valueType: "Binding",
|
||||
value: `{{ ${safe(formId)}.${safe(column.name)} }}`,
|
||||
})
|
||||
})
|
||||
return enrichedFilter
|
||||
}
|
||||
|
||||
// Determine data types for search fields and only use those that are valid
|
||||
const enrichSearchColumns = (searchColumns, schema) => {
|
||||
let enrichedColumns = []
|
||||
searchColumns?.forEach(column => {
|
||||
const schemaType = schema?.[column]?.type
|
||||
const componentType = schemaComponentMap[schemaType]
|
||||
if (componentType) {
|
||||
enrichedColumns.push({
|
||||
name: column,
|
||||
componentType,
|
||||
type: schemaType,
|
||||
})
|
||||
}
|
||||
})
|
||||
return enrichedColumns.slice(0, 3)
|
||||
}
|
||||
|
||||
// Load the datasource schema on mount so we can determine column types
|
||||
onMount(async () => {
|
||||
if (dataSource) {
|
||||
schema = await API.fetchDatasourceSchema(dataSource)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<div class={size} use:styleable={$component.styles}>
|
||||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}>
|
||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||
<div class="header" class:mobile={$context.device.mobile}>
|
||||
<div class="title">
|
||||
<Heading>{title || ""}</Heading>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{#if enrichedSearchColumns?.length}
|
||||
<div
|
||||
class="search"
|
||||
style="--cols:{enrichedSearchColumns?.length}"
|
||||
>
|
||||
{#each enrichedSearchColumns as column}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
field: column.name,
|
||||
placeholder: column.name,
|
||||
text: column.name,
|
||||
autoWidth: true,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTitleButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
onClick: titleButtonAction,
|
||||
text: titleButtonText,
|
||||
type: "cta",
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
bind:id={dataProviderId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter: enrichedFilter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
paginate,
|
||||
limit: rowCount,
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="table"
|
||||
props={{
|
||||
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
|
||||
columns: tableColumns,
|
||||
showAutoColumns,
|
||||
rowCount,
|
||||
quiet,
|
||||
size,
|
||||
linkRows,
|
||||
linkURL,
|
||||
linkColumn,
|
||||
linkPeek,
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
}
|
||||
.title :global(.spectrum-Heading) {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 0 1 auto;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
|
||||
}
|
||||
.search :global(.spectrum-InputGroup) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
.mobile {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.mobile .controls {
|
||||
flex-direction: column-reverse;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.mobile .search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
export { default as tableblock } from "./TableBlock.svelte"
|
||||
export { default as cardsblock } from "./CardsBlock.svelte"
|
||||
export { default as repeaterblock } from "./RepeaterBlock.svelte"
|
|
@ -34,25 +34,34 @@
|
|||
return closestContext || {}
|
||||
}
|
||||
|
||||
// Fetches the form schema from this form's dataSource, if one exists
|
||||
// Fetches the form schema from this form's dataSource
|
||||
const fetchSchema = async () => {
|
||||
if (!dataSource?.tableId) {
|
||||
if (!dataSource) {
|
||||
schema = {}
|
||||
table = null
|
||||
} else {
|
||||
table = await API.fetchTableDefinition(dataSource?.tableId)
|
||||
if (table) {
|
||||
if (dataSource?.type === "query") {
|
||||
schema = {}
|
||||
const params = table.parameters || []
|
||||
}
|
||||
|
||||
// If the datasource is a query, then we instead use a schema of the query
|
||||
// parameters rather than the output schema
|
||||
else if (
|
||||
dataSource.type === "query" &&
|
||||
dataSource._id &&
|
||||
actionType === "Create"
|
||||
) {
|
||||
const query = await API.fetchQueryDefinition(dataSource._id)
|
||||
let paramSchema = {}
|
||||
const params = query.parameters || []
|
||||
params.forEach(param => {
|
||||
schema[param.name] = { ...param, type: "string" }
|
||||
paramSchema[param.name] = { ...param, type: "string" }
|
||||
})
|
||||
} else {
|
||||
schema = table.schema || {}
|
||||
}
|
||||
schema = paramSchema
|
||||
}
|
||||
|
||||
// For all other cases, just grab the normal schema
|
||||
else {
|
||||
const dataSourceSchema = await API.fetchDatasourceSchema(dataSource)
|
||||
schema = dataSourceSchema || {}
|
||||
}
|
||||
|
||||
loaded = true
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { CoreSelect, CoreMultiselect } from "@budibase/bbui"
|
||||
import { getContext } from "svelte"
|
||||
import Field from "./Field.svelte"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
|
||||
const { API } = getContext("sdk")
|
||||
|
||||
|
@ -68,7 +69,7 @@
|
|||
{field}
|
||||
{disabled}
|
||||
{validation}
|
||||
type="link"
|
||||
type={FieldTypes.LINK}
|
||||
bind:fieldState
|
||||
bind:fieldApi
|
||||
bind:fieldSchema
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import flatpickr from "flatpickr"
|
||||
import { FieldTypes } from "../../../constants"
|
||||
|
||||
/**
|
||||
* Creates a validation function from a combination of schema-level constraints
|
||||
|
@ -154,7 +155,7 @@ const parseType = (value, type) => {
|
|||
}
|
||||
|
||||
// Parse as string
|
||||
if (type === "string") {
|
||||
if (type === FieldTypes.STRING) {
|
||||
if (typeof value === "string" || Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
@ -165,7 +166,7 @@ const parseType = (value, type) => {
|
|||
}
|
||||
|
||||
// Parse as number
|
||||
if (type === "number") {
|
||||
if (type === FieldTypes.NUMBER) {
|
||||
if (isNaN(value)) {
|
||||
return null
|
||||
}
|
||||
|
@ -173,7 +174,7 @@ const parseType = (value, type) => {
|
|||
}
|
||||
|
||||
// Parse as date
|
||||
if (type === "datetime") {
|
||||
if (type === FieldTypes.DATETIME) {
|
||||
if (value instanceof Date) {
|
||||
return value.getTime()
|
||||
}
|
||||
|
@ -182,7 +183,7 @@ const parseType = (value, type) => {
|
|||
}
|
||||
|
||||
// Parse as boolean
|
||||
if (type === "boolean") {
|
||||
if (type === FieldTypes.BOOLEAN) {
|
||||
if (typeof value === "string") {
|
||||
return value.toLowerCase() === "true"
|
||||
}
|
||||
|
@ -190,7 +191,7 @@ const parseType = (value, type) => {
|
|||
}
|
||||
|
||||
// Parse attachments, treating no elements as null
|
||||
if (type === "attachment") {
|
||||
if (type === FieldTypes.ATTACHMENT) {
|
||||
if (!Array.isArray(value) || !value.length) {
|
||||
return null
|
||||
}
|
||||
|
@ -198,14 +199,14 @@ const parseType = (value, type) => {
|
|||
}
|
||||
|
||||
// Parse links, treating no elements as null
|
||||
if (type === "link") {
|
||||
if (type === FieldTypes.LINK) {
|
||||
if (!Array.isArray(value) || !value.length) {
|
||||
return null
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
if (type === "array") {
|
||||
if (type === FieldTypes.ARRAY) {
|
||||
if (!Array.isArray(value) || !value.length) {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export { default as spectrumcard } from "./SpectrumCard.svelte"
|
|||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./table"
|
||||
export * from "./blocks"
|
||||
|
||||
// Deprecated component left for compatibility in old apps
|
||||
export { default as navigation } from "./deprecated/Navigation.svelte"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { Table } from "@budibase/bbui"
|
||||
import SlotRenderer from "./SlotRenderer.svelte"
|
||||
import { UnsortableTypes } from "../../../constants"
|
||||
|
||||
export let dataProvider
|
||||
export let columns
|
||||
|
@ -9,9 +10,17 @@
|
|||
export let rowCount
|
||||
export let quiet
|
||||
export let size
|
||||
export let linkRows
|
||||
export let linkURL
|
||||
export let linkColumn
|
||||
export let linkPeek
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
|
||||
const setSorting = getAction(
|
||||
dataProvider?.id,
|
||||
ActionTypes.SetDataProviderSorting
|
||||
)
|
||||
const customColumnKey = `custom-${Math.random()}`
|
||||
const customRenderers = [
|
||||
{
|
||||
|
@ -65,11 +74,35 @@
|
|||
divider: true,
|
||||
}
|
||||
}
|
||||
|
||||
fields.forEach(field => {
|
||||
newSchema[field] = schema[field]
|
||||
if (schema[field] && UnsortableTypes.indexOf(schema[field].type) !== -1) {
|
||||
newSchema[field].sortable = false
|
||||
}
|
||||
})
|
||||
return newSchema
|
||||
}
|
||||
|
||||
const onSort = e => {
|
||||
setSorting({
|
||||
column: e.detail.column,
|
||||
order: e.detail.order,
|
||||
})
|
||||
}
|
||||
|
||||
const onClick = e => {
|
||||
if (!linkRows || !linkURL) {
|
||||
return
|
||||
}
|
||||
const col = linkColumn || "_id"
|
||||
const id = e.detail?.[col]
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
const split = linkURL.split("/:")
|
||||
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles} class={size}>
|
||||
|
@ -84,6 +117,9 @@
|
|||
allowEditRows={false}
|
||||
allowEditColumns={false}
|
||||
showAutoColumns={true}
|
||||
disableSorting
|
||||
on:sort={onSort}
|
||||
on:click={onClick}
|
||||
>
|
||||
<slot />
|
||||
</Table>
|
||||
|
|
|
@ -8,6 +8,12 @@
|
|||
import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
const MessageTypes = {
|
||||
NOTIFICATION: "notification",
|
||||
CLOSE_SCREEN_MODAL: "close-screen-modal",
|
||||
INVALIDATE_DATASOURCE: "invalidate-datasource",
|
||||
}
|
||||
|
||||
let iframe
|
||||
let listenersAttached = false
|
||||
|
||||
|
@ -21,32 +27,33 @@
|
|||
notificationStore.actions.send(message, type, icon)
|
||||
}
|
||||
|
||||
function receiveMessage(message) {
|
||||
const handlers = {
|
||||
[MessageTypes.NOTIFICATION]: () => {
|
||||
proxyNotification(message.data)
|
||||
},
|
||||
[MessageTypes.CLOSE_SCREEN_MODAL]: peekStore.actions.hidePeek,
|
||||
[MessageTypes.INVALIDATE_DATASOURCE]: () => {
|
||||
invalidateDataSource(message.data)
|
||||
},
|
||||
}
|
||||
|
||||
const messageHandler = handlers[message.data.type]
|
||||
if (messageHandler) {
|
||||
messageHandler(message)
|
||||
} else {
|
||||
console.warning("Unknown event type", message?.data?.type)
|
||||
}
|
||||
}
|
||||
|
||||
const attachListeners = () => {
|
||||
// Mirror datasource invalidation to keep the parent window up to date
|
||||
iframe.contentWindow.addEventListener(
|
||||
"invalidate-datasource",
|
||||
invalidateDataSource
|
||||
)
|
||||
// Listen for a close event to close the screen peek
|
||||
iframe.contentWindow.addEventListener(
|
||||
"close-screen-modal",
|
||||
peekStore.actions.hidePeek
|
||||
)
|
||||
// Proxy notifications back to the parent window instead of iframe
|
||||
iframe.contentWindow.addEventListener("notification", proxyNotification)
|
||||
window.addEventListener("message", receiveMessage)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
peekStore.actions.hidePeek()
|
||||
iframe.contentWindow.removeEventListener(
|
||||
"invalidate-datasource",
|
||||
invalidateDataSource
|
||||
)
|
||||
iframe.contentWindow.removeEventListener(
|
||||
"close-screen-modal",
|
||||
peekStore.actions.hidePeek
|
||||
)
|
||||
iframe.contentWindow.removeEventListener("notification", proxyNotification)
|
||||
window.removeEventListener("message", receiveMessage)
|
||||
}
|
||||
|
||||
const handleFullscreen = () => {
|
||||
|
|
|
@ -147,7 +147,7 @@
|
|||
return
|
||||
}
|
||||
|
||||
const element = e.target.closest(".component")
|
||||
const element = e.target.closest(".component:not(.block)")
|
||||
if (
|
||||
element &&
|
||||
element.classList.contains("droppable") &&
|
||||
|
|
|
@ -17,7 +17,19 @@
|
|||
|
||||
$: definition = $builderStore.selectedComponentDefinition
|
||||
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
|
||||
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
||||
$: settings = getBarSettings(definition)
|
||||
|
||||
const getBarSettings = definition => {
|
||||
let allSettings = []
|
||||
definition?.settings?.forEach(setting => {
|
||||
if (setting.section) {
|
||||
allSettings = allSettings.concat(setting.settings || [])
|
||||
} else {
|
||||
allSettings.push(setting)
|
||||
}
|
||||
})
|
||||
return allSettings.filter(setting => setting.showInBar)
|
||||
}
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!showBar) {
|
||||
|
|
|
@ -2,10 +2,31 @@ export const TableNames = {
|
|||
USERS: "ta_users",
|
||||
}
|
||||
|
||||
export const FieldTypes = {
|
||||
STRING: "string",
|
||||
LONGFORM: "longform",
|
||||
OPTIONS: "options",
|
||||
NUMBER: "number",
|
||||
BOOLEAN: "boolean",
|
||||
ARRAY: "array",
|
||||
DATETIME: "datetime",
|
||||
ATTACHMENT: "attachment",
|
||||
LINK: "link",
|
||||
FORMULA: "formula",
|
||||
}
|
||||
|
||||
export const UnsortableTypes = [
|
||||
FieldTypes.FORMULA,
|
||||
FieldTypes.ATTACHMENT,
|
||||
FieldTypes.ARRAY,
|
||||
FieldTypes.LINK,
|
||||
]
|
||||
|
||||
export const ActionTypes = {
|
||||
ValidateForm: "ValidateForm",
|
||||
RefreshDatasource: "RefreshDatasource",
|
||||
SetDataProviderQuery: "SetDataProviderQuery",
|
||||
SetDataProviderSorting: "SetDataProviderSorting",
|
||||
ClearForm: "ClearForm",
|
||||
ChangeFormStep: "ChangeFormStep",
|
||||
}
|
||||
|
|
|
@ -4,11 +4,7 @@ import { findComponentById, findComponentPathById } from "../utils/components"
|
|||
import { pingEndUser } from "../api"
|
||||
|
||||
const dispatchEvent = (type, data = {}) => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("bb-event", {
|
||||
detail: { type, data },
|
||||
})
|
||||
)
|
||||
window.parent.postMessage({ type, data })
|
||||
}
|
||||
|
||||
const createBuilderStore = () => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { writable, derived } from "svelte/store"
|
||||
import { hashString } from "../utils/helpers"
|
||||
|
||||
export const createContextStore = oldContext => {
|
||||
const newContext = writable({})
|
||||
|
@ -9,7 +10,7 @@ export const createContextStore = oldContext => {
|
|||
for (let i = 0; i < $contexts.length - 1; i++) {
|
||||
key += $contexts[i].key
|
||||
}
|
||||
key += JSON.stringify($contexts[$contexts.length - 1])
|
||||
key = hashString(key + JSON.stringify($contexts[$contexts.length - 1]))
|
||||
|
||||
// Reduce global state
|
||||
const reducer = (total, context) => ({ ...total, ...context })
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { fetchTableDefinition } from "../api"
|
||||
import { FieldTypes } from "../constants"
|
||||
|
||||
export const createDataSourceStore = () => {
|
||||
const store = writable([])
|
||||
|
@ -20,7 +21,7 @@ export const createDataSourceStore = () => {
|
|||
|
||||
// Only one side of the relationship is required as a trigger, as it will
|
||||
// automatically invalidate related table IDs
|
||||
else if (dataSource.type === "link") {
|
||||
else if (dataSource.type === FieldTypes.LINK) {
|
||||
dataSourceId = dataSource.tableId || dataSource.rowTableId
|
||||
}
|
||||
|
||||
|
@ -59,11 +60,10 @@ export const createDataSourceStore = () => {
|
|||
|
||||
// Emit this as a window event, so parent screens which are iframing us in
|
||||
// can also invalidate the same datasource
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("invalidate-datasource", {
|
||||
window.parent.postMessage({
|
||||
type: "close-screen-modal",
|
||||
detail: { dataSourceId },
|
||||
})
|
||||
)
|
||||
|
||||
let invalidations = [dataSourceId]
|
||||
|
||||
|
@ -73,7 +73,7 @@ export const createDataSourceStore = () => {
|
|||
if (schema) {
|
||||
Object.values(schema).forEach(fieldSchema => {
|
||||
if (
|
||||
fieldSchema.type === "link" &&
|
||||
fieldSchema.type === FieldTypes.LINK &&
|
||||
fieldSchema.tableId &&
|
||||
!fieldSchema.autocolumn
|
||||
) {
|
||||
|
|
|
@ -26,11 +26,14 @@ const createNotificationStore = () => {
|
|||
|
||||
// If peeking, pass notifications back to parent window
|
||||
if (get(routeStore).queryParams?.peek) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("notification", {
|
||||
detail: { message, type, icon },
|
||||
window.parent.postMessage({
|
||||
type: "notification",
|
||||
detail: {
|
||||
message,
|
||||
type,
|
||||
icon,
|
||||
},
|
||||
})
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue