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" |
import filterTests from "../support/filterTests" |
||||
|
|
||||
filterTests(['all'], () => { |
filterTests(['all'], () => { |
||||
context("Rename an App", () => { |
context("Rename an App", () => { |
||||
beforeEach(() => { |
beforeEach(() => { |
||||
cy.login() |
cy.login() |
||||
cy.createTestApp() |
cy.createTestApp() |
||||
}) |
}) |
||||
|
|
||||
it("should rename an unpublished application", () => { |
it("should rename an unpublished application", () => { |
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
const appRename = "Cypress Renamed" |
const appRename = "Cypress Renamed" |
||||
// Rename app, Search for app, Confirm name was changed
|
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click() |
cy.get(".home-logo").click() |
||||
renameApp(appName, appRename) |
renameApp(appName, appRename) |
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
cy.searchForApplication(appRename) |
cy.get(".appTable").find(".title").should("have.length", 1) |
||||
cy.get(".appTable").find(".title").should("have.length", 1) |
cy.applicationInAppTable(appRename) |
||||
// Set app name back to Cypress Tests
|
// Set app name back to Cypress Tests
|
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
renameApp(appRename, appName) |
renameApp(appRename, appName) |
||||
}) |
}) |
||||
|
|
||||
xit("Should rename a published application", () => { |
xit("Should rename a published application", () => { |
||||
// It is not possible to rename a published application
|
// It is not possible to rename a published application
|
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
const appRename = "Cypress Renamed" |
const appRename = "Cypress Renamed" |
||||
// Publish the app
|
// Publish the app
|
||||
cy.get(".toprightnav") |
cy.get(".toprightnav") |
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
cy.get(".spectrum-Button").contains("Publish").click({ force: true }) |
||||
cy.get(".spectrum-Dialog-grid") |
cy.get(".spectrum-Dialog-grid") |
||||
.within(() => { |
.within(() => { |
||||
// Click publish again within the modal
|
// Click publish again within the modal
|
||||
cy.get(".spectrum-Button").contains("Publish").click({force: true}) |
cy.get(".spectrum-Button").contains("Publish").click({ force: true }) |
||||
}) |
}) |
||||
// Rename app, Search for app, Confirm name was changed
|
// Rename app, Search for app, Confirm name was changed
|
||||
cy.get(".home-logo").click() |
cy.get(".home-logo").click() |
||||
renameApp(appName, appRename, true) |
renameApp(appName, appRename, true) |
||||
cy.searchForApplication(appRename) |
cy.get(".appTable").find(".wrapper").should("have.length", 1) |
||||
cy.get(".appTable").find(".wrapper").should("have.length", 1) |
cy.applicationInAppTable(appRename) |
||||
}) |
}) |
||||
|
|
||||
it("Should try to rename an application to have no name", () => { |
it("Should try to rename an application to have no name", () => { |
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
cy.get(".home-logo").click() |
cy.get(".home-logo").click() |
||||
renameApp(appName, " ", false, true) |
renameApp(appName, " ", false, true) |
||||
cy.wait(500) |
cy.wait(500) |
||||
// Close modal and confirm name has not been changed
|
// Close modal and confirm name has not been changed
|
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() |
cy.get(".spectrum-Dialog-grid").contains("Cancel").click() |
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
cy.searchForApplication(appName) |
cy.applicationInAppTable(appName) |
||||
cy.get(".appTable").find(".title").should("have.length", 1) |
cy.get(".appTable").find(".title").should("have.length", 1) |
||||
|
|
||||
}) |
}) |
||||
|
|
||||
xit("Should create two applications with the same name", () => { |
xit("Should create two applications with the same name", () => { |
||||
// It is not possible to have applications with the same name
|
// It is not possible to have applications with the same name
|
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
cy.visit(`${Cypress.config().baseUrl}/builder`) |
cy.visit(`${Cypress.config().baseUrl}/builder`) |
||||
cy.wait(500) |
cy.wait(500) |
||||
cy.get(".spectrum-Button").contains("Create app").click({force: true}) |
cy.get(".spectrum-Button").contains("Create app").click({ force: true }) |
||||
cy.contains(/Start from scratch/).click() |
cy.contains(/Start from scratch/).click() |
||||
cy.get(".spectrum-Modal") |
cy.get(".spectrum-Modal") |
||||
.within(() => { |
.within(() => { |
||||
cy.get("input").eq(0).type(appName) |
cy.get("input").eq(0).type(appName) |
||||
cy.get(".spectrum-ButtonGroup").contains("Create app").click({force: true}) |
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(".error").should("have.text", "Another app with the same name already exists") |
||||
}) |
}) |
||||
}) |
}) |
||||
|
|
||||
it("should validate application names", () => { |
it("should validate application names", () => { |
||||
// App name must be letters, numbers and spaces only
|
// App name must be letters, numbers and spaces only
|
||||
// This test checks numbers and special characters specifically
|
// This test checks numbers and special characters specifically
|
||||
const appName = "Cypress Tests" |
const appName = "Cypress Tests" |
||||
const numberName = 12345 |
const numberName = 12345 |
||||
const specialCharName = "£$%^" |
const specialCharName = "£$%^" |
||||
cy.get(".home-logo").click() |
cy.get(".home-logo").click() |
||||
renameApp(appName, numberName) |
renameApp(appName, numberName) |
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
cy.searchForApplication(numberName) |
cy.applicationInAppTable(numberName) |
||||
cy.get(".appTable").find(".title").should("have.length", 1) |
cy.get(".appTable").find(".title").should("have.length", 1) |
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
renameApp(numberName, specialCharName) |
renameApp(numberName, specialCharName) |
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") |
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only") |
||||
// Set app name back to Cypress Tests
|
// Set app name back to Cypress Tests
|
||||
cy.reload() |
cy.reload() |
||||
cy.wait(1000) |
cy.wait(1000) |
||||
renameApp(numberName, appName) |
renameApp(numberName, appName) |
||||
}) |
}) |
||||
|
|
||||
const renameApp = (originalName, changedName, published, noName) => { |
const renameApp = (originalName, changedName, published, noName) => { |
||||
cy.searchForApplication(originalName) |
cy.applicationInAppTable(originalName) |
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) |
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`) |
||||
.its("body") |
.its("body") |
||||
.then(val => { |
.then(val => { |
||||
if (val.length > 0) { |
if (val.length > 0) { |
||||
cy.get(".appTable") |
cy.get(".appTable") |
||||
.within(() => { |
.within(() => { |
||||
cy.get(".spectrum-Icon").eq(1).click() |
cy.get(".spectrum-Icon").eq(1).click() |
||||
}) |
}) |
||||
// Check for when an app is published
|
// Check for when an app is published
|
||||
if (published == true){ |
if (published == true) { |
||||
// Should not have Edit as option, will unpublish app
|
// Should not have Edit as option, will unpublish app
|
||||
cy.should("not.have.value", "Edit") |
cy.should("not.have.value", "Edit") |
||||
cy.get(".spectrum-Menu").contains("Unpublish").click() |
cy.get(".spectrum-Menu").contains("Unpublish").click() |
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() |
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click() |
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").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("input").clear() |
||||
cy.get(".spectrum-Modal") |
cy.get("input").eq(0).type(changedName).should("have.value", changedName).blur() |
||||
.within(() => { |
cy.get(".spectrum-ButtonGroup").contains("Save").click({ force: true }) |
||||
if (noName == true){ |
cy.wait(500) |
||||
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) |
|
||||
}) |
|
||||
} |
|
||||
}) |
}) |
||||
|
|
||||
} |
} |
||||
}) |
}) |
||||
}) |
}) |
||||
|
|||||
@ -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