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