mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
186 changed files with 14754 additions and 5131 deletions
@ -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
@ -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 |
|||
|
After Width: | Height: | Size: 6.1 KiB |
|
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
File diff suppressed because it is too large
@ -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) |
|||
}) |
|||
} |
|||
}) |
|||
|
|||
} |
|||
}) |
|||
}) |
|||
}) |
|||
|
|||
@ -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> |
|||
|
After Width: | Height: | Size: 3.1 KiB |
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue