forked from tsai/budibase
committed by
GitHub
280 changed files with 20399 additions and 9795 deletions
@ -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,3 +1,4 @@ |
|||||
module.exports = { |
module.exports = { |
||||
user: require("./src/cache/user"), |
user: require("./src/cache/user"), |
||||
|
app: require("./src/cache/appMetadata"), |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,85 @@ |
|||||
|
const redis = require("../redis/authRedis") |
||||
|
const { getCouch } = require("../db") |
||||
|
const { DocumentTypes } = require("../db/constants") |
||||
|
|
||||
|
const AppState = { |
||||
|
INVALID: "invalid", |
||||
|
} |
||||
|
const EXPIRY_SECONDS = 3600 |
||||
|
|
||||
|
/** |
||||
|
* The default populate app metadata function |
||||
|
*/ |
||||
|
const populateFromDB = async (appId, CouchDB = null) => { |
||||
|
if (!CouchDB) { |
||||
|
CouchDB = getCouch() |
||||
|
} |
||||
|
const db = new CouchDB(appId, { skip_setup: true }) |
||||
|
return db.get(DocumentTypes.APP_METADATA) |
||||
|
} |
||||
|
|
||||
|
const isInvalid = metadata => { |
||||
|
return !metadata || metadata.state === AppState.INVALID |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the requested app metadata by id. |
||||
|
* Use redis cache to first read the app metadata. |
||||
|
* If not present fallback to loading the app metadata directly and re-caching. |
||||
|
* @param {string} appId the id of the app to get metadata from. |
||||
|
* @param {object} CouchDB the database being passed |
||||
|
* @returns {object} the app metadata. |
||||
|
*/ |
||||
|
exports.getAppMetadata = async (appId, CouchDB = null) => { |
||||
|
const client = await redis.getAppClient() |
||||
|
// try cache
|
||||
|
let metadata = await client.get(appId) |
||||
|
if (!metadata) { |
||||
|
let expiry = EXPIRY_SECONDS |
||||
|
try { |
||||
|
metadata = await populateFromDB(appId, CouchDB) |
||||
|
} catch (err) { |
||||
|
// app DB left around, but no metadata, it is invalid
|
||||
|
if (err && err.status === 404) { |
||||
|
metadata = { state: AppState.INVALID } |
||||
|
// don't expire the reference to an invalid app, it'll only be
|
||||
|
// updated if a metadata doc actually gets stored (app is remade/reverted)
|
||||
|
expiry = undefined |
||||
|
} else { |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
// needed for cypress/some scenarios where the caching happens
|
||||
|
// so quickly the requests can get slightly out of sync
|
||||
|
// might store its invalid just before it stores its valid
|
||||
|
if (isInvalid(metadata)) { |
||||
|
const temp = await client.get(appId) |
||||
|
if (temp) { |
||||
|
metadata = temp |
||||
|
} |
||||
|
} |
||||
|
await client.store(appId, metadata, expiry) |
||||
|
} |
||||
|
// we've stored in the cache an object to tell us that it is currently invalid
|
||||
|
if (isInvalid(metadata)) { |
||||
|
throw { status: 404, message: "No app metadata found" } |
||||
|
} |
||||
|
return metadata |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Invalidate/reset the cached metadata when a change occurs in the db. |
||||
|
* @param appId {string} the cache key to bust/update. |
||||
|
* @param newMetadata {object|undefined} optional - can simply provide the new metadata to update with. |
||||
|
* @return {Promise<void>} will respond with success when cache is updated. |
||||
|
*/ |
||||
|
exports.invalidateAppMetadata = async (appId, newMetadata = null) => { |
||||
|
if (!appId) { |
||||
|
throw "Cannot invalidate if no app ID provided." |
||||
|
} |
||||
|
const client = await redis.getAppClient() |
||||
|
await client.delete(appId) |
||||
|
if (newMetadata) { |
||||
|
await client.store(appId, newMetadata, EXPIRY_SECONDS) |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,56 @@ |
|||||
|
<script> |
||||
|
import "@spectrum-css/inlinealert/dist/index-vars.css" |
||||
|
import Button from "../Button/Button.svelte" |
||||
|
|
||||
|
export let type = "info" |
||||
|
export let header = "" |
||||
|
export let message = "" |
||||
|
export let onConfirm = undefined |
||||
|
|
||||
|
$: icon = selectIcon(type) |
||||
|
// if newlines used, convert them to different elements |
||||
|
$: split = message.split("\n") |
||||
|
|
||||
|
function selectIcon(alertType) { |
||||
|
switch (alertType) { |
||||
|
case "error": |
||||
|
case "negative": |
||||
|
return "Alert" |
||||
|
case "success": |
||||
|
return "CheckmarkCircle" |
||||
|
case "help": |
||||
|
return "Help" |
||||
|
default: |
||||
|
return "Info" |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="spectrum-InLineAlert spectrum-InLineAlert--{type}"> |
||||
|
<svg |
||||
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-InLineAlert-icon" |
||||
|
focusable="false" |
||||
|
aria-hidden="true" |
||||
|
> |
||||
|
<use xlink:href="#spectrum-icon-18-{icon}" /> |
||||
|
</svg> |
||||
|
<div class="spectrum-InLineAlert-header">{header}</div> |
||||
|
{#each split as splitMsg} |
||||
|
<div class="spectrum-InLineAlert-content">{splitMsg}</div> |
||||
|
{/each} |
||||
|
{#if onConfirm} |
||||
|
<div class="spectrum-InLineAlert-footer"> |
||||
|
<Button secondary on:click={onConfirm}>OK</Button> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.spectrum-InLineAlert { |
||||
|
--spectrum-semantic-negative-border-color: #e34850; |
||||
|
--spectrum-semantic-positive-border-color: #2d9d78; |
||||
|
--spectrum-semantic-positive-icon-color: #2d9d78; |
||||
|
--spectrum-semantic-negative-icon-color: #e34850; |
||||
|
min-width: 100px; |
||||
|
} |
||||
|
</style> |
||||
@ -1,16 +1,67 @@ |
|||||
<script> |
<script> |
||||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
||||
|
import Tooltip from "../Tooltip/Tooltip.svelte" |
||||
|
import Icon from "../Icon/Icon.svelte" |
||||
|
|
||||
export let size = "M" |
export let size = "M" |
||||
|
export let tooltip = "" |
||||
|
export let showTooltip = false |
||||
</script> |
</script> |
||||
|
|
||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
{#if tooltip} |
||||
<slot /> |
<div class="container"> |
||||
</label> |
<label |
||||
|
for="" |
||||
|
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`} |
||||
|
> |
||||
|
<slot /> |
||||
|
</label> |
||||
|
<div class="icon-container"> |
||||
|
<div |
||||
|
class="icon" |
||||
|
on:mouseover={() => (showTooltip = true)} |
||||
|
on:mouseleave={() => (showTooltip = false)} |
||||
|
> |
||||
|
<Icon name="InfoOutline" size="S" disabled={true} /> |
||||
|
</div> |
||||
|
{#if showTooltip} |
||||
|
<div class="tooltip"> |
||||
|
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</div> |
||||
|
{:else} |
||||
|
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
||||
|
<slot /> |
||||
|
</label> |
||||
|
{/if} |
||||
|
|
||||
<style> |
<style> |
||||
label { |
label { |
||||
padding: 0; |
padding: 0; |
||||
white-space: nowrap; |
white-space: nowrap; |
||||
} |
} |
||||
|
.container { |
||||
|
display: flex; |
||||
|
} |
||||
|
.icon-container { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: 1px; |
||||
|
margin-left: 5px; |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
.tooltip { |
||||
|
position: absolute; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
top: 15px; |
||||
|
z-index: 1; |
||||
|
width: 160px; |
||||
|
} |
||||
|
.icon { |
||||
|
transform: scale(0.75); |
||||
|
} |
||||
</style> |
</style> |
||||
|
|||||
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
@ -1,41 +1,44 @@ |
|||||
context("Add Multi-Option Datatype", () => { |
context("Add Multi-Option Datatype", () => { |
||||
before(() => { |
before(() => { |
||||
cy.login() |
cy.login() |
||||
cy.createTestApp() |
cy.createTestApp() |
||||
}) |
}) |
||||
|
|
||||
it("should create a new table, with data", () => { |
it("should create a new table, with data", () => { |
||||
cy.createTable("Multi Data") |
cy.createTable("Multi Data") |
||||
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5") |
cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5") |
||||
cy.addRowMultiValue(["1", "2", "3", "4", "5"]) |
cy.addRowMultiValue(["1", "2", "3", "4", "5"]) |
||||
}) |
}) |
||||
|
|
||||
it ("should add form with multi select picker, containing 5 options", () => { |
it("should add form with multi select picker, containing 5 options", () => { |
||||
cy.navigateToFrontend() |
cy.navigateToFrontend() |
||||
cy.wait(500) |
cy.wait(500) |
||||
// Add data provider
|
// Add data provider
|
||||
cy.get(`[data-cy="category-Data Provider"]`).click() |
cy.get(`[data-cy="category-Data"]`).click() |
||||
cy.get('[data-cy="dataSource-prop-control"]').click() |
cy.get(`[data-cy="component-Data Provider"]`).click() |
||||
cy.get(".dropdown").contains("Multi Data").click() |
cy.get('[data-cy="dataSource-prop-control"]').click() |
||||
cy.wait(500) |
cy.get(".dropdown").contains("Multi Data").click() |
||||
// Add Form with schema to match table
|
cy.wait(500) |
||||
cy.addComponent("Form", "Form") |
// Add Form with schema to match table
|
||||
cy.get('[data-cy="dataSource-prop-control"').click() |
cy.addComponent("Form", "Form") |
||||
cy.get(".dropdown").contains("Multi Data").click() |
cy.get('[data-cy="dataSource-prop-control"').click() |
||||
cy.wait(500) |
cy.get(".dropdown").contains("Multi Data").click() |
||||
// Add multi-select picker to form
|
cy.wait(500) |
||||
cy.addComponent("Form", "Multi-select Picker").then((componentId) => { |
// Add multi-select picker to form
|
||||
cy.get('[data-cy="field-prop-control"]').type("Test Data").type('{enter}') |
cy.addComponent("Form", "Multi-select Picker").then(componentId => { |
||||
cy.wait(1000) |
cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}") |
||||
cy.getComponent(componentId).contains("Choose some options").click() |
cy.wait(1000) |
||||
// Check picker has 5 items
|
cy.getComponent(componentId).contains("Choose some options").click() |
||||
cy.getComponent(componentId).find('li').should('have.length', 5) |
// Check picker has 5 items
|
||||
// Select all items
|
cy.getComponent(componentId).find("li").should("have.length", 5) |
||||
for (let i = 1; i < 6; i++) { |
// Select all items
|
||||
cy.getComponent(componentId).find('li').contains(i).click() |
for (let i = 1; i < 6; i++) { |
||||
} |
cy.getComponent(componentId).find("li").contains(i).click() |
||||
// Check items have been selected
|
} |
||||
cy.getComponent(componentId).find('.spectrum-Picker-label').contains("(5)") |
// Check items have been selected
|
||||
}) |
cy.getComponent(componentId) |
||||
|
.find(".spectrum-Picker-label") |
||||
|
.contains("(5)") |
||||
}) |
}) |
||||
|
}) |
||||
}) |
}) |
||||
|
|||||
@ -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} |
||||
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,16 @@ |
|||||
|
<script> |
||||
|
export let width = "18" |
||||
|
export let height = "18" |
||||
|
|
||||
|
import OracleLogo from "assets/oracle.png" |
||||
|
</script> |
||||
|
|
||||
|
<div class> |
||||
|
<img {height} {width} src={OracleLogo} alt="oracle logo" /> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
img { |
||||
|
padding-top: 1px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,30 @@ |
|||||
|
<script> |
||||
|
import BindingPanel from "./BindingPanel.svelte" |
||||
|
|
||||
|
export let bindings = [] |
||||
|
export let valid |
||||
|
export let value = "" |
||||
|
export let allowJS = false |
||||
|
|
||||
|
$: enrichedBindings = enrichBindings(bindings) |
||||
|
|
||||
|
// Ensure bindings have the correct categories |
||||
|
const enrichBindings = bindings => { |
||||
|
if (!bindings?.length) { |
||||
|
return bindings |
||||
|
} |
||||
|
return bindings?.map(binding => ({ |
||||
|
...binding, |
||||
|
category: "Bindable Values", |
||||
|
type: null, |
||||
|
})) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<BindingPanel |
||||
|
bind:valid |
||||
|
bindings={enrichedBindings} |
||||
|
{value} |
||||
|
{allowJS} |
||||
|
on:change |
||||
|
/> |
||||
@ -1,209 +1,27 @@ |
|||||
<script> |
<script> |
||||
import groupBy from "lodash/fp/groupBy" |
import BindingPanel from "./BindingPanel.svelte" |
||||
import { Search, TextArea, DrawerContent } from "@budibase/bbui" |
|
||||
import { createEventDispatcher } from "svelte" |
|
||||
import { isValid } from "@budibase/string-templates" |
|
||||
import { handlebarsCompletions } from "constants/completions" |
|
||||
import { readableToRuntimeBinding } from "builderStore/dataBinding" |
|
||||
import { addHBSBinding } from "./utils" |
|
||||
|
|
||||
const dispatch = createEventDispatcher() |
export let bindings = [] |
||||
|
export let valid |
||||
export let bindableProperties = [] |
|
||||
export let valid = true |
|
||||
export let value = "" |
export let value = "" |
||||
|
export let allowJS = false |
||||
|
|
||||
let helpers = handlebarsCompletions() |
$: enrichedBindings = enrichBindings(bindings) |
||||
let getCaretPosition |
|
||||
let search = "" |
|
||||
|
|
||||
$: categories = Object.entries(groupBy("category", bindableProperties)) |
|
||||
$: valid = isValid(readableToRuntimeBinding(bindableProperties, value)) |
|
||||
$: dispatch("change", value) |
|
||||
$: searchRgx = new RegExp(search, "ig") |
|
||||
$: filteredCategories = categories.map(([categoryName, bindings]) => { |
|
||||
const filteredBindings = bindings.filter(binding => { |
|
||||
return binding.label.match(searchRgx) |
|
||||
}) |
|
||||
return [categoryName, filteredBindings] |
|
||||
}) |
|
||||
$: filteredHelpers = helpers?.filter(helper => { |
|
||||
return helper.label.match(searchRgx) || helper.description.match(searchRgx) |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<DrawerContent> |
|
||||
<svelte:fragment slot="sidebar"> |
|
||||
<div class="container"> |
|
||||
<section> |
|
||||
<div class="heading">Search</div> |
|
||||
<Search placeholder="Search" bind:value={search} /> |
|
||||
</section> |
|
||||
{#each filteredCategories as [categoryName, bindings]} |
|
||||
{#if bindings.length} |
|
||||
<section> |
|
||||
<div class="heading">{categoryName}</div> |
|
||||
<ul> |
|
||||
{#each bindings as binding} |
|
||||
<li |
|
||||
on:click={() => { |
|
||||
value = addHBSBinding(value, getCaretPosition(), binding) |
|
||||
}} |
|
||||
> |
|
||||
<span class="binding__label">{binding.label}</span> |
|
||||
<span class="binding__type">{binding.type}</span> |
|
||||
{#if binding.description} |
|
||||
<br /> |
|
||||
<div class="binding__description"> |
|
||||
{binding.description || ""} |
|
||||
</div> |
|
||||
{/if} |
|
||||
</li> |
|
||||
{/each} |
|
||||
</ul> |
|
||||
</section> |
|
||||
{/if} |
|
||||
{/each} |
|
||||
{#if filteredHelpers?.length} |
|
||||
<section> |
|
||||
<div class="heading">Helpers</div> |
|
||||
<ul> |
|
||||
{#each filteredHelpers as helper} |
|
||||
<li |
|
||||
on:click={() => { |
|
||||
value = addHBSBinding(value, getCaretPosition(), helper.text) |
|
||||
}} |
|
||||
> |
|
||||
<div class="helper"> |
|
||||
<div class="helper__name">{helper.displayText}</div> |
|
||||
<div class="helper__description"> |
|
||||
{@html helper.description} |
|
||||
</div> |
|
||||
<pre class="helper__example">{helper.example || ''}</pre> |
|
||||
</div> |
|
||||
</li> |
|
||||
{/each} |
|
||||
</ul> |
|
||||
</section> |
|
||||
{/if} |
|
||||
</div> |
|
||||
</svelte:fragment> |
|
||||
<div class="main"> |
|
||||
<TextArea |
|
||||
bind:getCaretPosition |
|
||||
bind:value |
|
||||
placeholder="Add text, or click the objects on the left to add them to the textbox." |
|
||||
/> |
|
||||
{#if !valid} |
|
||||
<p class="syntax-error"> |
|
||||
Current Handlebars syntax is invalid, please check the guide |
|
||||
<a href="https://handlebarsjs.com/guide/">here</a> |
|
||||
for more details. |
|
||||
</p> |
|
||||
{/if} |
|
||||
</div> |
|
||||
</DrawerContent> |
|
||||
|
|
||||
<style> |
|
||||
.main :global(textarea) { |
|
||||
min-height: 150px !important; |
|
||||
} |
|
||||
|
|
||||
.container { |
|
||||
margin: calc(-1 * var(--spacing-xl)); |
|
||||
} |
|
||||
.heading { |
|
||||
font-size: var(--font-size-s); |
|
||||
font-weight: 600; |
|
||||
text-transform: uppercase; |
|
||||
color: var(--spectrum-global-color-gray-600); |
|
||||
padding: var(--spacing-xl) 0 var(--spacing-m) 0; |
|
||||
} |
|
||||
|
|
||||
section { |
|
||||
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl); |
|
||||
} |
|
||||
section:not(:first-child) { |
|
||||
border-top: var(--border-light); |
|
||||
} |
|
||||
ul { |
|
||||
list-style: none; |
|
||||
padding: 0; |
|
||||
margin: 0; |
|
||||
} |
|
||||
|
|
||||
li { |
|
||||
font-size: var(--font-size-s); |
|
||||
padding: var(--spacing-m); |
|
||||
border-radius: 4px; |
|
||||
border: var(--border-light); |
|
||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out, |
|
||||
border-color 130ms ease-in-out; |
|
||||
} |
|
||||
li:not(:last-of-type) { |
|
||||
margin-bottom: var(--spacing-s); |
|
||||
} |
|
||||
li :global(*) { |
|
||||
transition: color 130ms ease-in-out; |
|
||||
} |
|
||||
li:hover { |
|
||||
color: var(--spectrum-global-color-gray-900); |
|
||||
background-color: var(--spectrum-global-color-gray-50); |
|
||||
border-color: var(--spectrum-global-color-gray-500); |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
li:hover :global(*) { |
|
||||
color: var(--spectrum-global-color-gray-900) !important; |
|
||||
} |
|
||||
|
|
||||
.helper { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: flex-start; |
|
||||
gap: var(--spacing-xs); |
|
||||
} |
|
||||
.helper__name { |
|
||||
font-weight: bold; |
|
||||
} |
|
||||
.helper__description, |
|
||||
.helper__description :global(*) { |
|
||||
color: var(--spectrum-global-color-gray-700); |
|
||||
} |
|
||||
.helper__example { |
|
||||
white-space: normal; |
|
||||
margin: 0.5rem 0 0 0; |
|
||||
font-weight: 700; |
|
||||
} |
|
||||
.helper__description :global(p) { |
|
||||
margin: 0; |
|
||||
} |
|
||||
|
|
||||
.syntax-error { |
// Ensure bindings have the correct properties |
||||
padding-top: var(--spacing-m); |
const enrichBindings = bindings => { |
||||
color: var(--red); |
return bindings?.map(binding => ({ |
||||
font-size: 12px; |
...binding, |
||||
} |
readableBinding: binding.label || binding.readableBinding, |
||||
.syntax-error a { |
runtimeBinding: binding.path || binding.runtimeBinding, |
||||
color: var(--red); |
})) |
||||
text-decoration: underline; |
|
||||
} |
} |
||||
|
</script> |
||||
|
|
||||
.binding__label { |
<BindingPanel |
||||
font-weight: 600; |
bind:valid |
||||
text-transform: capitalize; |
bindings={enrichedBindings} |
||||
} |
{value} |
||||
.binding__description { |
{allowJS} |
||||
color: var(--spectrum-global-color-gray-700); |
on:change |
||||
margin: 0.5rem 0 0 0; |
/> |
||||
white-space: normal; |
|
||||
} |
|
||||
.binding__type { |
|
||||
font-family: monospace; |
|
||||
background-color: var(--spectrum-global-color-gray-200); |
|
||||
border-radius: var(--border-radius-s); |
|
||||
padding: 2px 4px; |
|
||||
margin-left: 2px; |
|
||||
font-weight: 600; |
|
||||
} |
|
||||
</style> |
|
||||
|
|||||
@ -0,0 +1,93 @@ |
|||||
|
<script> |
||||
|
import { ModalContent, Body, Detail } from "@budibase/bbui" |
||||
|
|
||||
|
export let selectedScreens |
||||
|
export let chooseModal |
||||
|
export let save |
||||
|
let selectedNav |
||||
|
let createdScreens = [] |
||||
|
$: blankSelected = selectedScreens.length === 1 |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
title="Select navigation" |
||||
|
cancelText="Back" |
||||
|
onCancel={() => (blankSelected ? chooseModal(1) : chooseModal(0))} |
||||
|
size="M" |
||||
|
onConfirm={() => { |
||||
|
save(createdScreens) |
||||
|
}} |
||||
|
disabled={!selectedNav} |
||||
|
> |
||||
|
<Body size="S" |
||||
|
>Please select your preferred layout for the new application:</Body |
||||
|
> |
||||
|
|
||||
|
<div class="wrapper"> |
||||
|
<div |
||||
|
data-cy="left-nav" |
||||
|
on:click={() => (selectedNav = "Left")} |
||||
|
class:unselected={selectedNav && selectedNav !== "Left"} |
||||
|
> |
||||
|
<div class="box"> |
||||
|
<div class="side-nav" /> |
||||
|
</div> |
||||
|
<div><Detail>Side Nav</Detail></div> |
||||
|
</div> |
||||
|
<div |
||||
|
on:click={() => (selectedNav = "Top")} |
||||
|
class:unselected={selectedNav && selectedNav !== "Top"} |
||||
|
> |
||||
|
<div class="box"> |
||||
|
<div class="top-nav" /> |
||||
|
</div> |
||||
|
<div><Detail>Top Nav</Detail></div> |
||||
|
</div> |
||||
|
<div |
||||
|
on:click={() => (selectedNav = "None")} |
||||
|
class:unselected={selectedNav && selectedNav !== "None"} |
||||
|
> |
||||
|
<div class="box" /> |
||||
|
<div><Detail>No Nav</Detail></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</ModalContent> |
||||
|
|
||||
|
<style> |
||||
|
.side-nav { |
||||
|
float: left; |
||||
|
background: #d3d3d3 0% 0% no-repeat padding-box; |
||||
|
border-radius: 2px 0px 0px 2px; |
||||
|
height: 100%; |
||||
|
width: 10%; |
||||
|
} |
||||
|
|
||||
|
.top-nav { |
||||
|
background: #d3d3d3 0% 0% no-repeat padding-box; |
||||
|
vertical-align: top; |
||||
|
width: 100%; |
||||
|
height: 15%; |
||||
|
} |
||||
|
.box { |
||||
|
display: inline-block; |
||||
|
background: #eaeaea 0% 0% no-repeat padding-box; |
||||
|
border: 1px solid #d3d3d3; |
||||
|
border-radius: 2px; |
||||
|
opacity: 1; |
||||
|
width: 120px; |
||||
|
height: 70px; |
||||
|
margin-right: 20px; |
||||
|
} |
||||
|
|
||||
|
.wrapper { |
||||
|
display: flex; |
||||
|
padding-top: 4%; |
||||
|
list-style-type: none; |
||||
|
margin: 0; |
||||
|
padding: 0; |
||||
|
margin-right: 5%; |
||||
|
} |
||||
|
.unselected { |
||||
|
opacity: 0.3; |
||||
|
} |
||||
|
</style> |
||||
@ -1,117 +1,178 @@ |
|||||
<script> |
<script> |
||||
import { store, allScreens, selectedAccessRole } from "builderStore" |
import { store } from "builderStore" |
||||
import { tables } from "stores/backend" |
import { tables } from "stores/backend" |
||||
import { roles } from "stores/backend" |
import { |
||||
import { Input, Select, ModalContent, Toggle } from "@budibase/bbui" |
ModalContent, |
||||
|
Body, |
||||
|
Detail, |
||||
|
Layout, |
||||
|
Icon, |
||||
|
ProgressCircle, |
||||
|
} from "@budibase/bbui" |
||||
import getTemplates from "builderStore/store/screenTemplates" |
import getTemplates from "builderStore/store/screenTemplates" |
||||
import analytics, { Events } from "analytics" |
import { onDestroy } from "svelte" |
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" |
|
||||
|
import { createEventDispatcher } from "svelte" |
||||
const CONTAINER = "@budibase/standard-components/container" |
|
||||
|
export let chooseModal |
||||
let name = "" |
export let save |
||||
let routeError |
export let showProgressCircle = false |
||||
let baseComponent = CONTAINER |
|
||||
let templateIndex |
let selectedScreens = [] |
||||
let draftScreen |
|
||||
let createLink = true |
const blankScreen = "createFromScratch" |
||||
let roleId = $selectedAccessRole || "BASIC" |
const dispatch = createEventDispatcher() |
||||
|
|
||||
$: templates = getTemplates($store, $tables.list) |
function setScreens() { |
||||
$: route = !route && $allScreens.length === 0 ? "*" : route |
dispatch("save", { |
||||
$: { |
screens: selectedScreens, |
||||
if (templates && templateIndex === undefined) { |
}) |
||||
templateIndex = 0 |
|
||||
templateChanged(0) |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
const templateChanged = newTemplateIndex => { |
$: blankSelected = selectedScreens?.length === 1 |
||||
if (newTemplateIndex === undefined) return |
$: autoSelected = selectedScreens?.length > 0 && !blankSelected |
||||
draftScreen = templates[newTemplateIndex].create() |
|
||||
if (draftScreen.props._instanceName) { |
|
||||
name = draftScreen.props._instanceName |
|
||||
} |
|
||||
|
|
||||
if (draftScreen.props._component) { |
let templates = getTemplates($store, $tables.list) |
||||
baseComponent = draftScreen.props._component |
|
||||
} |
|
||||
|
|
||||
if (draftScreen.routing) { |
const confirm = async () => { |
||||
route = draftScreen.routing.route |
if (autoSelected) { |
||||
|
setScreens() |
||||
|
await save() |
||||
|
} else { |
||||
|
setScreens() |
||||
|
chooseModal(1) |
||||
} |
} |
||||
} |
} |
||||
|
const toggleScreenSelection = table => { |
||||
const save = async () => { |
if (selectedScreens.find(s => s.table === table.name)) { |
||||
if (!route) { |
selectedScreens = selectedScreens.filter( |
||||
routeError = "URL is required" |
screen => screen.table !== table.name |
||||
|
) |
||||
} else { |
} else { |
||||
if (routeExists(route, roleId)) { |
let partialTemplates = getTemplates($store, $tables.list).filter( |
||||
routeError = "This URL is already taken for this access role" |
template => template.table === table.name |
||||
} else { |
) |
||||
routeError = "" |
selectedScreens = [...partialTemplates, ...selectedScreens] |
||||
} |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
if (routeError) return false |
onDestroy(() => { |
||||
|
selectedScreens = [] |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
draftScreen.props._instanceName = name |
<div> |
||||
draftScreen.props._component = baseComponent |
<ModalContent |
||||
draftScreen.routing = { route, roleId } |
title="Add screens" |
||||
|
confirmText="Add Screens" |
||||
|
cancelText="Cancel" |
||||
|
onConfirm={() => confirm()} |
||||
|
disabled={!selectedScreens.length} |
||||
|
size="L" |
||||
|
> |
||||
|
<Body size="XS" |
||||
|
>Please select the screens you would like to add to your application. |
||||
|
Autogenerated screens come with CRUD functionality.</Body |
||||
|
> |
||||
|
|
||||
await store.actions.screens.create(draftScreen) |
<Layout noPadding gap="S"> |
||||
if (createLink) { |
<Detail size="S">Blank screen</Detail> |
||||
await store.actions.components.links.save(route, name) |
<div |
||||
} |
class="item" |
||||
await store.actions.routing.fetch() |
class:selected={selectedScreens.find(x => x.id.includes(blankScreen))} |
||||
|
on:click={() => |
||||
|
toggleScreenSelection(templates.find(t => t.id === blankScreen))} |
||||
|
class:disabled={autoSelected} |
||||
|
> |
||||
|
<div data-cy="blank-screen" class="content"> |
||||
|
<div class="text">Blank</div> |
||||
|
</div> |
||||
|
<div |
||||
|
style="color: var(--spectrum-global-color-green-600); float: right" |
||||
|
> |
||||
|
{#if selectedScreens.find(x => x.id === blankScreen)} |
||||
|
<div class="checkmark-spacing"> |
||||
|
<Icon size="S" name="CheckmarkCircleOutline" /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</div> |
||||
|
{#if $tables.list.filter(table => table._id !== "ta_users").length > 0} |
||||
|
<Detail size="S">Autogenerated Screens</Detail> |
||||
|
|
||||
if (templateIndex !== undefined) { |
{#each $tables.list.filter(table => table._id !== "ta_users") as table} |
||||
const template = templates[templateIndex] |
<div |
||||
analytics.captureEvent(Events.SCREEN.CREATED, { |
class:disabled={blankSelected} |
||||
template: template.id || template.name, |
class:selected={selectedScreens.find(x => x.table === table.name)} |
||||
}) |
on:click={() => toggleScreenSelection(table)} |
||||
} |
class="item" |
||||
|
> |
||||
|
<div class="content"> |
||||
|
<div class="text">{table.name}</div> |
||||
|
</div> |
||||
|
<div |
||||
|
style="color: var(--spectrum-global-color-green-600); float: right" |
||||
|
> |
||||
|
{#if selectedScreens.find(x => x.table === table.name)} |
||||
|
<div class="checkmark-spacing"> |
||||
|
<Icon size="S" name="CheckmarkCircleOutline" /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</div> |
||||
|
{/each} |
||||
|
{/if} |
||||
|
</Layout> |
||||
|
<div slot="footer"> |
||||
|
{#if showProgressCircle} |
||||
|
<div class="footer-progress"><ProgressCircle size="S" /></div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</ModalContent> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.disabled { |
||||
|
opacity: 0.3; |
||||
|
pointer-events: none; |
||||
|
} |
||||
|
.checkmark-spacing { |
||||
|
margin-right: var(--spacing-m); |
||||
} |
} |
||||
|
|
||||
const routeExists = (route, roleId) => { |
.content { |
||||
return $allScreens.some( |
letter-spacing: 0px; |
||||
screen => |
|
||||
screen.routing.route.toLowerCase() === route.toLowerCase() && |
|
||||
screen.routing.roleId === roleId |
|
||||
) |
|
||||
} |
} |
||||
|
|
||||
const routeChanged = event => { |
.footer-progress { |
||||
if (!event.detail.startsWith("/")) { |
margin-top: var(--spacing-s); |
||||
route = "/" + event.detail |
|
||||
} |
|
||||
route = sanitizeUrl(route) |
|
||||
} |
} |
||||
</script> |
|
||||
|
|
||||
<ModalContent title="New Screen" confirmText="Create Screen" onConfirm={save}> |
.text { |
||||
<Select |
font-weight: 600; |
||||
label="Choose a Template" |
margin-left: var(--spacing-m); |
||||
bind:value={templateIndex} |
font-size: 14px; |
||||
on:change={ev => templateChanged(ev.detail)} |
text-transform: capitalize; |
||||
options={templates} |
} |
||||
placeholder={null} |
|
||||
getOptionLabel={x => x.name} |
.item { |
||||
getOptionValue={(x, idx) => idx} |
cursor: pointer; |
||||
/> |
grid-gap: var(--spectrum-alias-grid-margin-xsmall); |
||||
<Input label="Name" bind:value={name} /> |
padding: var(--spectrum-alias-item-padding-s); |
||||
<Input |
background: var(--spectrum-alias-background-color-primary); |
||||
label="Url" |
transition: 0.3s all; |
||||
error={routeError} |
border: 1px solid var(--spectrum-global-color-gray-300); |
||||
bind:value={route} |
border-radius: 4px; |
||||
on:change={routeChanged} |
box-sizing: border-box; |
||||
/> |
border-width: 1px; |
||||
<Select |
display: flex; |
||||
label="Access" |
justify-content: space-between; |
||||
bind:value={roleId} |
align-items: center; |
||||
options={$roles} |
height: 60px; |
||||
getOptionLabel={x => x.name} |
} |
||||
getOptionValue={x => x._id} |
|
||||
/> |
.item:hover, |
||||
<Toggle text="Create link in navigation bar" bind:value={createLink} /> |
.selected { |
||||
</ModalContent> |
background: var(--spectrum-alias-background-color-tertiary); |
||||
|
} |
||||
|
</style> |
||||
|
|||||
@ -0,0 +1,70 @@ |
|||||
|
<script> |
||||
|
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui" |
||||
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" |
||||
|
import { selectedAccessRole, allScreens } from "builderStore" |
||||
|
import { onDestroy } from "svelte" |
||||
|
|
||||
|
export let screenName |
||||
|
export let url |
||||
|
export let chooseModal |
||||
|
export let save |
||||
|
export let showProgressCircle = false |
||||
|
|
||||
|
let routeError |
||||
|
let roleId = $selectedAccessRole || "BASIC" |
||||
|
|
||||
|
const routeChanged = event => { |
||||
|
if (!event.detail.startsWith("/")) { |
||||
|
url = "/" + event.detail |
||||
|
} |
||||
|
url = sanitizeUrl(url) |
||||
|
|
||||
|
if (routeExists(url, roleId)) { |
||||
|
routeError = "This URL is already taken for this access role" |
||||
|
} else { |
||||
|
routeError = "" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const routeExists = (url, roleId) => { |
||||
|
return $allScreens.some( |
||||
|
screen => |
||||
|
screen.routing.route.toLowerCase() === url.toLowerCase() && |
||||
|
screen.routing.roleId === roleId |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
onDestroy(() => { |
||||
|
screenName = "" |
||||
|
url = "" |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
size="M" |
||||
|
title={"Enter details"} |
||||
|
confirmText={"Continue"} |
||||
|
onCancel={() => chooseModal(0)} |
||||
|
onConfirm={() => save()} |
||||
|
cancelText={"Back"} |
||||
|
disabled={!screenName || !url || routeError} |
||||
|
> |
||||
|
<Input label="Name" bind:value={screenName} /> |
||||
|
<Input |
||||
|
label="URL" |
||||
|
error={routeError} |
||||
|
bind:value={url} |
||||
|
on:change={routeChanged} |
||||
|
/> |
||||
|
<div slot="footer"> |
||||
|
{#if showProgressCircle} |
||||
|
<div class="footer-progress"><ProgressCircle size="S" /></div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</ModalContent> |
||||
|
|
||||
|
<style> |
||||
|
.footer-progress { |
||||
|
margin-top: var(--spacing-s); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,133 @@ |
|||||
|
<script> |
||||
|
import ScreenDetailsModal from "components/design/NavigationPanel/ScreenDetailsModal.svelte" |
||||
|
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte" |
||||
|
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl" |
||||
|
import { Modal } from "@budibase/bbui" |
||||
|
import { store, selectedAccessRole, allScreens } from "builderStore" |
||||
|
import analytics, { Events } from "analytics" |
||||
|
|
||||
|
let newScreenModal |
||||
|
let navigationSelectionModal |
||||
|
let screenDetailsModal |
||||
|
let screenName = "" |
||||
|
let url = "" |
||||
|
let selectedScreens = [] |
||||
|
let roleId = $selectedAccessRole || "BASIC" |
||||
|
let showProgressCircle = false |
||||
|
let routeError |
||||
|
let createdScreens = [] |
||||
|
|
||||
|
const createScreens = async () => { |
||||
|
for (let screen of selectedScreens) { |
||||
|
let test = screen.create() |
||||
|
createdScreens.push(test) |
||||
|
analytics.captureEvent(Events.SCREEN.CREATED, { |
||||
|
template: screen.id || screen.name, |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const save = async () => { |
||||
|
showProgressCircle = true |
||||
|
await createScreens() |
||||
|
for (let screen of createdScreens) { |
||||
|
await saveScreens(screen) |
||||
|
} |
||||
|
await store.actions.routing.fetch() |
||||
|
selectedScreens = [] |
||||
|
createdScreens = [] |
||||
|
screenName = "" |
||||
|
url = "" |
||||
|
showProgressCircle = false |
||||
|
} |
||||
|
|
||||
|
const saveScreens = async draftScreen => { |
||||
|
let existingScreenCount = $store.screens.filter( |
||||
|
s => s.props._instanceName == draftScreen.props._instanceName |
||||
|
).length |
||||
|
if (existingScreenCount > 0) { |
||||
|
let oldUrlArr = draftScreen.routing.route.split("/") |
||||
|
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}` |
||||
|
draftScreen.routing.route = oldUrlArr.join("/") |
||||
|
} |
||||
|
|
||||
|
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route |
||||
|
if (draftScreen) { |
||||
|
if (!route) { |
||||
|
routeError = "URL is required" |
||||
|
} else { |
||||
|
if (routeExists(route, roleId)) { |
||||
|
routeError = "This URL is already taken for this access role" |
||||
|
} else { |
||||
|
routeError = "" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (routeError) return false |
||||
|
|
||||
|
if (screenName) { |
||||
|
draftScreen.props._instanceName = screenName |
||||
|
} |
||||
|
|
||||
|
draftScreen.routing.route = route |
||||
|
|
||||
|
await store.actions.screens.create(draftScreen) |
||||
|
if (draftScreen.props._instanceName.endsWith("List")) { |
||||
|
await store.actions.components.links.save( |
||||
|
draftScreen.routing.route, |
||||
|
draftScreen.routing.route.split("/")[1] |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const routeExists = (route, roleId) => { |
||||
|
return $allScreens.some( |
||||
|
screen => |
||||
|
screen.routing.route.toLowerCase() === route.toLowerCase() && |
||||
|
screen.routing.roleId === roleId |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const showModal = () => { |
||||
|
newScreenModal.show() |
||||
|
} |
||||
|
|
||||
|
const setScreens = evt => { |
||||
|
selectedScreens = evt.detail.screens |
||||
|
} |
||||
|
|
||||
|
const chooseModal = index => { |
||||
|
/* |
||||
|
0 = newScreenModal |
||||
|
1 = screenDetailsModal |
||||
|
2 = navigationSelectionModal |
||||
|
*/ |
||||
|
if (index === 0) { |
||||
|
newScreenModal.show() |
||||
|
} else if (index === 1) { |
||||
|
screenDetailsModal.show() |
||||
|
} else if (index === 2) { |
||||
|
navigationSelectionModal.show() |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<Modal bind:this={newScreenModal}> |
||||
|
<NewScreenModal |
||||
|
on:save={setScreens} |
||||
|
{showProgressCircle} |
||||
|
{save} |
||||
|
{chooseModal} |
||||
|
/> |
||||
|
</Modal> |
||||
|
|
||||
|
<Modal bind:this={screenDetailsModal}> |
||||
|
<ScreenDetailsModal |
||||
|
bind:screenName |
||||
|
bind:url |
||||
|
{showProgressCircle} |
||||
|
{save} |
||||
|
{chooseModal} |
||||
|
/> |
||||
|
</Modal> |
||||
@ -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 |
|
||||
/> |
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue