commit
c5672e7be3
|
@ -44,5 +44,8 @@
|
|||
],
|
||||
"rules": {
|
||||
"no-self-assign": "off"
|
||||
},
|
||||
"globals": {
|
||||
"GeolocationPositionError": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,3 +98,4 @@ hosting/proxy/.generated-nginx.prod.conf
|
|||
bin/
|
||||
hosting/.generated*
|
||||
packages/builder/cypress.env.json
|
||||
stats.html
|
||||
|
|
|
@ -22,9 +22,16 @@
|
|||
"name": "Budibase Worker",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/packages/worker/src/index.js",
|
||||
"runtimeArgs": [
|
||||
"--nolazy",
|
||||
"-r",
|
||||
"ts-node/register/transpile-only"
|
||||
],
|
||||
"args": [
|
||||
"${workspaceFolder}/packages/worker/src/index.ts"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/packages/worker"
|
||||
}
|
||||
},
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
|
|
43
README.md
43
README.md
|
@ -11,7 +11,7 @@
|
|||
The low code platform you'll enjoy using
|
||||
</h3>
|
||||
<p align="center">
|
||||
Budibase is an open source low-code platform, and the easiest way to build internal tools that improve productivity.
|
||||
Budibase is an open source low-code platform, and the easiest way to build internal apps that improve productivity.
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
|
@ -40,9 +40,11 @@
|
|||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://docs.budibase.com/getting-started">Get started</a>
|
||||
<a href="https://account.budibase.app/register">Get started - we host (Budibase Cloud)</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Docs</a>
|
||||
<a href="https://docs.budibase.com/docs/hosting-methods">Get started - you host (Docker, K8s, DO)</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com/docs">Docs</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas">Feature request</a>
|
||||
<span> · </span>
|
||||
|
@ -100,16 +102,45 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
|||
|
||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br />
|
||||
|
||||
---
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Budibase Public API
|
||||
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||
|
||||
- Budibase as a backend
|
||||
- Interoperability
|
||||
|
||||
|
||||
#### Docs
|
||||
You can learn more about the Budibase API at the following places:
|
||||
|
||||
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
||||
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||
|
||||
#### Guides
|
||||
|
||||
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
## 🏁 Get started
|
||||
|
||||
<a href="https://docs.budibase.com/self-hosting/self-host"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||
|
||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||
|
||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/self-hosting/self-host)
|
||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||
|
||||
### [Get started with Budibase Cloud](https://budibase.com)
|
||||
|
||||
|
@ -118,7 +149,7 @@ Or use Budibase Cloud if you don't need to self-host, and would like to get star
|
|||
|
||||
## 🎓 Learning Budibase
|
||||
|
||||
The Budibase documentation [lives here](https://docs.budibase.com).
|
||||
The Budibase documentation [lives here](https://docs.budibase.com/docs).
|
||||
<br />
|
||||
|
||||
|
||||
|
|
|
@ -110,6 +110,10 @@ spec:
|
|||
value: {{ .Values.globals.cookieDomain | quote }}
|
||||
- name: HTTP_MIGRATIONS
|
||||
value: {{ .Values.globals.httpMigrations | quote }}
|
||||
- name: GOOGLE_CLIENT_ID
|
||||
value: {{ .Values.globals.google.clientId | quote }}
|
||||
- name: GOOGLE_CLIENT_SECRET
|
||||
value: {{ .Values.globals.google.secret | quote }}
|
||||
image: budibase/apps:{{ .Values.globals.appVersion }}
|
||||
imagePullPolicy: Always
|
||||
name: bbapps
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
|
@ -0,0 +1,41 @@
|
|||
# Budibase API + Next.js example
|
||||
|
||||
This is an example of how Budibase can be used as a backend for a Postgres database for a Next.js sales app. You will
|
||||
need to follow the walk-through that has been published in the Budibase docs to set up your Budibase app for this example.
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
To use this example you will need:
|
||||
1. [Docker](https://www.docker.com/)
|
||||
2. [Docker Compose](https://docs.docker.com/compose/)
|
||||
3. [Node.js](https://nodejs.org/en/)
|
||||
4. A self-hosted Budibase installation
|
||||
|
||||
## Getting Started
|
||||
|
||||
The first step is to set up the database - you can do this by going to the `db/` directory and running the command:
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The next step is to follow the example walk-through and set up a Budibase app as it describes. Once you've done
|
||||
this you can configure the settings in `next.config.js`, specifically the `apiKey`, `host` and `appName`.
|
||||
|
||||
Finally, you can start the dev server with the following command:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Accessing the app
|
||||
|
||||
Open [http://localhost:3001](http://localhost:3001) with your browser to see the sales app.
|
||||
|
||||
Look in the API routes (`pages/api/sales.ts` and `pages/api/salespeople.ts`) to see how this is integrated with Budibase.
|
||||
There is also a utility file where some core functions and types have been defined, in `utilities/index.ts`.
|
||||
|
||||
## Attribution
|
||||
This example was set up using [Next.js](https://nextjs.org/) and bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
|
@ -0,0 +1,42 @@
|
|||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { ReactNotifications } from "react-notifications-component"
|
||||
|
||||
function layout(props: any) {
|
||||
return (
|
||||
<>
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div id="navbar" className="navbar-menu">
|
||||
<div className="logo">
|
||||
<Image alt="logo" src="/bb-emblem.svg" width="50" height="50" />
|
||||
</div>
|
||||
<div className="navbar-start">
|
||||
<Link href="/">
|
||||
<a className="navbar-item">
|
||||
List
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/save">
|
||||
<a className="navbar-item">
|
||||
New sale
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<div className="buttons">
|
||||
<a className="button is-primary" href="https://budibase.readme.io/reference">
|
||||
<strong>API Documentation</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<ReactNotifications />
|
||||
{props.children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default layout
|
|
@ -0,0 +1,28 @@
|
|||
import { Store } from "react-notifications-component"
|
||||
|
||||
const notifications = {
|
||||
error: (error: string, title: string) => {
|
||||
Store.addNotification({
|
||||
container: "top-right",
|
||||
type: "danger",
|
||||
message: error,
|
||||
title: title,
|
||||
dismiss: {
|
||||
duration: 10000,
|
||||
},
|
||||
})
|
||||
},
|
||||
success: (message: string, title: string) => {
|
||||
Store.addNotification({
|
||||
container: "top-right",
|
||||
type: "success",
|
||||
message: message,
|
||||
title: title,
|
||||
dismiss: {
|
||||
duration: 3000,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default notifications
|
|
@ -0,0 +1,17 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
db:
|
||||
container_name: postgres
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data/
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
volumes:
|
||||
pg_data:
|
|
@ -0,0 +1,21 @@
|
|||
CREATE TABLE IF NOT EXISTS sales_people (
|
||||
person_id SERIAL PRIMARY KEY,
|
||||
name varchar(200) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sales (
|
||||
sale_id SERIAL PRIMARY KEY,
|
||||
sale_name varchar(200) NOT NULL,
|
||||
sold_by INT,
|
||||
CONSTRAINT sold_by_fk
|
||||
FOREIGN KEY(sold_by)
|
||||
REFERENCES sales_people(person_id)
|
||||
);
|
||||
|
||||
INSERT INTO sales_people (name)
|
||||
select 'Salesperson ' || id
|
||||
FROM GENERATE_SERIES(1, 50) as id;
|
||||
|
||||
INSERT INTO sales (sale_name, sold_by)
|
||||
select 'Sale ' || id, floor(random() * 50 + 1)::int
|
||||
FROM GENERATE_SERIES(1, 200) as id;
|
|
@ -0,0 +1,7 @@
|
|||
import { components } from "./openapi"
|
||||
|
||||
export type App = components["schemas"]["applicationOutput"]["data"]
|
||||
export type Table = components["schemas"]["tableOutput"]["data"]
|
||||
export type TableSearch = components["schemas"]["tableSearch"]
|
||||
export type AppSearch = components["schemas"]["applicationSearch"]
|
||||
export type RowSearch = components["schemas"]["searchOutput"]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -0,0 +1,16 @@
|
|||
const { join } = require("path")
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
sassOptions: {
|
||||
includePaths: [join(__dirname, "styles")],
|
||||
},
|
||||
serverRuntimeConfig: {
|
||||
apiKey:
|
||||
"bf4d86af933b5ac0af0fdbe4bf7d89ff-f929752a1eeaafb00f4b5e3325097d51a44fe4b39f22ed857923409cc75414b379323a25ebfb4916",
|
||||
appName: "sales",
|
||||
host: "http://localhost:10000",
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "nextjs-api-sales",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"bulma": "^0.9.3",
|
||||
"next": "12.1.0",
|
||||
"node-fetch": "^3.2.2",
|
||||
"node-sass": "^7.0.1",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-notifications-component": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.21",
|
||||
"@types/react": "17.0.39",
|
||||
"eslint": "8.10.0",
|
||||
"eslint-config-next": "12.1.0",
|
||||
"typescript": "4.6.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import "../styles/global.sass"
|
||||
import type { AppProps } from "next/app"
|
||||
import Head from "next/head"
|
||||
import Layout from "../components/layout"
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<Layout>
|
||||
<Head>
|
||||
<title>BB NextJS Sales Example</title>
|
||||
</Head>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
|
@ -0,0 +1,46 @@
|
|||
import { getApp, findTable, makeCall } from "../../utilities"
|
||||
|
||||
async function getSales(req: any) {
|
||||
const { page } = req.query
|
||||
const { _id: appId } = await getApp()
|
||||
const table = await findTable(appId, "sales")
|
||||
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||
appId,
|
||||
body: {
|
||||
limit: 10,
|
||||
sort: {
|
||||
type: "string",
|
||||
order: "descending",
|
||||
column: "sale_id",
|
||||
},
|
||||
paginate: true,
|
||||
bookmark: parseInt(page),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function saveSale(req: any) {
|
||||
const { _id: appId } = await getApp()
|
||||
const table = await findTable(appId, "sales")
|
||||
return await makeCall("post", `tables/${table._id}/rows`, {
|
||||
body: req.body,
|
||||
appId,
|
||||
})
|
||||
}
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
let response: any = {}
|
||||
try {
|
||||
if (req.method === "POST") {
|
||||
response = await saveSale(req)
|
||||
} else if (req.method === "GET") {
|
||||
response = await getSales(req)
|
||||
} else {
|
||||
res.status(404)
|
||||
return
|
||||
}
|
||||
res.status(200).json(response)
|
||||
} catch (err: any) {
|
||||
res.status(400).send(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { getApp, findTable, makeCall } from "../../utilities"
|
||||
|
||||
async function getSalespeople() {
|
||||
const { _id: appId } = await getApp()
|
||||
const table = await findTable(appId, "sales_people")
|
||||
return await makeCall("post", `tables/${table._id}/rows/search`, {
|
||||
appId,
|
||||
body: {
|
||||
sort: {
|
||||
type: "string",
|
||||
order: "ascending",
|
||||
column: "person_id",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default async function handler(req: any, res: any) {
|
||||
let response: any = {}
|
||||
try {
|
||||
if (req.method === "GET") {
|
||||
response = await getSalespeople()
|
||||
} else {
|
||||
res.status(404)
|
||||
return
|
||||
}
|
||||
res.status(200).json(response)
|
||||
} catch (err: any) {
|
||||
res.status(400).send(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import type { NextPage } from "next"
|
||||
import styles from "../styles/home.module.css"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import Notifications from "../components/notifications"
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [sales, setSales] = useState([])
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
const getSales = useCallback(async (page: Number = 1) => {
|
||||
let url = "/api/sales"
|
||||
if (page) {
|
||||
url += `?page=${page}`
|
||||
}
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
Notifications.error(error, "Failed to get sales")
|
||||
return
|
||||
}
|
||||
const sales = await response.json()
|
||||
// @ts-ignore
|
||||
setCurrentPage(page)
|
||||
return setSales(sales.data)
|
||||
}, [])
|
||||
|
||||
const goToNextPage = useCallback(async () => {
|
||||
await getSales(currentPage + 1)
|
||||
}, [currentPage, getSales])
|
||||
|
||||
const goToPrevPage = useCallback(async () => {
|
||||
if (currentPage > 1) {
|
||||
await getSales(currentPage - 1)
|
||||
}
|
||||
}, [currentPage, getSales])
|
||||
|
||||
useEffect(() => {
|
||||
getSales().then(() => {
|
||||
setLoaded(true)
|
||||
}).catch(() => {
|
||||
setSales([])
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!loaded) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.tableSection}>
|
||||
<h1 className="subtitle">Sales</h1>
|
||||
<div className={styles.table}>
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sale ID</th>
|
||||
<th>name</th>
|
||||
<th>Sold by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sales.map((sale: any) =>
|
||||
<tr key={sale.sale_id}>
|
||||
<th>{sale.sale_id}</th>
|
||||
<th>{sale.sale_name}</th>
|
||||
<th>{sale.sales_person?.map((person: any) => person.primaryDisplay)[0]}</th>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={styles.buttons}>
|
||||
<button className="button" onClick={goToPrevPage}>Prev Page</button>
|
||||
<button className="button" onClick={goToNextPage}>Next Page</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
|
@ -0,0 +1,81 @@
|
|||
import type { NextPage } from "next"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import styles from "../styles/save.module.css"
|
||||
import Notifications from "../components/notifications"
|
||||
|
||||
const Save: NextPage = () => {
|
||||
const [salespeople, setSalespeople] = useState([])
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
const saveSale = useCallback(async (event: any) => {
|
||||
event.preventDefault()
|
||||
const sale = {
|
||||
sale_name: event.target.name.value,
|
||||
sales_person: [event.target.soldBy.value],
|
||||
}
|
||||
const response = await fetch("/api/sales", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(sale),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const error = await response.text()
|
||||
Notifications.error(error, "Failed to save sale")
|
||||
return
|
||||
}
|
||||
Notifications.success("Sale saved successfully!", "Sale saved")
|
||||
}, [])
|
||||
|
||||
const getSalespeople = useCallback(async () => {
|
||||
const response: any = await fetch("/api/salespeople")
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text())
|
||||
}
|
||||
const json = await response.json()
|
||||
setSalespeople(json.data)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getSalespeople().then(() => {
|
||||
setLoaded(true)
|
||||
}).catch(() => {
|
||||
setSalespeople([])
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!loaded) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.formSection}>
|
||||
<h1 className="subtitle">New sale</h1>
|
||||
<form onSubmit={saveSale}>
|
||||
<div className="field">
|
||||
<label className="label">Name</label>
|
||||
<div className="control">
|
||||
<input id="name" className="input" type="text" placeholder="Text input" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Sold by</label>
|
||||
<div className="control">
|
||||
<div className="select">
|
||||
<select id="soldBy">
|
||||
{salespeople.map((person: any) => <option key={person._id} value={person._id}>{person.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button className="button is-link">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Save
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#393C44;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#4285F4;}
|
||||
</style>
|
||||
<rect x="-152.17" y="-24.17" class="st0" width="96.17" height="96.17"/>
|
||||
<path class="st1" d="M-83.19,48h-41.79c-1.76,0-3.19-1.43-3.19-3.19V3.02c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C-80,46.57-81.43,48-83.19,48z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M-99.62,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35h-4.89V12.57H-99.62z
|
||||
M-93.46,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C-93.55,28.92-93.46,28.52-93.46,28.11z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M-114.76,12.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58
|
||||
c0.86,0.39,1.59,0.91,2.19,1.57c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89
|
||||
c-0.35,0.9-0.84,1.68-1.47,2.35c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V35
|
||||
h-4.89V12.57H-114.76z M-108.6,28.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C-108.68,28.92-108.6,28.52-108.6,28.11z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st2" d="M44.81,159H3.02c-1.76,0-3.19-1.43-3.19-3.19v-41.79c0-1.76,1.43-3.19,3.19-3.19h41.79
|
||||
c1.76,0,3.19,1.43,3.19,3.19v41.79C48,157.57,46.57,159,44.81,159z"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st1" d="M28.38,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146h-4.89v-22.43H28.38z
|
||||
M34.54,139.11c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69
|
||||
c-0.38-0.17-0.79-0.26-1.24-0.26c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01
|
||||
c-0.17,0.39-0.26,0.8-0.26,1.23c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68
|
||||
c0.39,0.17,0.8,0.26,1.23,0.26c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1
|
||||
C34.45,139.92,34.54,139.52,34.54,139.11z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M13.24,123.57v9.94c1.15-1.21,2.59-1.81,4.32-1.81c1.03,0,1.97,0.19,2.82,0.58c0.86,0.39,1.59,0.91,2.19,1.57
|
||||
c0.6,0.66,1.08,1.43,1.42,2.32c0.34,0.89,0.51,1.84,0.51,2.85c0,1.03-0.18,1.99-0.53,2.89c-0.35,0.9-0.84,1.68-1.47,2.35
|
||||
c-0.63,0.67-1.37,1.19-2.23,1.58c-0.86,0.39-1.78,0.58-2.77,0.58c-1.8,0-3.22-0.66-4.27-1.97V146H8.35v-22.43H13.24z M19.4,139.11
|
||||
c0-0.43-0.08-0.84-0.24-1.23c-0.16-0.39-0.39-0.72-0.68-1.01c-0.29-0.29-0.62-0.52-1-0.69c-0.38-0.17-0.79-0.26-1.24-0.26
|
||||
c-0.43,0-0.84,0.08-1.22,0.24c-0.38,0.16-0.71,0.39-0.99,0.68c-0.28,0.29-0.5,0.63-0.68,1.01c-0.17,0.39-0.26,0.8-0.26,1.23
|
||||
c0,0.43,0.08,0.84,0.24,1.22c0.16,0.38,0.39,0.71,0.68,0.99c0.29,0.28,0.63,0.5,1.01,0.68c0.39,0.17,0.8,0.26,1.23,0.26
|
||||
c0.43,0,0.84-0.08,1.22-0.24c0.38-0.16,0.71-0.39,0.99-0.68c0.28-0.29,0.5-0.62,0.68-1C19.32,139.92,19.4,139.52,19.4,139.11z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st0" d="M44,48H4c-2.21,0-4-1.79-4-4V4c0-2.21,1.79-4,4-4h40c2.21,0,4,1.79,4,4v40C48,46.21,46.21,48,44,48z"/>
|
||||
<g>
|
||||
<path class="st1" d="M28.48,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||
c-0.64,0.7-1.4,1.25-2.28,1.66C34.8,35.8,33.86,36,32.84,36c-1.84,0-3.3-0.69-4.37-2.07v1.62h-5V12H28.48z M34.78,28.31
|
||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C34.69,29.16,34.78,28.75,34.78,28.31z"
|
||||
/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M13,12v10.44c1.18-1.27,2.65-1.9,4.42-1.9c1.05,0,2.01,0.2,2.89,0.61c0.87,0.41,1.62,0.96,2.24,1.65
|
||||
c0.62,0.69,1.1,1.5,1.45,2.44c0.35,0.94,0.52,1.93,0.52,2.99c0,1.08-0.18,2.09-0.54,3.04c-0.36,0.95-0.86,1.77-1.51,2.47
|
||||
c-0.64,0.7-1.4,1.25-2.28,1.66C19.32,35.8,18.38,36,17.37,36c-1.84,0-3.3-0.69-4.37-2.07v1.62H8V12H13z M19.3,28.31
|
||||
c0-0.45-0.08-0.88-0.25-1.29c-0.17-0.41-0.4-0.76-0.69-1.06c-0.3-0.3-0.64-0.54-1.02-0.72c-0.39-0.18-0.81-0.27-1.27-0.27
|
||||
c-0.44,0-0.86,0.09-1.24,0.26c-0.39,0.17-0.72,0.41-1.01,0.71c-0.29,0.3-0.52,0.66-0.69,1.06c-0.18,0.41-0.26,0.84-0.26,1.29
|
||||
s0.08,0.88,0.25,1.28c0.17,0.4,0.4,0.74,0.69,1.04c0.29,0.29,0.64,0.53,1.04,0.71c0.4,0.18,0.82,0.27,1.26,0.27
|
||||
c0.44,0,0.86-0.09,1.24-0.26c0.39-0.17,0.72-0.41,1.01-0.71c0.29-0.3,0.52-0.65,0.69-1.05C19.21,29.16,19.3,28.75,19.3,28.31z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
|
@ -0,0 +1,26 @@
|
|||
@charset "utf-8"
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700')
|
||||
$family-sans-serif: "Roboto", sans-serif
|
||||
|
||||
#__next
|
||||
display: flex
|
||||
flex-direction: column
|
||||
justify-content: flex-start
|
||||
align-items: stretch
|
||||
height: 100vh
|
||||
--bg-color: #f5f5f5
|
||||
|
||||
.logo
|
||||
padding: 0.75rem
|
||||
|
||||
@import "../node_modules/bulma/bulma.sass"
|
||||
@import "../node_modules/react-notifications-component/dist/theme.css"
|
||||
|
||||
// applied after bulma styles are enabled
|
||||
html
|
||||
overflow-y: auto
|
||||
|
||||
.navbar
|
||||
background-color: var(--bg-color)
|
||||
color: white
|
|
@ -0,0 +1,30 @@
|
|||
.container {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 5rem 2rem 0;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tableSection {
|
||||
padding: 2rem;
|
||||
background: var(--bg-color);
|
||||
width: 800px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.table table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableSection h1 {
|
||||
text-align: center;
|
||||
color: black;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
.container {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 5rem 2rem 0;
|
||||
align-items: center;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.formSection {
|
||||
padding: 2rem;
|
||||
background: var(--bg-color);
|
||||
width: 400px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.formSection h1 {
|
||||
text-align: center;
|
||||
color: black;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { App, AppSearch, Table, TableSearch } from "../definitions"
|
||||
import getConfig from "next/config"
|
||||
|
||||
const { serverRuntimeConfig } = getConfig()
|
||||
const apiKey = serverRuntimeConfig["apiKey"]
|
||||
const appName = serverRuntimeConfig["appName"]
|
||||
const host = serverRuntimeConfig["host"]
|
||||
|
||||
let APP: App | null = null
|
||||
let TABLES: { [key: string]: Table } = {}
|
||||
|
||||
export async function makeCall(
|
||||
method: string,
|
||||
url: string,
|
||||
opts?: { body?: any; appId?: string }
|
||||
): Promise<any> {
|
||||
const fetchOpts: any = {
|
||||
method,
|
||||
headers: {
|
||||
"x-budibase-api-key": apiKey,
|
||||
},
|
||||
}
|
||||
if (opts?.appId) {
|
||||
fetchOpts.headers["x-budibase-app-id"] = opts.appId
|
||||
}
|
||||
if (opts?.body) {
|
||||
fetchOpts.body =
|
||||
typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body
|
||||
fetchOpts.headers["Content-Type"] = "application/json"
|
||||
}
|
||||
const finalUrl = `${host}/api/public/v1/${url}`
|
||||
const response = await fetch(finalUrl, fetchOpts)
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
} else {
|
||||
const error = await response.text()
|
||||
console.error("Budibase server error - ", error)
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getApp(): Promise<App> {
|
||||
if (APP) {
|
||||
return APP
|
||||
}
|
||||
const apps: AppSearch = await makeCall("post", "applications/search", {
|
||||
body: {
|
||||
name: appName,
|
||||
},
|
||||
})
|
||||
const app = apps.data.find((app: App) => app.name === appName)
|
||||
if (!app) {
|
||||
throw new Error(
|
||||
"Could not find app, please make sure app name in config is correct."
|
||||
)
|
||||
}
|
||||
APP = app
|
||||
return app
|
||||
}
|
||||
|
||||
export async function findTable(
|
||||
appId: string,
|
||||
tableName: string
|
||||
): Promise<Table> {
|
||||
if (TABLES[tableName]) {
|
||||
return TABLES[tableName]
|
||||
}
|
||||
const tables: TableSearch = await makeCall("post", "tables/search", {
|
||||
body: {
|
||||
name: tableName,
|
||||
},
|
||||
appId,
|
||||
})
|
||||
const table = tables.data.find((table: Table) => table.name === tableName)
|
||||
if (!table) {
|
||||
throw new Error(
|
||||
"Could not find table, please make sure your app is configured with the Postgres datasource correctly."
|
||||
)
|
||||
}
|
||||
TABLES[tableName] = table
|
||||
return table
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
|
|||
|
||||
* Budibase website: http://budibase.com
|
||||
|
||||
For help and more information, visit https://docs.budibase.com/self-hosting/hosting-methods/digitalocean
|
||||
For help and more information, visit https://docs.budibase.com/docs/digitalocean
|
||||
|
||||
********************************************************************************
|
||||
To delete this message of the day: rm -rf $(readlink -f ${0})
|
||||
|
|
|
@ -5,7 +5,7 @@ version: "3"
|
|||
services:
|
||||
minio-service:
|
||||
container_name: budi-minio-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: minio/minio
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
|
@ -23,7 +23,7 @@ services:
|
|||
|
||||
proxy-service:
|
||||
container_name: budi-nginx-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- ./.generated-nginx.dev.conf:/etc/nginx/nginx.conf
|
||||
|
@ -38,7 +38,7 @@ services:
|
|||
couchdb-service:
|
||||
# platform: linux/amd64
|
||||
container_name: budi-couchdb-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: ibmcom/couchdb3
|
||||
environment:
|
||||
- COUCHDB_PASSWORD=${COUCH_DB_PASSWORD}
|
||||
|
@ -59,7 +59,7 @@ services:
|
|||
|
||||
redis-service:
|
||||
container_name: budi-redis-dev
|
||||
restart: always
|
||||
restart: on-failure
|
||||
image: redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD}
|
||||
ports:
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://docs.budibase.com/getting-started">Los Geht's</a>
|
||||
<a href="https://docs.budibase.com/docs/quickstart-tutorials">Los Geht's</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Dokumentation</a>
|
||||
<span> · </span>
|
||||
|
@ -109,7 +109,7 @@ $ budi hosting --start
|
|||
4. Lege einen Admin-Benutzer an.
|
||||
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
||||
|
||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
|
||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/docs/quickstart-tutorials).
|
||||
|
||||
<br />
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
|
|||
|
||||
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
|
||||
|
||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.98-alpha.9",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier",
|
||||
"lint:fix:eslint": "eslint --fix packages",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix:prettier": "prettier --write \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\"",
|
||||
"lint:fix": "yarn run lint:fix:prettier && yarn run lint:fix:eslint",
|
||||
"test:e2e": "lerna run cy:test --stream",
|
||||
"test:e2e:ci": "lerna run cy:ci --stream",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.98-alpha.9",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
const google = require("../google")
|
||||
const { Cookies } = require("../../../constants")
|
||||
const { Cookies, Configs } = require("../../../constants")
|
||||
const { clearCookie, getCookie } = require("../../../utils")
|
||||
const { getDB } = require("../../../db")
|
||||
const { getScopedConfig } = require("../../../db/utils")
|
||||
const environment = require("../../../environment")
|
||||
const { getGlobalDB } = require("../../../tenancy")
|
||||
|
||||
async function preAuth(passport, ctx, next) {
|
||||
// get the relevant config
|
||||
const googleConfig = {
|
||||
async function fetchGoogleCreds() {
|
||||
// try and get the config from the tenant
|
||||
const db = getGlobalDB()
|
||||
const googleConfig = await getScopedConfig(db, {
|
||||
type: Configs.GOOGLE,
|
||||
})
|
||||
// or fall back to env variables
|
||||
const config = googleConfig || {
|
||||
clientID: environment.GOOGLE_CLIENT_ID,
|
||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
async function preAuth(passport, ctx, next) {
|
||||
// get the relevant config
|
||||
const googleConfig = await fetchGoogleCreds()
|
||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(googleConfig, callbackUrl)
|
||||
|
||||
|
@ -26,10 +40,7 @@ async function preAuth(passport, ctx, next) {
|
|||
|
||||
async function postAuth(passport, ctx, next) {
|
||||
// get the relevant config
|
||||
const config = {
|
||||
clientID: environment.GOOGLE_CLIENT_ID,
|
||||
clientSecret: environment.GOOGLE_CLIENT_SECRET,
|
||||
}
|
||||
const config = await fetchGoogleCreds()
|
||||
|
||||
let callbackUrl = `${environment.PLATFORM_URL}/api/global/auth/datasource/google/callback`
|
||||
const strategy = await google.strategyFactory(
|
||||
|
|
|
@ -51,7 +51,10 @@ exports.strategyFactory = async function (
|
|||
)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new Error("Error constructing google authentication strategy", err)
|
||||
throw new Error(
|
||||
`Error constructing google authentication strategy: ${err}`,
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
// expose for testing
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.98-alpha.9",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.0.100",
|
||||
"@budibase/string-templates": "^1.0.98-alpha.9",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
export let icon = undefined
|
||||
export let active = false
|
||||
export let tooltip = undefined
|
||||
export let dataCy
|
||||
|
||||
let showTooltip = false
|
||||
</script>
|
||||
|
@ -27,6 +28,7 @@
|
|||
class:active
|
||||
class="spectrum-Button spectrum-Button--size{size.toUpperCase()}"
|
||||
{disabled}
|
||||
data-cy={dataCy}
|
||||
on:click|preventDefault
|
||||
on:mouseover={() => (showTooltip = true)}
|
||||
on:focus={() => (showTooltip = true)}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import Divider from "../Divider/Divider.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import Context from "../context"
|
||||
import ProgressCircle from "../ProgressCircle/ProgressCircle.svelte"
|
||||
|
||||
export let title = undefined
|
||||
export let size = "S"
|
||||
|
@ -102,15 +103,22 @@
|
|||
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||
{/if}
|
||||
{#if showConfirmButton}
|
||||
<Button
|
||||
group
|
||||
cta
|
||||
{...$$restProps}
|
||||
disabled={confirmDisabled}
|
||||
on:click={confirm}
|
||||
>
|
||||
{confirmText}
|
||||
</Button>
|
||||
<span class="confirm-wrap">
|
||||
<Button
|
||||
group
|
||||
cta
|
||||
{...$$restProps}
|
||||
disabled={confirmDisabled}
|
||||
on:click={confirm}
|
||||
>
|
||||
{#if loading}
|
||||
<ProgressCircle overBackground={true} size="S" />
|
||||
{/if}
|
||||
{#if !loading}
|
||||
{confirmText}
|
||||
{/if}
|
||||
</Button>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -169,4 +177,8 @@
|
|||
.spectrum-Dialog-buttonGroup {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.confirm-wrap :global(.spectrum-Button-label) {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
$: if (!loading) loaded = true
|
||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||
$: rows = fields?.length ? data || [] : []
|
||||
$: totalRowCount = rows?.length || 0
|
||||
$: visibleRowCount = getVisibleRowCount(
|
||||
loaded,
|
||||
height,
|
||||
|
@ -63,7 +64,12 @@
|
|||
rowCount,
|
||||
rowHeight
|
||||
)
|
||||
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
|
||||
$: heightStyle = getHeightStyle(
|
||||
visibleRowCount,
|
||||
rowCount,
|
||||
totalRowCount,
|
||||
rowHeight
|
||||
)
|
||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
||||
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
||||
$: showEditColumn = allowEditRows || allowSelectRows
|
||||
|
@ -107,11 +113,16 @@
|
|||
return Math.min(allRows, Math.ceil(height / rowHeight))
|
||||
}
|
||||
|
||||
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
|
||||
if (!rowCount || !visibleRows) {
|
||||
const getHeightStyle = (
|
||||
visibleRowCount,
|
||||
rowCount,
|
||||
totalRowCount,
|
||||
rowHeight
|
||||
) => {
|
||||
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
|
||||
return ""
|
||||
}
|
||||
return `height: ${headerHeight + visibleRows * rowHeight}px;`
|
||||
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
|
||||
}
|
||||
|
||||
const getGridStyle = (fields, schema, showEditColumn) => {
|
||||
|
@ -264,11 +275,11 @@
|
|||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||
>
|
||||
{#if !loaded}
|
||||
<div class="loading" style={contentStyle}>
|
||||
<div class="loading" style={heightStyle}>
|
||||
<ProgressCircle />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
|
||||
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
|
||||
{#if fields.length}
|
||||
<div class="spectrum-Table-head">
|
||||
{#if showEditColumn}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -5,10 +5,14 @@ filterTests(['all'], () => {
|
|||
before(() => {
|
||||
cy.login()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.deleteAllApps()
|
||||
})
|
||||
|
||||
it("should change the icon and colour for an application", () => {
|
||||
// Search for test application
|
||||
cy.searchForApplication("Cypress Tests")
|
||||
cy.applicationInAppTable("Cypress Tests")
|
||||
cy.get(".appTable")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
|
|
|
@ -2,11 +2,146 @@ import filterTests from '../support/filterTests'
|
|||
|
||||
filterTests(['smoke', 'all'], () => {
|
||||
context("Create an Application", () => {
|
||||
it("should create a new application", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.contains("Cypress Tests").should("exist")
|
||||
})
|
||||
|
||||
it("should show the new user UI/UX", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').should("exist")
|
||||
cy.get(`[data-cy="import-app-btn"]`).should("exist")
|
||||
|
||||
cy.get(".template-category-filters").should("exist")
|
||||
cy.get(".template-categories").should("exist")
|
||||
|
||||
cy.get(".appTable").should("not.exist")
|
||||
})
|
||||
|
||||
it("should provide filterable templates", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
|
||||
cy.get(".template-category-filters").should("exist")
|
||||
cy.get(".template-categories").should("exist")
|
||||
|
||||
cy.get(".template-category").its('length').should('be.gt', 1)
|
||||
cy.get(".template-category-filters .spectrum-ActionButton").its('length').should('be.gt', 2)
|
||||
|
||||
cy.get(".template-category-filters .spectrum-ActionButton").eq(1).click()
|
||||
cy.get(".template-category").should('have.length', 1)
|
||||
|
||||
cy.get(".template-category-filters .spectrum-ActionButton").eq(0).click()
|
||||
cy.get(".template-category").its('length').should('be.gt', 1)
|
||||
})
|
||||
|
||||
it("should enforce a valid url before submission", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
|
||||
const appName = "A New App"
|
||||
|
||||
cy.get(`[data-cy="create-app-btn"]`).contains('Start from scratch').click({force: true})
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
|
||||
//Auto fill
|
||||
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
||||
cy.get("input").eq(1).should("have.value", "/a-new-app")
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
|
||||
|
||||
//Empty the app url - disabled create
|
||||
cy.get("input").eq(1).clear().blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
|
||||
|
||||
//Invalid url
|
||||
cy.get("input").eq(1).type("/new app-url").blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('be.disabled')
|
||||
|
||||
//Specifically alter the url
|
||||
cy.get("input").eq(1).clear()
|
||||
cy.get("input").eq(1).type("another-app-name").blur()
|
||||
cy.get("input").eq(1).should("have.value", "/another-app-name")
|
||||
cy.get("input").eq(0).should("have.value", appName)
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").should('not.be.disabled')
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
it("should create the first application from scratch", () => {
|
||||
const appName = "Cypress Tests"
|
||||
cy.deleteApp(appName)
|
||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(1000)
|
||||
|
||||
cy.applicationInAppTable(appName)
|
||||
cy.deleteApp(appName)
|
||||
})
|
||||
|
||||
it("should generate the first application from a template", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
|
||||
cy.get(".template-category-filters").should("exist")
|
||||
cy.get(".template-categories").should("exist")
|
||||
|
||||
//### Select nth template and choose to create?
|
||||
cy.get('.template-category').eq(0).within(() => {
|
||||
const card = cy.get('.template-card').eq(0).should("exist");
|
||||
const cardOverlay = card.get('.template-thumbnail-action-overlay').should("exist")
|
||||
cardOverlay.invoke("show")
|
||||
cardOverlay.get("button").contains("Use template").should("exist").click({force: true})
|
||||
})
|
||||
|
||||
//### CMD Create app from theme card
|
||||
cy.get(".spectrum-Modal").should('be.visible')
|
||||
|
||||
const templateName = cy.get(".spectrum-Modal .template-thumbnail-text")
|
||||
templateName.invoke('text')
|
||||
.then(templateNameText => {
|
||||
const templateNameParsed = "/"+templateNameText.toLowerCase().replace(/\s+/g, "-")
|
||||
cy.get(".spectrum-Modal input").eq(0).should("have.value", templateNameText)
|
||||
cy.get(".spectrum-Modal input").eq(1).should("have.value", templateNameParsed)
|
||||
|
||||
cy.get(".spectrum-Modal .spectrum-ButtonGroup").contains("Create app").click()
|
||||
cy.wait(5000)
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(1000)
|
||||
|
||||
cy.applicationInAppTable(templateNameText)
|
||||
cy.deleteAllApps()
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
it("should display a second application and app filtering", () => {
|
||||
const appName = "Cypress Tests"
|
||||
cy.deleteApp(appName)
|
||||
cy.createApp(appName, "This app is used for Cypress testing.")
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
|
||||
const secondAppName = "Second App Demo"
|
||||
cy.deleteApp(secondAppName)
|
||||
|
||||
cy.get(`[data-cy="create-app-btn"]`).contains('Create new app').click({force: true})
|
||||
cy.wait(500)
|
||||
cy.url().should('include', '/builder/portal/apps/create')
|
||||
|
||||
cy.createAppFromScratch(secondAppName)
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
|
||||
//Both applications should exist and be searchable
|
||||
cy.searchForApplication(appName)
|
||||
cy.searchForApplication(secondAppName)
|
||||
|
||||
cy.deleteAllApps()
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,6 +7,10 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.createTestApp()
|
||||
})
|
||||
|
||||
after(() => {
|
||||
cy.deleteAllApps()
|
||||
})
|
||||
|
||||
it("should create a new Table", () => {
|
||||
cy.createTable("dog")
|
||||
cy.wait(1000)
|
||||
|
|
|
@ -4,9 +4,14 @@ filterTests(["smoke", "all"], () => {
|
|||
context("Create a User and Assign Roles", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.createAppFromScratch("Initial App")
|
||||
})
|
||||
|
||||
it("should create a user", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(1000)
|
||||
cy.createUser("bbuser@test.com")
|
||||
cy.get(".spectrum-Table").should("contain", "bbuser")
|
||||
})
|
||||
|
@ -21,95 +26,104 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||
})
|
||||
|
||||
it("should assign role types", () => {
|
||||
// 3 apps minimum required - to assign an app to each role type
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length < 3) {
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const uuid = () => Cypress._.random(0, 1e6)
|
||||
const name = uuid()
|
||||
cy.createApp(name)
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
it("should assign role types", () => {
|
||||
// 3 apps minimum required - to assign an app to each role type
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length < 3) {
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const uuid = () => Cypress._.random(0, 1e6)
|
||||
const name = uuid()
|
||||
if(i < 1){
|
||||
cy.createApp(name)
|
||||
} else {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||
cy.createAppFromScratch(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
// Navigate back to the user
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-SideNav").contains("Users").click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||
cy.wait(1000)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.contains("Choose an option")
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wait(1000)
|
||||
if (i == 0) {
|
||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||
}
|
||||
if (i == 1) {
|
||||
cy.get(".spectrum-Popover").contains("Power").click()
|
||||
}
|
||||
if (i == 2) {
|
||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Confirm roles exist within Configure roles table
|
||||
cy.wait(2000)
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.within(assginedRoles => {
|
||||
expect(assginedRoles).to.contain("Admin")
|
||||
expect(assginedRoles).to.contain("Power")
|
||||
expect(assginedRoles).to.contain("Basic")
|
||||
})
|
||||
})
|
||||
|
||||
it("should unassign role types", () => {
|
||||
// Set each app within Configure roles table to 'No Access'
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.its("length")
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(".spectrum-Picker").eq(1).click({ force: true })
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Popover").contains("No Access").click()
|
||||
})
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
}
|
||||
})
|
||||
// Confirm Configure roles table no longer has any apps in it
|
||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||
})
|
||||
// Navigate back to the user
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-SideNav").contains("Users").click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Table").contains("bbuser").click()
|
||||
cy.wait(1000)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(1)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.contains("Choose an option")
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wait(1000)
|
||||
if (i == 0) {
|
||||
cy.get(".spectrum-Popover").contains("Admin").click()
|
||||
}
|
||||
if (i == 1) {
|
||||
cy.get(".spectrum-Popover").contains("Power").click()
|
||||
}
|
||||
if (i == 2) {
|
||||
cy.get(".spectrum-Popover").contains("Basic").click()
|
||||
}
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Confirm roles exist within Configure roles table
|
||||
cy.wait(2000)
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.within(assginedRoles => {
|
||||
expect(assginedRoles).to.contain("Admin")
|
||||
expect(assginedRoles).to.contain("Power")
|
||||
expect(assginedRoles).to.contain("Basic")
|
||||
})
|
||||
})
|
||||
|
||||
it("should unassign role types", () => {
|
||||
// Set each app within Configure roles table to 'No Access'
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.its("length")
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".spectrum-Table")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-row")
|
||||
.eq(0)
|
||||
.find(".spectrum-Table-cell")
|
||||
.eq(0)
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get(".spectrum-Picker").eq(1).click({ force: true })
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Popover").contains("No Access").click()
|
||||
})
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Update role")
|
||||
.click({ force: true })
|
||||
cy.wait(1000)
|
||||
}
|
||||
})
|
||||
// Confirm Configure roles table no longer has any apps in it
|
||||
cy.get(".spectrum-Table").eq(0).contains("No rows found")
|
||||
})
|
||||
}
|
||||
|
||||
it("should enable Developer access", () => {
|
||||
// Enable Developer access
|
||||
|
|
|
@ -4,6 +4,8 @@ filterTests(['smoke', 'all'], () => {
|
|||
context("Create a View", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.deleteAllApps()
|
||||
|
||||
cy.createTestApp()
|
||||
cy.createTable("data")
|
||||
cy.addColumn("data", "group", "Text")
|
||||
|
|
|
@ -4,6 +4,7 @@ filterTests(["smoke", "all"], () => {
|
|||
context("REST Datasource Testing", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.deleteAllApps()
|
||||
cy.createTestApp()
|
||||
})
|
||||
|
||||
|
@ -36,10 +37,12 @@ filterTests(["smoke", "all"], () => {
|
|||
// createRestQuery confirms query creation
|
||||
cy.createRestQuery("GET", restUrl, "/breweries")
|
||||
// Confirm status code response within REST datasource
|
||||
cy.get(".spectrum-FieldLabel")
|
||||
.contains("Status")
|
||||
.children()
|
||||
cy.wait(1000)
|
||||
cy.get(".stats").within(() => {
|
||||
cy.get(".spectrum-FieldLabel")
|
||||
.eq(0)
|
||||
.should("contain", 200)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,7 +4,7 @@ filterTests(["smoke", "all"], () => {
|
|||
context("Query Level Transformers", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.deleteApp("Cypress Tests")
|
||||
cy.deleteAllApps()
|
||||
cy.createApp("Cypress Tests")
|
||||
})
|
||||
|
||||
|
|
|
@ -1,133 +1,133 @@
|
|||
import filterTests from "../support/filterTests"
|
||||
|
||||
filterTests(['all'], () => {
|
||||
context("Rename an App", () => {
|
||||
beforeEach(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
})
|
||||
context("Rename an App", () => {
|
||||
beforeEach(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
})
|
||||
|
||||
it("should rename an unpublished application", () => {
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, appRename)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(appRename, appName)
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, appRename)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
cy.applicationInAppTable(appRename)
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(appRename, appName)
|
||||
})
|
||||
|
||||
|
||||
xit("Should rename a published application", () => {
|
||||
// It is not possible to rename a published application
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Publish the app
|
||||
cy.get(".toprightnav")
|
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.within(() => {
|
||||
// Click publish again within the modal
|
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true})
|
||||
})
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, appRename, true)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
||||
// It is not possible to rename a published application
|
||||
const appName = "Cypress Tests"
|
||||
const appRename = "Cypress Renamed"
|
||||
// Publish the app
|
||||
cy.get(".toprightnav")
|
||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
.within(() => {
|
||||
// Click publish again within the modal
|
||||
cy.get(".spectrum-Button").contains("Publish").click({ force: true })
|
||||
})
|
||||
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, appRename, true)
|
||||
cy.get(".appTable").find(".wrapper").should("have.length", 1)
|
||||
cy.applicationInAppTable(appRename)
|
||||
})
|
||||
|
||||
it("Should try to rename an application to have no name", () => {
|
||||
const appName = "Cypress Tests"
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, " ", false, true)
|
||||
cy.wait(500)
|
||||
// Close modal and confirm name has not been changed
|
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(appName)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
const appName = "Cypress Tests"
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, " ", false, true)
|
||||
cy.wait(500)
|
||||
// Close modal and confirm name has not been changed
|
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.applicationInAppTable(appName)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
|
||||
})
|
||||
|
||||
xit("Should create two applications with the same name", () => {
|
||||
// It is not possible to have applications with the same name
|
||||
const appName = "Cypress Tests"
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Button").contains("Create app").click({force: true})
|
||||
cy.contains(/Start from scratch/).click()
|
||||
cy.get(".spectrum-Modal")
|
||||
// It is not possible to have applications with the same name
|
||||
const appName = "Cypress Tests"
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
||||
cy.contains(/Start from scratch/).click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
cy.get("input").eq(0).type(appName)
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true})
|
||||
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
||||
cy.get("input").eq(0).type(appName)
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({ force: true })
|
||||
cy.get(".error").should("have.text", "Another app with the same name already exists")
|
||||
})
|
||||
})
|
||||
|
||||
it("should validate application names", () => {
|
||||
// App name must be letters, numbers and spaces only
|
||||
// This test checks numbers and special characters specifically
|
||||
const appName = "Cypress Tests"
|
||||
const numberName = 12345
|
||||
const specialCharName = "£$%^"
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, numberName)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.searchForApplication(numberName)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, specialCharName)
|
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, appName)
|
||||
// App name must be letters, numbers and spaces only
|
||||
// This test checks numbers and special characters specifically
|
||||
const appName = "Cypress Tests"
|
||||
const numberName = 12345
|
||||
const specialCharName = "£$%^"
|
||||
cy.get(".home-logo").click()
|
||||
renameApp(appName, numberName)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
cy.applicationInAppTable(numberName)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, specialCharName)
|
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
||||
// Set app name back to Cypress Tests
|
||||
cy.reload()
|
||||
cy.wait(1000)
|
||||
renameApp(numberName, appName)
|
||||
})
|
||||
|
||||
const renameApp = (originalName, changedName, published, noName) => {
|
||||
cy.searchForApplication(originalName)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
|
||||
const renameApp = (originalName, changedName, published, noName) => {
|
||||
cy.applicationInAppTable(originalName)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.get(".appTable")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
// Check for when an app is published
|
||||
if (published == true){
|
||||
// Should not have Edit as option, will unpublish app
|
||||
cy.should("not.have.value", "Edit")
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||
if (val.length > 0) {
|
||||
cy.get(".appTable")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
})
|
||||
// Check for when an app is published
|
||||
if (published == true) {
|
||||
// Should not have Edit as option, will unpublish app
|
||||
cy.should("not.have.value", "Edit")
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||
}
|
||||
cy.contains("Edit").click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
if (noName == true) {
|
||||
cy.get("input").clear()
|
||||
cy.get(".spectrum-Dialog-grid").click()
|
||||
.contains("App name must be letters, numbers and spaces only")
|
||||
return cy
|
||||
}
|
||||
cy.contains("Edit").click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.within(() => {
|
||||
if (noName == true){
|
||||
cy.get("input").clear()
|
||||
cy.get(".spectrum-Dialog-grid").click()
|
||||
.contains("App name must be letters, numbers and spaces only")
|
||||
return cy
|
||||
}
|
||||
cy.get("input").clear()
|
||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({force: true})
|
||||
cy.wait(500)
|
||||
})
|
||||
}
|
||||
cy.get("input").clear()
|
||||
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -35,7 +35,9 @@ Cypress.Commands.add("login", () => {
|
|||
Cypress.Commands.add("createApp", name => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Button").contains("Create app").click({ force: true })
|
||||
|
||||
cy.get(`[data-cy="create-app-btn"]`).click({ force: true })
|
||||
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("input").eq(0).type(name).should("have.value", name).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
|
@ -51,10 +53,23 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.searchForApplication(name)
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click()
|
||||
const appId = val.reduce((acc, app) => {
|
||||
if (name === app.name) {
|
||||
acc = app.appId
|
||||
}
|
||||
return acc
|
||||
}, "")
|
||||
|
||||
if (appId == "") {
|
||||
return
|
||||
}
|
||||
|
||||
const appIdParsed = appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Menu").then($menu => {
|
||||
if ($menu.text().includes("Unpublish")) {
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
|
@ -80,22 +95,18 @@ Cypress.Commands.add("deleteAllApps", () => {
|
|||
.its("body")
|
||||
.then(val => {
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
cy.get(".spectrum-Heading")
|
||||
.eq(1)
|
||||
.then(app => {
|
||||
const name = app.text()
|
||||
cy.get(".title")
|
||||
.children()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
})
|
||||
cy.reload()
|
||||
})
|
||||
const appIdParsed = val[i].appId.split("_").pop()
|
||||
const actionEleId = `[data-cy=row_actions_${appIdParsed}]`
|
||||
cy.get(actionEleId).within(() => {
|
||||
cy.get(".spectrum-Icon").eq(0).click()
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(val[i].name)
|
||||
cy.get(".spectrum-Button--warning").click()
|
||||
})
|
||||
cy.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -190,9 +201,11 @@ Cypress.Commands.add("addRowMultiValue", values => {
|
|||
Cypress.Commands.add("createUser", email => {
|
||||
// quick hacky recorded way to create a user
|
||||
cy.contains("Users").click()
|
||||
cy.get(".spectrum-Button--primary").click()
|
||||
cy.get(`[data-cy="add-user"]`).click()
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.get(".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel").click()
|
||||
|
||||
//Onboarding type selector
|
||||
cy.get(
|
||||
":nth-child(2) > .spectrum-Form-itemField > .spectrum-Textfield > .spectrum-Textfield-input"
|
||||
)
|
||||
|
@ -247,7 +260,7 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
|
|||
cy.get("[aria-label=AddCircle]").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item").contains("Blank").click()
|
||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
|
@ -265,7 +278,7 @@ Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
|
|||
for (let i = 0; i < screenNames.length; i++) {
|
||||
cy.get(".item").contains(screenNames[i]).click()
|
||||
}
|
||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||
cy.wait(4000)
|
||||
})
|
||||
|
||||
|
@ -312,16 +325,37 @@ Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
|
|||
})
|
||||
})
|
||||
|
||||
//Filters visible with 1 or more
|
||||
Cypress.Commands.add("searchForApplication", appName => {
|
||||
cy.wait(1000)
|
||||
// Searches for the app
|
||||
cy.get(".filter").then(() => {
|
||||
cy.get(".spectrum-Textfield").within(() => {
|
||||
cy.get("input").eq(0).clear()
|
||||
cy.get("input").eq(0).type(appName)
|
||||
})
|
||||
})
|
||||
// Confirms app exists after search
|
||||
cy.get(".appTable").contains(appName)
|
||||
cy.applicationInAppTable(appName)
|
||||
})
|
||||
|
||||
//Assumes there are no others
|
||||
Cypress.Commands.add("applicationInAppTable", appName => {
|
||||
cy.get(".appTable").within(() => {
|
||||
cy.get(".title").contains(appName).should("exist")
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createAppFromScratch", appName => {
|
||||
cy.get(`[data-cy="create-app-btn"]`)
|
||||
.contains("Start from scratch")
|
||||
.click({ force: true })
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("input").eq(0).type(appName).should("have.value", appName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click()
|
||||
cy.wait(10000)
|
||||
})
|
||||
cy.createTable("Cypress Tests", true)
|
||||
})
|
||||
|
||||
Cypress.Commands.add("selectExternalDatasource", datasourceName => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.100",
|
||||
"version": "1.0.98-alpha.9",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.100",
|
||||
"@budibase/client": "^1.0.100",
|
||||
"@budibase/frontend-core": "^1.0.100",
|
||||
"@budibase/string-templates": "^1.0.100",
|
||||
"@budibase/bbui": "^1.0.98-alpha.9",
|
||||
"@budibase/client": "^1.0.98-alpha.9",
|
||||
"@budibase/frontend-core": "^1.0.98-alpha.9",
|
||||
"@budibase/string-templates": "^1.0.98-alpha.9",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { store } from "./index"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import {
|
||||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
findHBSBlocks,
|
||||
} from "@budibase/string-templates"
|
||||
|
||||
/**
|
||||
* Recursively searches for a specific component ID
|
||||
|
@ -161,3 +167,58 @@ export const getComponentSettings = componentType => {
|
|||
|
||||
return settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomises a components ID's, including all child component IDs, and also
|
||||
* updates all data bindings to still be valid.
|
||||
* This mutates the object in place.
|
||||
* @param component the component to randomise
|
||||
*/
|
||||
export const makeComponentUnique = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
// Replace component ID
|
||||
const oldId = component._id
|
||||
const newId = Helpers.uuid()
|
||||
component._id = newId
|
||||
|
||||
if (component._children?.length) {
|
||||
let children = JSON.stringify(component._children)
|
||||
|
||||
// Replace all instances of this ID in child HBS bindings
|
||||
children = children.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Replace all instances of this ID in child JS bindings
|
||||
const bindings = findHBSBlocks(children)
|
||||
bindings.forEach(binding => {
|
||||
// JSON.stringify will have escaped double quotes, so we need
|
||||
// to account for that
|
||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||
|
||||
// Check if this is a valid JS binding
|
||||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
|
||||
// Replace escaped double quotes
|
||||
newBinding = newBinding.replace(/"/g, '\\"')
|
||||
|
||||
// Insert new JS back into binding.
|
||||
// A single string replace here is better than a regex as
|
||||
// the binding contains special characters, and we only need
|
||||
// to replace a single instance.
|
||||
children = children.replace(binding, newBinding)
|
||||
}
|
||||
})
|
||||
|
||||
// Recurse on all children
|
||||
component._children = JSON.parse(children)
|
||||
component._children.forEach(makeComponentUnique)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
if (dataProviderSetting) {
|
||||
const settingValue = component[dataProviderSetting.key]
|
||||
const providerId = extractLiteralHandlebarsID(settingValue)
|
||||
const provider = findComponent(asset.props, providerId)
|
||||
const provider = findComponent(asset?.props, providerId)
|
||||
return getDatasourceForProvider(asset, provider)
|
||||
}
|
||||
|
||||
|
@ -393,18 +393,45 @@ const getUrlBindings = asset => {
|
|||
|
||||
/**
|
||||
* Gets all bindable properties exposed in a button actions flow up until
|
||||
* the specified action ID.
|
||||
* the specified action ID, as well as context provided for the action
|
||||
* setting as a whole by the component.
|
||||
*/
|
||||
export const getButtonContextBindings = (actions, actionId) => {
|
||||
export const getButtonContextBindings = (
|
||||
asset,
|
||||
componentId,
|
||||
settingKey,
|
||||
actions,
|
||||
actionId
|
||||
) => {
|
||||
let bindings = []
|
||||
|
||||
// Check if any context bindings are provided by the component for this
|
||||
// setting
|
||||
const component = findComponent(asset.props, componentId)
|
||||
const settings = getComponentSettings(component?._component)
|
||||
const eventSetting = settings.find(setting => setting.key === settingKey)
|
||||
if (!eventSetting) {
|
||||
return bindings
|
||||
}
|
||||
if (eventSetting.context?.length) {
|
||||
eventSetting.context.forEach(contextEntry => {
|
||||
bindings.push({
|
||||
readableBinding: contextEntry.label,
|
||||
runtimeBinding: `${makePropSafe("eventContext")}.${makePropSafe(
|
||||
contextEntry.key
|
||||
)}`,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Get the steps leading up to this value
|
||||
const index = actions?.findIndex(action => action.id === actionId)
|
||||
if (index == null || index === -1) {
|
||||
return []
|
||||
return bindings
|
||||
}
|
||||
const prevActions = actions.slice(0, index)
|
||||
|
||||
// Generate bindings for any steps which provide context
|
||||
let bindings = []
|
||||
prevActions.forEach((action, idx) => {
|
||||
const def = ActionDefinitions.actions.find(
|
||||
x => x.name === action["##eventHandlerType"]
|
||||
|
@ -418,6 +445,7 @@ export const getButtonContextBindings = (actions, actionId) => {
|
|||
})
|
||||
}
|
||||
})
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
|
@ -458,7 +486,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
// Determine the entity which backs this datasource.
|
||||
// "provider" datasources are those targeting another data provider
|
||||
if (type === "provider") {
|
||||
const component = findComponent(asset.props, datasource.providerId)
|
||||
const component = findComponent(asset?.props, datasource.providerId)
|
||||
const source = getDatasourceForProvider(asset, component)
|
||||
return getSchemaForDatasource(asset, source, options)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { findComponent } from "./componentUtils"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
@ -25,7 +25,17 @@ export const selectedComponent = derived(
|
|||
if (!$currentAsset || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
||||
return findComponent($currentAsset?.props, $store.selectedComponentId)
|
||||
}
|
||||
)
|
||||
|
||||
export const selectedComponentPath = derived(
|
||||
[store, currentAsset],
|
||||
([$store, $currentAsset]) => {
|
||||
return findComponentPath(
|
||||
$currentAsset?.props,
|
||||
$store.selectedComponentId
|
||||
).map(component => component._id)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ import {
|
|||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
getComponentSettings,
|
||||
makeComponentUnique,
|
||||
} from "../componentUtils"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { removeBindings } from "../dataBinding"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
apps: [],
|
||||
|
@ -45,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
customThemes: false,
|
||||
devicePreview: false,
|
||||
messagePassing: false,
|
||||
continueIfAction: false,
|
||||
},
|
||||
currentFrontEndType: "none",
|
||||
selectedScreenId: "",
|
||||
|
@ -400,11 +401,11 @@ export const getFrontendStore = () => {
|
|||
parentComponent = selected
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(asset.props, selected._id)
|
||||
parentComponent = findComponentParent(asset?.props, selected._id)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = asset.props
|
||||
parentComponent = asset?.props
|
||||
}
|
||||
|
||||
// Attach component
|
||||
|
@ -490,37 +491,22 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
}
|
||||
},
|
||||
paste: async (targetComponent, mode, preserveBindings = false) => {
|
||||
paste: async (targetComponent, mode) => {
|
||||
let promises = []
|
||||
store.update(state => {
|
||||
// Stop if we have nothing to paste
|
||||
if (!state.componentToPaste) {
|
||||
return state
|
||||
}
|
||||
|
||||
// defines if this is a copy or a cut
|
||||
const cut = state.componentToPaste.isCut
|
||||
|
||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
||||
if (!cut && !preserveBindings) {
|
||||
state.componentToPaste = removeBindings(state.componentToPaste, "")
|
||||
}
|
||||
|
||||
// Clone the component to paste
|
||||
// Retain the same ID if cutting as things may be referencing this component
|
||||
// Clone the component to paste and make unique if copying
|
||||
delete state.componentToPaste.isCut
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
const randomizeIds = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
component._id = Helpers.uuid()
|
||||
component._children?.forEach(randomizeIds)
|
||||
}
|
||||
randomizeIds(componentToPaste)
|
||||
makeComponentUnique(componentToPaste)
|
||||
}
|
||||
|
||||
if (mode === "inside") {
|
||||
|
|
|
@ -10,17 +10,18 @@ const allTemplates = tables => [
|
|||
]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, create) => () => {
|
||||
const screen = create()
|
||||
const createTemplateOverride = (frontendState, template) => () => {
|
||||
const screen = template.create()
|
||||
screen.name = screen.props._id
|
||||
screen.routing.route = screen.routing.route.toLowerCase()
|
||||
screen.template = template.id
|
||||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, tables) => {
|
||||
const enrichTemplate = template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template.create),
|
||||
create: createTemplateOverride(frontendState, template),
|
||||
})
|
||||
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
|
|
|
@ -2,9 +2,10 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
|
|||
|
||||
export const getThemeStore = () => {
|
||||
const themeElement = document.documentElement
|
||||
|
||||
const initialValue = {
|
||||
theme: "darkest",
|
||||
options: ["lightest", "light", "dark", "darkest"],
|
||||
options: ["lightest", "light", "dark", "darkest", "nord"],
|
||||
}
|
||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||
|
||||
|
@ -21,6 +22,7 @@ export const getThemeStore = () => {
|
|||
`spectrum--${option}`,
|
||||
option === state.theme
|
||||
)
|
||||
themeElement.classList.add("spectrum--darkest")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
<div class="title">
|
||||
<Tabs selected="Automations">
|
||||
<Tab title="Automations">
|
||||
<div class="tab-content-padding">
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</div>
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="add-button" data-cy="new-screen">
|
||||
|
@ -24,9 +22,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/introduction-to-automate"
|
||||
href="https://docs.budibase.com/docs/automation-steps"
|
||||
>
|
||||
<Icon name="InfoOutline" />
|
||||
<span>Learn about automations</span>
|
||||
|
|
|
@ -162,7 +162,7 @@
|
|||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
options={Object.keys(table.schema)}
|
||||
options={Object.keys(table?.schema || {})}
|
||||
/>
|
||||
{:else if value.customType === "filters"}
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Input, Select } from "@budibase/bbui"
|
||||
import { Input, Select, Button } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -62,9 +62,6 @@
|
|||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="add-field">
|
||||
<i class="ri-add-line" on:click={addField} />
|
||||
</div>
|
||||
<div class="spacer" />
|
||||
{#each fieldsArray as field}
|
||||
<div class="field">
|
||||
|
@ -88,6 +85,7 @@
|
|||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<Button quiet secondary icon="Add" on:click={addField}>Add field</Button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -103,52 +101,11 @@
|
|||
|
||||
.field {
|
||||
max-width: 100%;
|
||||
background-color: var(--grey-2);
|
||||
margin-bottom: var(--spacing-m);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--grey-4);
|
||||
display: grid;
|
||||
/*grid-template-rows: auto auto;
|
||||
grid-template-columns: auto;*/
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field :global(select) {
|
||||
padding: var(--spacing-xs) 2rem var(--spacing-m) var(--spacing-s) !important;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
.field :global(.pointer) {
|
||||
padding-bottom: var(--spacing-m) !important;
|
||||
color: var(--grey-2);
|
||||
}
|
||||
|
||||
.field :global(input) {
|
||||
padding: var(--spacing-m) var(--spacing-xl) var(--spacing-xs)
|
||||
var(--spacing-m);
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.remove-field {
|
||||
cursor: pointer;
|
||||
color: var(--grey-6);
|
||||
position: absolute;
|
||||
top: var(--spacing-m);
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.remove-field:hover {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.add-field {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.add-field > i {
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/steps/triggers"
|
||||
href="https://docs.budibase.com/docs/trigger"
|
||||
>
|
||||
<Icon name="InfoOutline" />
|
||||
<span>Learn about webhooks</span>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables } from "stores/backend"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
|
@ -321,6 +321,12 @@
|
|||
}
|
||||
return newError
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (primaryDisplay) {
|
||||
field.constraints.presence = { allowEmpty: false }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
|
|
@ -22,10 +22,11 @@
|
|||
const selected = $datasources.selected === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(datasource)
|
||||
const onlySource = $datasources.list.length === 1
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
open: selected || open || containsSelected,
|
||||
open: selected || open || containsSelected || onlySource,
|
||||
}
|
||||
})
|
||||
: []
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import ArrayRenderer from "components/common/renderers/ArrayRenderer.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||
|
||||
export let datasource
|
||||
export let save
|
||||
|
@ -160,6 +161,11 @@
|
|||
Fetch tables
|
||||
</Button>
|
||||
<Button cta icon="Add" on:click={createNewTable}>New table</Button>
|
||||
{#if integration.auth}
|
||||
{#if integration.auth.type === "google"}
|
||||
<GoogleButton {datasource} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Body>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
export let width = "100"
|
||||
export let height = "100"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="23 6 469 132"
|
||||
{width}
|
||||
{height}
|
||||
>
|
||||
<defs id="defs202">
|
||||
<linearGradient id="a" x1="-3.49%" x2="100.83%" y1="17.02%" y2="92.9%">
|
||||
<stop offset="0%" stop-color="#fff" stop-opacity=".1" id="stop192" />
|
||||
<stop offset="14%" stop-color="#fff" stop-opacity=".08" id="stop194" />
|
||||
<stop offset="61%" stop-color="#fff" stop-opacity=".02" id="stop196" />
|
||||
<stop offset="100%" stop-color="#fff" stop-opacity="0" id="stop198" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="b"
|
||||
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z"
|
||||
/>
|
||||
</defs>
|
||||
<g id="g305" transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
|
||||
<path
|
||||
fill="#ffa000"
|
||||
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
|
||||
id="path204"
|
||||
/>
|
||||
<path
|
||||
fill="#f57c00"
|
||||
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
|
||||
id="path206"
|
||||
/>
|
||||
<path
|
||||
fill="#ffca28"
|
||||
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
|
||||
id="path208"
|
||||
/>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.2"
|
||||
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
|
||||
id="path210"
|
||||
/>
|
||||
<path
|
||||
fill="#a52714"
|
||||
opacity="0.2"
|
||||
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
|
||||
id="path212"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="23 6 469 132"
|
||||
width="100"
|
||||
height="100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs202">
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="-3.49%"
|
||||
x2="100.83%"
|
||||
y1="17.02%"
|
||||
y2="92.9%">
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#fff"
|
||||
stop-opacity=".1"
|
||||
id="stop192" />
|
||||
<stop
|
||||
offset="14%"
|
||||
stop-color="#fff"
|
||||
stop-opacity=".08"
|
||||
id="stop194" />
|
||||
<stop
|
||||
offset="61%"
|
||||
stop-color="#fff"
|
||||
stop-opacity=".02"
|
||||
id="stop196" />
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#fff"
|
||||
stop-opacity="0"
|
||||
id="stop198" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="b"
|
||||
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z" />
|
||||
</defs>
|
||||
<g
|
||||
id="g305"
|
||||
transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
|
||||
<path
|
||||
fill="#ffa000"
|
||||
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
|
||||
id="path204" />
|
||||
<path
|
||||
fill="#f57c00"
|
||||
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
|
||||
id="path206" />
|
||||
<path
|
||||
fill="#ffca28"
|
||||
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
|
||||
id="path208" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.2"
|
||||
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
|
||||
id="path210" />
|
||||
<path
|
||||
fill="#a52714"
|
||||
opacity="0.2"
|
||||
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
|
||||
id="path212" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -12,6 +12,7 @@ import Rest from "./Rest.svelte"
|
|||
import Budibase from "./Budibase.svelte"
|
||||
import Oracle from "./Oracle.svelte"
|
||||
import GoogleSheets from "./GoogleSheets.svelte"
|
||||
import Firebase from "./Firebase.svelte"
|
||||
|
||||
export default {
|
||||
BUDIBASE: Budibase,
|
||||
|
@ -28,4 +29,5 @@ export default {
|
|||
REST: Rest,
|
||||
ORACLE: Oracle,
|
||||
GOOGLE_SHEETS: GoogleSheets,
|
||||
FIREBASE: Firebase,
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
Layout,
|
||||
Tabs,
|
||||
Tab,
|
||||
Input,
|
||||
Heading,
|
||||
TextArea,
|
||||
Dropzone,
|
||||
|
@ -98,15 +97,16 @@
|
|||
<Body size="XS"
|
||||
>Import your rest collection using one of the options below</Body
|
||||
>
|
||||
<Tabs selected="Link">
|
||||
<Tab title="Link">
|
||||
<Tabs selected="File">
|
||||
<!-- Commenting until nginx csp issue resolved -->
|
||||
<!-- <Tab title="Link">
|
||||
<Input
|
||||
bind:value={$data.url}
|
||||
on:change={() => (lastTouched = "url")}
|
||||
label="Enter a URL"
|
||||
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
||||
/>
|
||||
</Tab>
|
||||
</Tab> -->
|
||||
<Tab title="File">
|
||||
<Dropzone
|
||||
gallery={false}
|
||||
|
@ -115,7 +115,14 @@
|
|||
$data.file = e.detail?.[0]
|
||||
lastTouched = "file"
|
||||
}}
|
||||
fileTags={["OpenAPI 2.0", "Swagger 2.0", "cURL", "YAML", "JSON"]}
|
||||
fileTags={[
|
||||
"OpenAPI 3.0",
|
||||
"OpenAPI 2.0",
|
||||
"Swagger 2.0",
|
||||
"cURL",
|
||||
"YAML",
|
||||
"JSON",
|
||||
]}
|
||||
maximum={1}
|
||||
/>
|
||||
</Tab>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
|
||||
export let icon
|
||||
export let withArrow = false
|
||||
|
@ -14,29 +14,46 @@
|
|||
export let iconText
|
||||
export let iconColor
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function onIconClick(event) {
|
||||
event.stopPropagation()
|
||||
let contentRef
|
||||
$: selected && contentRef && scrollToView()
|
||||
|
||||
const onClick = () => {
|
||||
scrollToView()
|
||||
dispatch("click")
|
||||
}
|
||||
|
||||
const onIconClick = e => {
|
||||
e.stopPropagation()
|
||||
dispatch("iconClick")
|
||||
}
|
||||
|
||||
const scrollToView = () => {
|
||||
if (!scrollApi || !contentRef) {
|
||||
return
|
||||
}
|
||||
const bounds = contentRef.getBoundingClientRect()
|
||||
scrollApi.scrollTo(bounds)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="nav-item"
|
||||
class:border
|
||||
class:selected
|
||||
style={`padding-left: ${indentLevel * 14}px`}
|
||||
style={`padding-left: ${20 + indentLevel * 14}px`}
|
||||
{draggable}
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
on:click
|
||||
on:click={onClick}
|
||||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
{#if withArrow}
|
||||
<div class:opened class="icon arrow" on:click={onIconClick}>
|
||||
<Icon size="S" name="ChevronRight" />
|
||||
|
@ -64,11 +81,16 @@
|
|||
|
||||
<style>
|
||||
.nav-item {
|
||||
border-radius: var(--border-radius-s);
|
||||
cursor: pointer;
|
||||
color: var(--grey-7);
|
||||
transition: background-color
|
||||
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
||||
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
|
||||
height: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--grey-2);
|
||||
|
@ -81,14 +103,14 @@
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--spacing-s);
|
||||
height: 32px;
|
||||
.nav-item-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -111,12 +133,13 @@
|
|||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
font-weight: 600;
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
@ -125,9 +148,9 @@
|
|||
height: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
margin-left: var(--spacing-s);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.iconText {
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
<script>
|
||||
export let backgroundColour
|
||||
export let imageSrc
|
||||
export let name
|
||||
export let icon
|
||||
export let overlayEnabled = true
|
||||
|
||||
let imageError = false
|
||||
let imageLoaded = false
|
||||
|
||||
const imageRenderError = () => {
|
||||
imageError = true
|
||||
}
|
||||
|
||||
const imageLoadSuccess = () => {
|
||||
imageLoaded = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-card" style="background-color:{backgroundColour};">
|
||||
<div class="template-thumbnail card-body">
|
||||
<img
|
||||
alt={name}
|
||||
src={imageSrc}
|
||||
on:error={imageRenderError}
|
||||
on:load={imageLoadSuccess}
|
||||
class={`${imageLoaded ? "loaded" : ""}`}
|
||||
/>
|
||||
<div style={`display:${imageError ? "block" : "none"}`}>
|
||||
<svg
|
||||
width="26px"
|
||||
height="26px"
|
||||
class="spectrum-Icon"
|
||||
style="color: white"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class={overlayEnabled ? "template-thumbnail-action-overlay" : ""}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-thumbnail-text">
|
||||
<div>{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.template-thumbnail {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.template-card:hover .template-thumbnail-action-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.template-thumbnail-action-overlay {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
opacity: 0;
|
||||
transition: opacity var(--spectrum-global-animation-duration-100) ease;
|
||||
border-top-right-radius: inherit;
|
||||
border-top-left-radius: inherit;
|
||||
}
|
||||
|
||||
.template-thumbnail-text {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
color: var(
|
||||
--spectrum-heading-xs-text-color,
|
||||
var(--spectrum-alias-heading-text-color)
|
||||
);
|
||||
background-color: var(--spectrum-global-color-gray-50);
|
||||
}
|
||||
|
||||
.template-thumbnail-text > div {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
border-radius: var(--border-radius-s);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
overflow: hidden;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.template-card > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.template-card img.loaded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-card img {
|
||||
display: none;
|
||||
max-width: 100%;
|
||||
border-radius: var(--border-radius-s) 0px var(--border-radius-s) 0px;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding-left: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,152 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Detail,
|
||||
Heading,
|
||||
Button,
|
||||
Modal,
|
||||
ActionGroup,
|
||||
ActionButton,
|
||||
} from "@budibase/bbui"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
|
||||
export let templates
|
||||
|
||||
let selectedTemplateCategory
|
||||
let creationModal
|
||||
let template
|
||||
|
||||
const groupTemplatesByCategory = (templates, categoryFilter) => {
|
||||
let grouped = templates.reduce((acc, template) => {
|
||||
if (
|
||||
typeof categoryFilter === "string" &&
|
||||
[categoryFilter].indexOf(template.category) < 0
|
||||
) {
|
||||
return acc
|
||||
}
|
||||
|
||||
acc[template.category] = !acc[template.category]
|
||||
? []
|
||||
: acc[template.category]
|
||||
acc[template.category].push(template)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
return grouped
|
||||
}
|
||||
|
||||
$: filteredTemplates = groupTemplatesByCategory(
|
||||
templates,
|
||||
selectedTemplateCategory
|
||||
)
|
||||
|
||||
$: filteredTemplateCategories = filteredTemplates
|
||||
? Object.keys(filteredTemplates).sort()
|
||||
: []
|
||||
|
||||
$: templateCategories = templates
|
||||
? Object.keys(groupTemplatesByCategory(templates)).sort()
|
||||
: []
|
||||
|
||||
const stopAppCreation = () => {
|
||||
template = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="template-header">
|
||||
<Layout noPadding gap="S">
|
||||
<Heading size="S">Templates</Heading>
|
||||
<div class="template-category-filters spectrum-ActionGroup">
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
selected={!selectedTemplateCategory}
|
||||
on:click={() => {
|
||||
selectedTemplateCategory = null
|
||||
}}
|
||||
>
|
||||
All
|
||||
</ActionButton>
|
||||
{#each templateCategories as templateCategoryKey}
|
||||
<ActionButton
|
||||
dataCy={templateCategoryKey}
|
||||
selected={templateCategoryKey == selectedTemplateCategory}
|
||||
on:click={() => {
|
||||
selectedTemplateCategory = templateCategoryKey
|
||||
}}
|
||||
>
|
||||
{templateCategoryKey}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</ActionGroup>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<div class="template-categories">
|
||||
<Layout gap="XL" noPadding>
|
||||
{#each filteredTemplateCategories as templateCategoryKey}
|
||||
<div class="template-category" data-cy={templateCategoryKey}>
|
||||
<Detail size="M">{templateCategoryKey}</Detail>
|
||||
<div class="template-grid">
|
||||
{#each filteredTemplates[templateCategoryKey] as templateEntry}
|
||||
<TemplateCard
|
||||
name={templateEntry.name}
|
||||
imageSrc={templateEntry.image}
|
||||
backgroundColour={templateEntry.background}
|
||||
icon={templateEntry.icon}
|
||||
>
|
||||
<Button
|
||||
cta
|
||||
on:click={() => {
|
||||
template = templateEntry
|
||||
creationModal.show()
|
||||
}}
|
||||
>
|
||||
Use template
|
||||
</Button>
|
||||
<a
|
||||
href={templateEntry.url}
|
||||
target="_blank"
|
||||
class="overlay-preview-link spectrum-Button spectrum-Button--sizeM spectrum-Button--secondary"
|
||||
on:click|stopPropagation
|
||||
>
|
||||
Details
|
||||
</a>
|
||||
</TemplateCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
padding={false}
|
||||
width="600px"
|
||||
on:hide={stopAppCreation}
|
||||
>
|
||||
<CreateAppModal {template} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.template-grid {
|
||||
padding-top: 10px;
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-xl);
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
}
|
||||
|
||||
a:hover.spectrum-Button.spectrum-Button--secondary.overlay-preview-link {
|
||||
background-color: #c8c8c8;
|
||||
border-color: #c8c8c8;
|
||||
color: #505050;
|
||||
}
|
||||
|
||||
a.spectrum-Button--secondary.overlay-preview-link {
|
||||
margin-top: 20px;
|
||||
border-color: #c8c8c8;
|
||||
color: #c8c8c8;
|
||||
}
|
||||
</style>
|
|
@ -238,6 +238,7 @@
|
|||
border: var(--border-light);
|
||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||
border-color 130ms ease-in-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
li:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
const enrichBindings = bindings => {
|
||||
return bindings?.map(binding => ({
|
||||
...binding,
|
||||
readableBinding: binding.label || binding.readableBinding,
|
||||
runtimeBinding: binding.path || binding.runtimeBinding,
|
||||
readableBinding: binding.readableBinding || binding.label,
|
||||
runtimeBinding: binding.runtimeBinding || binding.path,
|
||||
}))
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/steps/triggers"
|
||||
href="https://docs.budibase.com/docs/trigger"
|
||||
>
|
||||
<i class="ri-information-line" />
|
||||
<span>Learn about webhooks</span>
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
customTheme: $store.customTheme,
|
||||
previewDevice: $store.previewDevice,
|
||||
messagePassing: $store.clientFeatures.messagePassing,
|
||||
isBudibaseEvent: true
|
||||
}
|
||||
$: json = JSON.stringify(previewData)
|
||||
|
||||
|
@ -160,6 +161,11 @@
|
|||
await store.actions.components.updateProp(data.prop, data.value)
|
||||
} else if (type === "delete-component" && data.id) {
|
||||
confirmDeleteComponent(data.id)
|
||||
} else if (type === "duplicate-component" && data.id) {
|
||||
const rootComponent = get(currentAsset).props
|
||||
const component = findComponent(rootComponent, data.id)
|
||||
store.actions.components.copy(component)
|
||||
await store.actions.components.paste(component)
|
||||
} else if (type === "preview-loaded") {
|
||||
// Wait for this event to show the client library if intelligent
|
||||
// loading is supported
|
||||
|
|
|
@ -82,7 +82,8 @@
|
|||
"link",
|
||||
"icon",
|
||||
"embed",
|
||||
"markdownviewer"
|
||||
"markdownviewer",
|
||||
"embeddedmap"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -52,7 +52,7 @@ export default `
|
|||
console.error("Client received invalid JSON")
|
||||
// Ignore
|
||||
}
|
||||
if (!parsed) {
|
||||
if (!parsed || !parsed.isBudibaseEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
const moveUpComponent = () => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
const parent = findComponentParent(asset?.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
|||
|
||||
const moveDownComponent = () => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
const parent = findComponentParent(asset?.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
|
@ -61,7 +61,7 @@
|
|||
|
||||
const duplicateComponent = () => {
|
||||
storeComponentForCopy(false)
|
||||
pasteComponent("below", true)
|
||||
pasteComponent("below")
|
||||
}
|
||||
|
||||
const deleteComponent = async () => {
|
||||
|
@ -73,14 +73,12 @@
|
|||
}
|
||||
|
||||
const storeComponentForCopy = (cut = false) => {
|
||||
// lives in store - also used by drag drop
|
||||
store.actions.components.copy(component, cut)
|
||||
}
|
||||
|
||||
const pasteComponent = (mode, preserveBindings = false) => {
|
||||
const pasteComponent = mode => {
|
||||
try {
|
||||
// lives in store - also used by drag drop
|
||||
store.actions.components.paste(component, mode, preserveBindings)
|
||||
store.actions.components.paste(component, mode)
|
||||
} catch (error) {
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
|
@ -140,3 +138,10 @@
|
|||
onOk={deleteComponent}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { selectedComponentPath } from "builderStore"
|
||||
|
||||
export let components = []
|
||||
export let currentComponent
|
||||
|
@ -71,10 +72,20 @@
|
|||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
||||
if (!component?._children?.length) {
|
||||
return false
|
||||
}
|
||||
if (selectedComponentPath.includes(component._id)) {
|
||||
return true
|
||||
}
|
||||
return !closedNodes[component._id]
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each components as component, index (component._id)}
|
||||
{#each components || [] as component, index (component._id)}
|
||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||
<div
|
||||
|
@ -97,12 +108,12 @@
|
|||
withArrow
|
||||
indentLevel={level + 1}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
opened={!closedNodes[component._id] && component?._children?.length}
|
||||
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
>
|
||||
<ComponentDropdownMenu {component} />
|
||||
</NavItem>
|
||||
|
||||
{#if component._children && !closedNodes[component._id]}
|
||||
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
<svelte:self
|
||||
components={component._children}
|
||||
{currentComponent}
|
||||
|
@ -133,6 +144,10 @@
|
|||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.drop-item {
|
||||
border-radius: var(--border-radius-m);
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={"Are you sure you wish to delete this layout?"}
|
||||
okText="Delete Layout"
|
||||
okText="Delete layout"
|
||||
onOk={deleteLayout}
|
||||
/>
|
||||
|
||||
|
@ -65,3 +65,10 @@
|
|||
<Input thin type="text" label="Name" bind:value={name} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let path
|
||||
export let screens
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
const deleteScreens = async () => {
|
||||
if (!screens?.length) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
for (let { id } of screens) {
|
||||
// We have to fetch the screen to be deleted immediately before deleting
|
||||
// as otherwise we're very likely to 409
|
||||
const screen = get(store).screens.find(screen => screen._id === id)
|
||||
if (!screen) {
|
||||
continue
|
||||
}
|
||||
await store.actions.screens.delete(screen)
|
||||
}
|
||||
notifications.success("Screens deleted successfully")
|
||||
$goto("../")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting screens")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
||||
Delete all screens
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
okText="Delete screens"
|
||||
onOk={deleteScreens}
|
||||
>
|
||||
<Layout noPadding gap="S">
|
||||
<div>
|
||||
Are you sure you want to delete all screens under the <b>{path}</b> route?
|
||||
</div>
|
||||
<div>The following screens will be deleted:</div>
|
||||
<div class="to-delete">
|
||||
{#each screens as screen}
|
||||
<div>{screen.route}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.to-delete {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding-left: var(--spacing-xl);
|
||||
}
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -8,6 +8,7 @@
|
|||
import instantiateStore from "./dragDropStore"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
|
@ -28,6 +29,7 @@
|
|||
export let border
|
||||
|
||||
let routeManuallyOpened = false
|
||||
|
||||
$: selectedScreen = $currentAsset
|
||||
$: allScreens = getAllScreens(route)
|
||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
||||
|
@ -73,14 +75,17 @@
|
|||
opened={routeOpened}
|
||||
{border}
|
||||
withArrow={route.subpaths}
|
||||
/>
|
||||
>
|
||||
<PathDropdownMenu screens={allScreens} {path} />
|
||||
</NavItem>
|
||||
|
||||
{#if routeOpened}
|
||||
{#each filteredScreens as screen (screen.id)}
|
||||
<NavItem
|
||||
icon="WebPage"
|
||||
indentLevel={indent || 1}
|
||||
selected={$store.selectedScreenId === screen.id}
|
||||
selected={$store.selectedScreenId === screen.id &&
|
||||
$store.currentView === "detail"}
|
||||
opened={$store.selectedScreenId === screen.id}
|
||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
||||
withArrow={route.subpaths}
|
||||
|
|
|
@ -2,14 +2,57 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { store, allScreens } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Modal,
|
||||
Helpers,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ScreenDetailsModal from "../ScreenDetailsModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||
|
||||
export let screenId
|
||||
|
||||
let confirmDeleteDialog
|
||||
let screenDetailsModal
|
||||
|
||||
$: screen = $allScreens.find(screen => screen._id === screenId)
|
||||
|
||||
const duplicateScreen = () => {
|
||||
screenDetailsModal.show()
|
||||
}
|
||||
|
||||
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
|
||||
// Create a dupe and ensure it is unique
|
||||
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||
delete duplicateScreen._id
|
||||
delete duplicateScreen._rev
|
||||
makeComponentUnique(duplicateScreen.props)
|
||||
|
||||
// Attach the new name and URL
|
||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||
duplicateScreen.props._instanceName = screenName
|
||||
|
||||
try {
|
||||
// Create the screen
|
||||
await store.actions.screens.save(duplicateScreen)
|
||||
|
||||
// Analytics
|
||||
if (screen.template) {
|
||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||
template: "createFromScratch",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error duplicating screen")
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteScreen = async () => {
|
||||
try {
|
||||
await store.actions.screens.delete(screen)
|
||||
|
@ -19,12 +62,28 @@
|
|||
notifications.error("Error deleting screen")
|
||||
}
|
||||
}
|
||||
|
||||
const pasteComponent = mode => {
|
||||
try {
|
||||
store.actions.components.paste(screen?.props, mode)
|
||||
} catch (error) {
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
|
||||
<MenuItem
|
||||
icon="ShowOneLayer"
|
||||
on:click={() => pasteComponent("inside")}
|
||||
disabled={!$store.componentToPaste}
|
||||
>
|
||||
Paste inside
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
|
@ -32,6 +91,22 @@
|
|||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={"Are you sure you wish to delete this screen?"}
|
||||
okText="Delete Screen"
|
||||
okText="Delete screen"
|
||||
onOk={deleteScreen}
|
||||
/>
|
||||
|
||||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
onConfirm={createDuplicateScreen}
|
||||
screenName={screen?.props._instanceName}
|
||||
screenUrl={screen?.routing.route}
|
||||
confirmText="Duplicate"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,11 +55,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="root" class:has-screens={!!paths?.length}>
|
||||
{#each paths as path, idx (path)}
|
||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
||||
{/each}
|
||||
|
||||
{#if !paths.length}
|
||||
<div class="empty">
|
||||
There aren't any screens configured with this access role.
|
||||
|
@ -68,9 +67,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.root.has-screens {
|
||||
min-width: max-content;
|
||||
}
|
||||
div.empty {
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--grey-5);
|
||||
padding-top: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import {
|
||||
store,
|
||||
|
@ -18,11 +18,63 @@
|
|||
Search,
|
||||
Tabs,
|
||||
Tab,
|
||||
Layout as BBUILayout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let showModal
|
||||
|
||||
let scrollRef
|
||||
|
||||
const scrollTo = bounds => {
|
||||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
const sidebarWidth = 259
|
||||
const navItemHeight = 32
|
||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||
|
||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||
let newOffsets = {}
|
||||
|
||||
// Calculate left offset
|
||||
const offsetX = bounds.left + bounds.width + scrollLeft + 20
|
||||
if (offsetX > sidebarWidth) {
|
||||
newOffsets.left = offsetX - sidebarWidth
|
||||
} else {
|
||||
newOffsets.left = 0
|
||||
}
|
||||
if (newOffsets.left === scrollLeft) {
|
||||
delete newOffsets.left
|
||||
}
|
||||
|
||||
// Calculate top offset
|
||||
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
||||
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
||||
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
||||
} else if (offsetY < scrollTop + navItemHeight) {
|
||||
newOffsets.top = offsetY - navItemHeight
|
||||
} else {
|
||||
delete newOffsets.top
|
||||
}
|
||||
|
||||
// Skip if offset is unchanged
|
||||
if (newOffsets.left == null && newOffsets.top == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Smoothly scroll to the offset
|
||||
scrollRef.scroll({
|
||||
...newOffsets,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
|
||||
setContext("scroll", {
|
||||
scrollTo,
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Screens",
|
||||
|
@ -79,7 +131,7 @@
|
|||
<Tabs {selected} on:select={navigate}>
|
||||
<Tab title="Screens">
|
||||
<div class="tab-content-padding">
|
||||
<div class="role-select">
|
||||
<BBUILayout noPadding gap="XS">
|
||||
<Select
|
||||
on:change={updateAccessRole}
|
||||
value={$selectedAccessRole}
|
||||
|
@ -93,17 +145,24 @@
|
|||
label="Search Screens"
|
||||
bind:value={$screenSearchString}
|
||||
/>
|
||||
</div>
|
||||
<div class="nav-items-container">
|
||||
</BBUILayout>
|
||||
<div class="nav-items-container" bind:this={scrollRef}>
|
||||
<ComponentNavigationTree />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Layouts">
|
||||
<div class="tab-content-padding">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
<div
|
||||
class="nav-items-container nav-items-container--layouts"
|
||||
bind:this={scrollRef}
|
||||
>
|
||||
<div class="layouts-container">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Modal bind:this={newLayoutModal}>
|
||||
<NewLayoutModal />
|
||||
</Modal>
|
||||
|
@ -126,23 +185,45 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.title :global(.spectrum-Tabs-content),
|
||||
.title :global(.spectrum-Tabs-content > div),
|
||||
.title :global(.spectrum-Tabs-content > div > div) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.role-select {
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin-bottom: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
.nav-items-container {
|
||||
border-top: var(--border-light);
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
padding: var(--spacing-m) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-items-container--layouts {
|
||||
border-top: none;
|
||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
||||
}
|
||||
|
||||
.layouts-container {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,39 +10,19 @@
|
|||
ProgressCircle,
|
||||
} from "@budibase/bbui"
|
||||
import getTemplates from "builderStore/store/screenTemplates"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let chooseModal
|
||||
export let save
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
export let showProgressCircle = false
|
||||
|
||||
let selectedScreens = []
|
||||
|
||||
const blankScreen = "createFromScratch"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function setScreens() {
|
||||
dispatch("save", {
|
||||
screens: selectedScreens,
|
||||
})
|
||||
}
|
||||
let selectedScreens = []
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
|
||||
$: blankSelected = selectedScreens?.length === 1
|
||||
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
|
||||
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
|
||||
const confirm = async () => {
|
||||
if (autoSelected) {
|
||||
setScreens()
|
||||
await save()
|
||||
} else {
|
||||
setScreens()
|
||||
chooseModal(1)
|
||||
}
|
||||
}
|
||||
const toggleScreenSelection = table => {
|
||||
if (selectedScreens.find(s => s.table === table.name)) {
|
||||
selectedScreens = selectedScreens.filter(
|
||||
|
@ -56,25 +36,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
selectedScreens = []
|
||||
})
|
||||
const confirmScreenSelection = async () => {
|
||||
await onConfirm(selectedScreens)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ModalContent
|
||||
title="Add screens"
|
||||
confirmText="Add Screens"
|
||||
confirmText="Add screens"
|
||||
cancelText="Cancel"
|
||||
onConfirm={() => confirm()}
|
||||
onConfirm={confirmScreenSelection}
|
||||
{onCancel}
|
||||
disabled={!selectedScreens.length}
|
||||
size="L"
|
||||
>
|
||||
<Body size="S"
|
||||
>Please select the screens you would like to add to your application.
|
||||
Autogenerated screens come with CRUD functionality.</Body
|
||||
>
|
||||
|
||||
<Body size="S">
|
||||
Please select the screens you would like to add to your application.
|
||||
Autogenerated screens come with CRUD functionality.
|
||||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
<Detail size="S">Blank screen</Detail>
|
||||
<div
|
||||
|
|
|
@ -2,58 +2,62 @@
|
|||
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { selectedAccessRole, allScreens } from "builderStore"
|
||||
import { onDestroy } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let screenName
|
||||
export let url
|
||||
export let chooseModal
|
||||
export let save
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
export let showProgressCircle = false
|
||||
export let screenName
|
||||
export let screenUrl
|
||||
export let confirmText = "Continue"
|
||||
|
||||
let routeError
|
||||
let roleId = $selectedAccessRole || "BASIC"
|
||||
let touched = false
|
||||
|
||||
const routeChanged = event => {
|
||||
if (!event.detail.startsWith("/")) {
|
||||
url = "/" + event.detail
|
||||
screenUrl = "/" + event.detail
|
||||
}
|
||||
url = sanitizeUrl(url)
|
||||
|
||||
if (routeExists(url, roleId)) {
|
||||
touched = true
|
||||
screenUrl = sanitizeUrl(screenUrl)
|
||||
if (routeExists(screenUrl)) {
|
||||
routeError = "This URL is already taken for this access role"
|
||||
} else {
|
||||
routeError = ""
|
||||
routeError = null
|
||||
}
|
||||
}
|
||||
|
||||
const routeExists = (url, roleId) => {
|
||||
return $allScreens.some(
|
||||
const routeExists = url => {
|
||||
const roleId = get(selectedAccessRole) || "BASIC"
|
||||
return get(allScreens).some(
|
||||
screen =>
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||
screen.routing.roleId === roleId
|
||||
)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
screenName = ""
|
||||
url = ""
|
||||
})
|
||||
const confirmScreenDetails = async () => {
|
||||
await onConfirm({
|
||||
screenName,
|
||||
screenUrl,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
size="M"
|
||||
title={"Enter details"}
|
||||
confirmText={"Continue"}
|
||||
onCancel={() => chooseModal(0)}
|
||||
onConfirm={() => save()}
|
||||
{confirmText}
|
||||
onConfirm={confirmScreenDetails}
|
||||
{onCancel}
|
||||
cancelText={"Back"}
|
||||
disabled={!screenName || !url || routeError}
|
||||
disabled={!screenName || !screenUrl || routeError || !touched}
|
||||
>
|
||||
<Input label="Name" bind:value={screenName} />
|
||||
<Input
|
||||
label="URL"
|
||||
error={routeError}
|
||||
bind:value={url}
|
||||
bind:value={screenUrl}
|
||||
on:change={routeChanged}
|
||||
/>
|
||||
<div slot="footer">
|
||||
|
|
|
@ -3,141 +3,133 @@
|
|||
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { Modal, notifications } from "@budibase/bbui"
|
||||
import { store, selectedAccessRole, allScreens } from "builderStore"
|
||||
import { store, selectedAccessRole } from "builderStore"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
let newScreenModal
|
||||
let navigationSelectionModal
|
||||
let screenDetailsModal
|
||||
let screenName = ""
|
||||
let url = ""
|
||||
let selectedScreens = []
|
||||
let pendingScreen
|
||||
let showProgressCircle = false
|
||||
let routeError
|
||||
let createdScreens = []
|
||||
|
||||
$: roleId = $selectedAccessRole || "BASIC"
|
||||
// Modal refs
|
||||
let newScreenModal
|
||||
let screenDetailsModal
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
// External handler to show the screen wizard
|
||||
export const showModal = () => {
|
||||
newScreenModal.show()
|
||||
|
||||
const save = async () => {
|
||||
showProgressCircle = true
|
||||
try {
|
||||
await createScreens()
|
||||
for (let screen of createdScreens) {
|
||||
await saveScreens(screen)
|
||||
}
|
||||
await store.actions.routing.fetch()
|
||||
selectedScreens = []
|
||||
createdScreens = []
|
||||
screenName = ""
|
||||
url = ""
|
||||
} catch (error) {
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
// Reset state when showing modal again
|
||||
pendingScreen = null
|
||||
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("/")
|
||||
// Creates an array of screens, checking and sanitising their URLs
|
||||
const createScreens = async screens => {
|
||||
if (!screens?.length) {
|
||||
return
|
||||
}
|
||||
showProgressCircle = true
|
||||
|
||||
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 = ""
|
||||
try {
|
||||
for (let screen of screens) {
|
||||
// Check we aren't clashing with an existing URL
|
||||
if (hasExistingUrl(screen.routing.route)) {
|
||||
let suffix = 2
|
||||
let candidateUrl = makeCandidateUrl(screen, suffix)
|
||||
while (hasExistingUrl(candidateUrl)) {
|
||||
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
||||
}
|
||||
screen.routing.route = candidateUrl
|
||||
}
|
||||
}
|
||||
|
||||
if (routeError) return false
|
||||
// Sanitise URL
|
||||
screen.routing.route = sanitizeUrl(screen.routing.route)
|
||||
|
||||
if (screenName) {
|
||||
draftScreen.props._instanceName = screenName
|
||||
}
|
||||
// Use the currently selected role
|
||||
screen.routing.roleId = get(selectedAccessRole) || "BASIC"
|
||||
|
||||
draftScreen.routing.route = route
|
||||
draftScreen.routing.roleId = roleId
|
||||
// Create the screen
|
||||
await store.actions.screens.save(screen)
|
||||
|
||||
await store.actions.screens.save(draftScreen)
|
||||
if (draftScreen.props._instanceName.endsWith("List")) {
|
||||
try {
|
||||
// Analytics
|
||||
if (screen.template) {
|
||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||
template: screen.template,
|
||||
})
|
||||
}
|
||||
|
||||
// Add link in layout for list screens
|
||||
if (screen.props._instanceName.endsWith("List")) {
|
||||
await store.actions.components.links.save(
|
||||
draftScreen.routing.route,
|
||||
draftScreen.routing.route.split("/")[1]
|
||||
screen.routing.route,
|
||||
screen.routing.route.split("/")[1]
|
||||
)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating link to screen")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
|
||||
showProgressCircle = false
|
||||
}
|
||||
|
||||
// Checks if any screens exist in the store with the given route and
|
||||
// currently selected role
|
||||
const hasExistingUrl = url => {
|
||||
const roleId = get(selectedAccessRole) || "BASIC"
|
||||
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
|
||||
return !!screens.find(s => s.routing?.route === url)
|
||||
}
|
||||
|
||||
// Constructs a candidate URL for a new screen, suffixing the base of the
|
||||
// screen's URL with a given suffix.
|
||||
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||
const makeCandidateUrl = (screen, suffix) => {
|
||||
let url = screen.routing?.route || ""
|
||||
if (url.startsWith("/")) {
|
||||
url = url.slice(1)
|
||||
}
|
||||
if (!url.includes("/")) {
|
||||
return `/${url}-${suffix}`
|
||||
} else {
|
||||
const split = url.split("/")
|
||||
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Handler for NewScreenModal
|
||||
const confirmScreenSelection = async templates => {
|
||||
// Handle template selection
|
||||
if (templates?.length > 1) {
|
||||
// Autoscreens, so create immediately
|
||||
const screens = templates.map(template => template.create())
|
||||
await createScreens(screens)
|
||||
} else {
|
||||
// Empty screen, so proceed to the next modal
|
||||
pendingScreen = templates[0].create()
|
||||
screenDetailsModal.show()
|
||||
} else if (index === 2) {
|
||||
navigationSelectionModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for ScreenDetailsModal
|
||||
const confirmScreenDetails = async ({ screenName, screenUrl }) => {
|
||||
if (!pendingScreen) {
|
||||
return
|
||||
}
|
||||
pendingScreen.props._instanceName = screenName
|
||||
pendingScreen.routing.route = screenUrl
|
||||
await createScreens([pendingScreen])
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={newScreenModal}>
|
||||
<NewScreenModal
|
||||
on:save={setScreens}
|
||||
{showProgressCircle}
|
||||
{save}
|
||||
{chooseModal}
|
||||
/>
|
||||
<NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
bind:screenName
|
||||
bind:url
|
||||
{showProgressCircle}
|
||||
{save}
|
||||
{chooseModal}
|
||||
onConfirm={confirmScreenDetails}
|
||||
onCancel={() => newScreenModal.show()}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
const customSections = settings.filter(setting => setting.section)
|
||||
return [
|
||||
{
|
||||
name: "General",
|
||||
name: componentDefinition?.name || "General",
|
||||
info: componentDefinition?.info,
|
||||
settings: generalSettings,
|
||||
},
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
import { getAvailableActions } from "./index"
|
||||
import { generate } from "shortid"
|
||||
import { getButtonContextBindings } from "builderStore/dataBinding"
|
||||
import { currentAsset, store } from "builderStore"
|
||||
|
||||
const flipDurationMs = 150
|
||||
const EVENT_TYPE_KEY = "##eventHandlerType"
|
||||
const actionTypes = getAvailableActions()
|
||||
|
||||
export let key
|
||||
export let actions
|
||||
export let bindings = []
|
||||
|
||||
|
@ -24,6 +26,9 @@
|
|||
|
||||
// These are ephemeral bindings which only exist while executing actions
|
||||
$: buttonContextBindings = getButtonContextBindings(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
key,
|
||||
actions,
|
||||
selectedAction?.id
|
||||
)
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let key
|
||||
export let value = []
|
||||
export let name
|
||||
export let bindings
|
||||
|
@ -81,5 +82,6 @@
|
|||
bind:actions={tmpValue}
|
||||
eventType={name}
|
||||
{bindings}
|
||||
{key}
|
||||
/>
|
||||
</Drawer>
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<script>
|
||||
import { Select, Body } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
export let parameters
|
||||
export let bindings
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
label: "Continue if",
|
||||
value: "continue",
|
||||
},
|
||||
{
|
||||
label: "Stop if",
|
||||
value: "stop",
|
||||
},
|
||||
]
|
||||
const operatorOptions = [
|
||||
{
|
||||
label: "Equals",
|
||||
value: "equal",
|
||||
},
|
||||
{
|
||||
label: "Not equals",
|
||||
value: "notEqual",
|
||||
},
|
||||
]
|
||||
|
||||
onMount(() => {
|
||||
if (!parameters.type) {
|
||||
parameters.type = "continue"
|
||||
}
|
||||
if (!parameters.operator) {
|
||||
parameters.operator = "equal"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Body size="S">
|
||||
Configure a condition to be evaluated which can stop further actions from
|
||||
being executed.
|
||||
</Body>
|
||||
<Select
|
||||
bind:value={parameters.type}
|
||||
options={typeOptions}
|
||||
placeholder={null}
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
placeholder="Value"
|
||||
value={parameters.value}
|
||||
on:change={e => (parameters.value = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
<Select
|
||||
bind:value={parameters.operator}
|
||||
options={operatorOptions}
|
||||
placeholder={null}
|
||||
/>
|
||||
<DrawerBindableInput
|
||||
placeholder="Reference value"
|
||||
bind:value={parameters.referenceValue}
|
||||
on:change={e => (parameters.referenceValue = e.detail)}
|
||||
{bindings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
export let parameters
|
||||
|
||||
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
||||
$: components = findAllMatchingComponents($currentAsset?.props, component =>
|
||||
component._component.endsWith("s3upload")
|
||||
)
|
||||
</script>
|
||||
|
|
|
@ -13,3 +13,4 @@ export { default as RefreshDataProvider } from "./RefreshDataProvider.svelte"
|
|||
export { default as DuplicateRow } from "./DuplicateRow.svelte"
|
||||
export { default as S3Upload } from "./S3Upload.svelte"
|
||||
export { default as ExportData } from "./ExportData.svelte"
|
||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||
|
|
|
@ -84,6 +84,11 @@
|
|||
{
|
||||
"name": "Export Data",
|
||||
"component": "ExportData"
|
||||
},
|
||||
{
|
||||
"name": "Continue if / Stop if",
|
||||
"component": "ContinueIf",
|
||||
"dependsOnFeature": "continueIfAction"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||
|
||||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
||||
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||
|
||||
// Set initial value to closest data provider
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue