forked from tsai/budibase
113 changed files with 11457 additions and 763 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
@ -0,0 +1,31 @@ |
|||||
|
<script> |
||||
|
import "@spectrum-css/toast/dist/index-vars.css" |
||||
|
import Portal from "svelte-portal" |
||||
|
import { banner } from "../Stores/banner" |
||||
|
import Banner from "./Banner.svelte" |
||||
|
import { fly } from "svelte/transition" |
||||
|
</script> |
||||
|
|
||||
|
<Portal target=".banner-container"> |
||||
|
<div class="banner"> |
||||
|
{#if $banner.message} |
||||
|
<div transition:fly={{ y: -30 }}> |
||||
|
<Banner |
||||
|
type={$banner.type} |
||||
|
extraButtonText={$banner.extraButtonText} |
||||
|
extraButtonAction={$banner.extraButtonAction} |
||||
|
on:change={$banner.onChange} |
||||
|
> |
||||
|
{$banner.message} |
||||
|
</Banner> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</Portal> |
||||
|
|
||||
|
<style> |
||||
|
.banner { |
||||
|
pointer-events: none; |
||||
|
width: 100%; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,37 @@ |
|||||
|
import { writable } from "svelte/store" |
||||
|
|
||||
|
export function createBannerStore() { |
||||
|
const DEFAULT_CONFIG = {} |
||||
|
|
||||
|
const banner = writable(DEFAULT_CONFIG) |
||||
|
|
||||
|
const show = async ( |
||||
|
// eslint-disable-next-line
|
||||
|
config = { message, type, extraButtonText, extraButtonAction, onChange } |
||||
|
) => { |
||||
|
banner.update(store => { |
||||
|
return { |
||||
|
...store, |
||||
|
...config, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const showStatus = async () => { |
||||
|
const config = { |
||||
|
message: "Some systems are experiencing issues", |
||||
|
type: "negative", |
||||
|
extraButtonText: "View Status", |
||||
|
extraButtonAction: () => window.open("https://status.budibase.com/"), |
||||
|
} |
||||
|
|
||||
|
await show(config) |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
subscribe: banner.subscribe, |
||||
|
showStatus, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const banner = createBannerStore() |
||||
@ -0,0 +1,86 @@ |
|||||
|
<script> |
||||
|
import { Label, Select, Body } from "@budibase/bbui" |
||||
|
import { findAllMatchingComponents } from "builderStore/componentUtils" |
||||
|
import { currentAsset } from "builderStore" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
export let parameters |
||||
|
|
||||
|
$: tables = findAllMatchingComponents($currentAsset?.props, component => |
||||
|
component._component.endsWith("table") |
||||
|
).map(table => ({ |
||||
|
label: table._instanceName, |
||||
|
value: table._id, |
||||
|
})) |
||||
|
|
||||
|
$: tableBlocks = findAllMatchingComponents($currentAsset?.props, component => |
||||
|
component._component.endsWith("tableblock") |
||||
|
).map(block => ({ |
||||
|
label: block._instanceName, |
||||
|
value: `${block._id}-table`, |
||||
|
})) |
||||
|
|
||||
|
$: componentOptions = tables.concat(tableBlocks) |
||||
|
|
||||
|
const FORMATS = [ |
||||
|
{ |
||||
|
label: "CSV", |
||||
|
value: "csv", |
||||
|
}, |
||||
|
{ |
||||
|
label: "JSON", |
||||
|
value: "json", |
||||
|
}, |
||||
|
] |
||||
|
|
||||
|
onMount(() => { |
||||
|
if (!parameters.type) { |
||||
|
parameters.type = "csv" |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
<Body size="S"> |
||||
|
Choose the table component that you would like to export your row selection |
||||
|
from. |
||||
|
<br /> |
||||
|
Please ensure you have enabled row selection in the table settings. |
||||
|
</Body> |
||||
|
|
||||
|
<div class="params"> |
||||
|
<Label small>Table</Label> |
||||
|
<Select |
||||
|
bind:value={parameters.tableComponentId} |
||||
|
options={componentOptions} |
||||
|
/> |
||||
|
|
||||
|
<Label small>Export as</Label> |
||||
|
<Select bind:value={parameters.type} options={FORMATS} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
width: 100%; |
||||
|
max-width: 500px; |
||||
|
margin: 0 auto; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: flex-start; |
||||
|
align-items: stretch; |
||||
|
gap: var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
.root :global(p) { |
||||
|
line-height: 1.5; |
||||
|
} |
||||
|
|
||||
|
.params { |
||||
|
display: grid; |
||||
|
column-gap: var(--spacing-xs); |
||||
|
row-gap: var(--spacing-s); |
||||
|
grid-template-columns: 70px 1fr; |
||||
|
align-items: center; |
||||
|
} |
||||
|
</style> |
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,61 +1,66 @@ |
|||||
export const buildAttachmentEndpoints = API => ({ |
export const buildAttachmentEndpoints = API => { |
||||
/** |
|
||||
* Uploads an attachment to the server. |
|
||||
* @param data the attachment to upload |
|
||||
* @param tableId the table ID to upload to |
|
||||
*/ |
|
||||
uploadAttachment: async ({ data, tableId }) => { |
|
||||
return await API.post({ |
|
||||
url: `/api/attachments/${tableId}/upload`, |
|
||||
body: data, |
|
||||
json: false, |
|
||||
}) |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* Uploads an attachment to the server as a builder user from the builder. |
|
||||
* @param data the data to upload |
|
||||
*/ |
|
||||
uploadBuilderAttachment: async data => { |
|
||||
return await API.post({ |
|
||||
url: "/api/attachments/process", |
|
||||
body: data, |
|
||||
json: false, |
|
||||
}) |
|
||||
}, |
|
||||
|
|
||||
/** |
/** |
||||
* Generates a signed URL to upload a file to an external datasource. |
* Generates a signed URL to upload a file to an external datasource. |
||||
* @param datasourceId the ID of the datasource to upload to |
* @param datasourceId the ID of the datasource to upload to |
||||
* @param bucket the name of the bucket to upload to |
* @param bucket the name of the bucket to upload to |
||||
* @param key the name of the file to upload to |
* @param key the name of the file to upload to |
||||
*/ |
*/ |
||||
getSignedDatasourceURL: async ({ datasourceId, bucket, key }) => { |
const getSignedDatasourceURL = async ({ datasourceId, bucket, key }) => { |
||||
return await API.post({ |
return await API.post({ |
||||
url: `/api/attachments/${datasourceId}/url`, |
url: `/api/attachments/${datasourceId}/url`, |
||||
body: { bucket, key }, |
body: { bucket, key }, |
||||
}) |
}) |
||||
}, |
} |
||||
|
|
||||
/** |
return { |
||||
* Uploads a file to an external datasource. |
getSignedDatasourceURL, |
||||
* @param datasourceId the ID of the datasource to upload to |
|
||||
* @param bucket the name of the bucket to upload to |
/** |
||||
* @param key the name of the file to upload to |
* Uploads an attachment to the server. |
||||
* @param data the file to upload |
* @param data the attachment to upload |
||||
*/ |
* @param tableId the table ID to upload to |
||||
externalUpload: async ({ datasourceId, bucket, key, data }) => { |
*/ |
||||
const { signedUrl, publicUrl } = await API.getSignedDatasourceURL({ |
uploadAttachment: async ({ data, tableId }) => { |
||||
datasourceId, |
return await API.post({ |
||||
bucket, |
url: `/api/attachments/${tableId}/upload`, |
||||
key, |
body: data, |
||||
}) |
json: false, |
||||
await API.put({ |
}) |
||||
url: signedUrl, |
}, |
||||
body: data, |
|
||||
json: false, |
/** |
||||
external: true, |
* Uploads an attachment to the server as a builder user from the builder. |
||||
}) |
* @param data the data to upload |
||||
return { publicUrl } |
*/ |
||||
}, |
uploadBuilderAttachment: async data => { |
||||
}) |
return await API.post({ |
||||
|
url: "/api/attachments/process", |
||||
|
body: data, |
||||
|
json: false, |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
/** |
||||
|
* Uploads a file to an external datasource. |
||||
|
* @param datasourceId the ID of the datasource to upload to |
||||
|
* @param bucket the name of the bucket to upload to |
||||
|
* @param key the name of the file to upload to |
||||
|
* @param data the file to upload |
||||
|
*/ |
||||
|
externalUpload: async ({ datasourceId, bucket, key, data }) => { |
||||
|
console.log(API) |
||||
|
const { signedUrl, publicUrl } = await getSignedDatasourceURL({ |
||||
|
datasourceId, |
||||
|
bucket, |
||||
|
key, |
||||
|
}) |
||||
|
await API.put({ |
||||
|
url: signedUrl, |
||||
|
body: data, |
||||
|
json: false, |
||||
|
external: true, |
||||
|
}) |
||||
|
return { publicUrl } |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
|||||
File diff suppressed because it is too large
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue