forked from tsai/budibase
33 changed files with 8813 additions and 208 deletions
File diff suppressed because it is too large
@ -1,75 +0,0 @@ |
|||||
{ |
|
||||
"categories": [ |
|
||||
{ |
|
||||
"name": "Basic", |
|
||||
"components": [ |
|
||||
{ |
|
||||
"component": "Container", |
|
||||
"description": "This component contains things within itself", |
|
||||
"icon": "ri-layout-row-fill", |
|
||||
"commonProps": {}, |
|
||||
"type": [] |
|
||||
}, |
|
||||
{ |
|
||||
"component": "Text", |
|
||||
"description": "This is a simple text component", |
|
||||
"icon": "ri-t-box-fill", |
|
||||
"commonProps": { |
|
||||
}, |
|
||||
"type": [ |
|
||||
{ |
|
||||
"_component": "@budibase/standard-components/header", |
|
||||
"name": "Headline", |
|
||||
"icon": "headline", |
|
||||
"props": { |
|
||||
"type": { |
|
||||
"type": "options", |
|
||||
"options": [ |
|
||||
"h1", |
|
||||
"h2" |
|
||||
], |
|
||||
"default": "h1" |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
{ |
|
||||
"_component": "@budibase/standard-components/text", |
|
||||
"name": "Paragraph", |
|
||||
"icon": "paragraph", |
|
||||
"props": { |
|
||||
} |
|
||||
} |
|
||||
] |
|
||||
}, |
|
||||
{ |
|
||||
"component": "Button", |
|
||||
"description": "A basic html button that is ready for styling", |
|
||||
"icon": "ri-radio-button-fill", |
|
||||
"commonProps": {}, |
|
||||
"type": [] |
|
||||
}, |
|
||||
{ |
|
||||
"component": "Icon", |
|
||||
"description": "A basic component for displaying icons", |
|
||||
"icon": "ri-sun-fill", |
|
||||
"commonProps": {}, |
|
||||
"type": [] |
|
||||
}, |
|
||||
{ |
|
||||
"component": "Avatar", |
|
||||
"description": "A basic component for rendering an avatar", |
|
||||
"icon": "ri-user-smile-fill", |
|
||||
"commonProps": {}, |
|
||||
"type": [] |
|
||||
}, |
|
||||
{ |
|
||||
"component": "Link", |
|
||||
"description": "A basic link component for internal and external links", |
|
||||
"icon": "ri-link", |
|
||||
"commonProps": {}, |
|
||||
"type": [] |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
] |
|
||||
} |
|
||||
@ -0,0 +1,41 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
||||
|
import AttachmentList from "../../attachments/AttachmentList.svelte" |
||||
|
// import Modal from "./Modal.svelte" |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
|
||||
|
let anchor |
||||
|
let dropdown |
||||
|
|
||||
|
export let files |
||||
|
</script> |
||||
|
|
||||
|
<AttachmentList {files} /> |
||||
|
<!-- <div bind:this={anchor}> |
||||
|
<Button text small on:click={dropdown.show}> |
||||
|
<Icon name="edit" /> |
||||
|
</Button> |
||||
|
</div> |
||||
|
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
||||
|
<h5>Edit Attachment(s)</h5> |
||||
|
<Modal |
||||
|
{_bb} |
||||
|
{model} |
||||
|
onClosed={dropdown.hide} |
||||
|
on:newRecord={() => dispatch('newRecord')} /> |
||||
|
</DropdownMenu> --> |
||||
|
|
||||
|
<!-- <style> |
||||
|
div { |
||||
|
display: grid; |
||||
|
grid-template-columns: auto auto; |
||||
|
place-items: start center; |
||||
|
} |
||||
|
h5 { |
||||
|
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
||||
|
margin: 0; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> --> |
||||
@ -0,0 +1,151 @@ |
|||||
|
<script> |
||||
|
// Import valueSetters and custom renderers |
||||
|
import { number } from "./valueSetters" |
||||
|
import { getRenderer } from "./customRenderer" |
||||
|
|
||||
|
// These maps need to be set up to handle whatever types that are used in the models. |
||||
|
const setters = new Map([["number", number]]) |
||||
|
|
||||
|
import fetchData from "../fetchData.js" |
||||
|
import { isEmpty } from "lodash/fp" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
import AgGrid from "@budibase/svelte-ag-grid" |
||||
|
import CreateRowButton from "./CreateRow/Button.svelte" |
||||
|
import { TextButton as DeleteButton, Icon } from "@budibase/bbui" |
||||
|
|
||||
|
export let _bb |
||||
|
export let datasource = {} |
||||
|
export let editable |
||||
|
export let theme = 'alpine' |
||||
|
|
||||
|
let dataLoaded = false |
||||
|
let data |
||||
|
let columnDefs |
||||
|
let selectedRows = [] |
||||
|
let model |
||||
|
let options = { |
||||
|
defaultColDef: { |
||||
|
flex: 1, |
||||
|
minWidth: 150, |
||||
|
filter: true, |
||||
|
}, |
||||
|
rowSelection: editable ? "multiple" : false, |
||||
|
suppressRowClickSelection: !editable, |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
if (datasource.modelId) { |
||||
|
const jsonModel = await _bb.api.get(`/api/models/${datasource.modelId}`) |
||||
|
model = await jsonModel.json() |
||||
|
const { schema } = model |
||||
|
if (!isEmpty(datasource)) { |
||||
|
data = await fetchData(datasource) |
||||
|
columnDefs = Object.keys(schema).map((key, i) => { |
||||
|
return { |
||||
|
headerCheckboxSelection: i === 0 && editable, |
||||
|
checkboxSelection: i === 0 && editable, |
||||
|
valueSetter: setters.get(schema[key].type), |
||||
|
headerName: key.charAt(0).toUpperCase() + key.slice(1), |
||||
|
field: key, |
||||
|
hide: shouldHideField(key), |
||||
|
sortable: true, |
||||
|
editable: editable, |
||||
|
cellRenderer: getRenderer(schema[key], editable), |
||||
|
autoHeight: true, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
dataLoaded = true |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
const isEditable = type => |
||||
|
type !== "boolean" && |
||||
|
type !== "options" && |
||||
|
// type !== "datetime" && |
||||
|
type !== "link" && |
||||
|
type !== "attachment" |
||||
|
|
||||
|
const shouldHideField = name => { |
||||
|
if (name.startsWith("_")) return true |
||||
|
// always 'record' |
||||
|
if (name === "type") return true |
||||
|
// tables are always tied to a single modelId, this is irrelevant |
||||
|
if (name === "modelId") return true |
||||
|
|
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
const handleNewRecord = async () => { |
||||
|
data = await fetchData(datasource) |
||||
|
} |
||||
|
|
||||
|
const handleUpdate = ({ detail }) => { |
||||
|
data[detail.row] = detail.data |
||||
|
updateRecord(detail.data) |
||||
|
} |
||||
|
|
||||
|
const updateRecord = async record => { |
||||
|
const response = await _bb.api.patch( |
||||
|
`/api/${record.modelId}/records/${record._id}`, |
||||
|
record |
||||
|
) |
||||
|
const json = await response.json() |
||||
|
} |
||||
|
|
||||
|
const deleteRecords = async () => { |
||||
|
const response = await _bb.api.post(`/api/${datasource.name}/records`, { |
||||
|
records: selectedRows, |
||||
|
type: "delete", |
||||
|
}) |
||||
|
data = data.filter(record => !selectedRows.includes(record)) |
||||
|
selectedRows = [] |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<svelte:head> |
||||
|
<link |
||||
|
rel="stylesheet" |
||||
|
href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" /> |
||||
|
</svelte:head> |
||||
|
|
||||
|
<div class="container"> |
||||
|
{#if dataLoaded} |
||||
|
{#if editable} |
||||
|
<div class="controls"> |
||||
|
<CreateRowButton {_bb} {model} on:newRecord={handleNewRecord} /> |
||||
|
{#if selectedRows.length > 0} |
||||
|
<DeleteButton text small on:click={deleteRecords}> |
||||
|
<Icon name="addrow" /> |
||||
|
Delete {selectedRows.length} row(s) |
||||
|
</DeleteButton> |
||||
|
{/if} |
||||
|
</div> |
||||
|
{/if} |
||||
|
<AgGrid |
||||
|
{theme} |
||||
|
{options} |
||||
|
{data} |
||||
|
{columnDefs} |
||||
|
on:update={handleUpdate} |
||||
|
on:select={({ detail }) => (selectedRows = detail)} /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.container { |
||||
|
--grid-height: 800px; |
||||
|
} |
||||
|
.container :global(form) { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(2); |
||||
|
} |
||||
|
.controls { |
||||
|
margin-bottom: var(--spacing-s); |
||||
|
display: grid; |
||||
|
grid-gap: var(--spacing-s); |
||||
|
grid-template-columns: auto auto; |
||||
|
justify-content: start; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,38 @@ |
|||||
|
<script> |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
||||
|
import Modal from "./Modal.svelte" |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
|
||||
|
let anchor |
||||
|
let dropdown |
||||
|
|
||||
|
export let _bb, model |
||||
|
</script> |
||||
|
|
||||
|
<div bind:this={anchor}> |
||||
|
<Button text small on:click={dropdown.show}> |
||||
|
<Icon name="addrow" /> |
||||
|
Create New Row |
||||
|
</Button> |
||||
|
</div> |
||||
|
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
||||
|
<h5>Add New Row</h5> |
||||
|
<Modal |
||||
|
{_bb} |
||||
|
{model} |
||||
|
onClosed={dropdown.hide} |
||||
|
on:newRecord={() => dispatch('newRecord')} /> |
||||
|
</DropdownMenu> |
||||
|
|
||||
|
<style> |
||||
|
div { |
||||
|
display: grid; |
||||
|
} |
||||
|
h5 { |
||||
|
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
||||
|
margin: 0; |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,151 @@ |
|||||
|
<script> |
||||
|
import { onMount, createEventDispatcher } from "svelte" |
||||
|
import { fade } from "svelte/transition" |
||||
|
import { Button, Label, DatePicker } from "@budibase/bbui" |
||||
|
import Dropzone from "../../attachments/Dropzone.svelte" |
||||
|
import debounce from "lodash.debounce" |
||||
|
const dispatch = createEventDispatcher() |
||||
|
|
||||
|
const DEFAULTS_FOR_TYPE = { |
||||
|
string: "", |
||||
|
boolean: false, |
||||
|
number: null, |
||||
|
link: [], |
||||
|
} |
||||
|
|
||||
|
export let _bb |
||||
|
export let model |
||||
|
export let onClosed |
||||
|
|
||||
|
let record = { modelId: model._id } |
||||
|
let store = _bb.store |
||||
|
let schema = model.schema |
||||
|
let saved = false |
||||
|
let recordId |
||||
|
let isNew = true |
||||
|
let errors = {} |
||||
|
|
||||
|
$: fields = schema ? Object.keys(schema) : [] |
||||
|
|
||||
|
$: errorMessages = Object.entries(errors).map( |
||||
|
([field, message]) => `${field} ${message}` |
||||
|
) |
||||
|
|
||||
|
const save = debounce(async () => { |
||||
|
for (let field of fields) { |
||||
|
// Assign defaults to empty fields to prevent validation issues |
||||
|
if (!(field in record)) { |
||||
|
record[field] = DEFAULTS_FOR_TYPE[schema[field].type] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const SAVE_RECORD_URL = `/api/${model._id}/records` |
||||
|
const response = await _bb.api.post(SAVE_RECORD_URL, record) |
||||
|
|
||||
|
const json = await response.json() |
||||
|
|
||||
|
if (response.status === 200) { |
||||
|
store.update(state => { |
||||
|
state[model._id] = state[model._id] |
||||
|
? [...state[model._id], json] |
||||
|
: [json] |
||||
|
return state |
||||
|
}) |
||||
|
|
||||
|
errors = {} |
||||
|
|
||||
|
// wipe form, if new record, otherwise update |
||||
|
// model to get new _rev |
||||
|
record = isNew ? { modelId: model._id } : json |
||||
|
|
||||
|
onClosed() |
||||
|
dispatch("newRecord") |
||||
|
} |
||||
|
|
||||
|
if (response.status === 400) { |
||||
|
errors = json.errors |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
onMount(async () => { |
||||
|
const routeParams = _bb.routeParams() |
||||
|
recordId = |
||||
|
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) |
||||
|
isNew = !recordId || recordId === "new" |
||||
|
|
||||
|
if (isNew) { |
||||
|
record = { modelId: model } |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const GET_RECORD_URL = `/api/${model._id}/records/${recordId}` |
||||
|
const response = await _bb.api.get(GET_RECORD_URL) |
||||
|
const json = await response.json() |
||||
|
record = json |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<div class="actions"> |
||||
|
{#each errorMessages as error} |
||||
|
<p class="error">{error}</p> |
||||
|
{/each} |
||||
|
<form on:submit|preventDefault> |
||||
|
{#each fields as field} |
||||
|
<div class="form-item"> |
||||
|
<Label small forAttr={'form-stacked-text'}>{field}</Label> |
||||
|
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} |
||||
|
<select bind:value={record[field]}> |
||||
|
{#each schema[field].constraints.inclusion as opt} |
||||
|
<option>{opt}</option> |
||||
|
{/each} |
||||
|
</select> |
||||
|
{:else if schema[field].type === 'datetime'} |
||||
|
<DatePicker bind:value={record[field]} /> |
||||
|
{:else if schema[field].type === 'boolean'} |
||||
|
<input class="input" type="checkbox" bind:checked={record[field]} /> |
||||
|
{:else if schema[field].type === 'number'} |
||||
|
<input class="input" type="number" bind:value={record[field]} /> |
||||
|
{:else if schema[field].type === 'string'} |
||||
|
<input class="input" type="text" bind:value={record[field]} /> |
||||
|
{:else if schema[field].type === 'attachment'} |
||||
|
<Dropzone bind:files={record[field]} /> |
||||
|
{/if} |
||||
|
</div> |
||||
|
<hr /> |
||||
|
{/each} |
||||
|
</form> |
||||
|
</div> |
||||
|
<footer> |
||||
|
<div class="button-margin-3"> |
||||
|
<Button secondary on:click={onClosed}>Cancel</Button> |
||||
|
</div> |
||||
|
<div class="button-margin-4"> |
||||
|
<Button primary on:click={save}>Save</Button> |
||||
|
</div> |
||||
|
</footer> |
||||
|
|
||||
|
<style> |
||||
|
.actions { |
||||
|
padding: var(--spacing-l) var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
footer { |
||||
|
padding: 20px 30px; |
||||
|
display: grid; |
||||
|
grid-template-columns: 1fr 1fr 1fr 1fr; |
||||
|
gap: 20px; |
||||
|
background: var(--grey-1); |
||||
|
border-bottom-left-radius: 0.5rem; |
||||
|
border-bottom-left-radius: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
.button-margin-3 { |
||||
|
grid-column-start: 3; |
||||
|
display: grid; |
||||
|
} |
||||
|
|
||||
|
.button-margin-4 { |
||||
|
grid-column-start: 4; |
||||
|
display: grid; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<script> |
||||
|
import { DatePicker } from "@budibase/bbui" |
||||
|
</script> |
||||
|
|
||||
|
<DatePicker /> |
||||
@ -0,0 +1,17 @@ |
|||||
|
<script> |
||||
|
import { Select } from "@budibase/bbui" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
const dispatch = createEventDispatcher() |
||||
|
|
||||
|
export let value |
||||
|
export let options |
||||
|
|
||||
|
$: dispatch("change", value) |
||||
|
</script> |
||||
|
|
||||
|
<Select label={false} bind:value> |
||||
|
<option value="">Choose an option</option> |
||||
|
{#each options as option} |
||||
|
<option value={option}>{option}</option> |
||||
|
{/each} |
||||
|
</Select> |
||||
@ -0,0 +1,117 @@ |
|||||
|
// Custom renderers to handle special types
|
||||
|
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
|
||||
|
|
||||
|
import AttachmentCell from "./AttachmentCell/Button.svelte" |
||||
|
import Select from "./Select/Wrapper.svelte" |
||||
|
import DatePicker from "./DateTime/Wrapper.svelte" |
||||
|
|
||||
|
const renderers = new Map([ |
||||
|
["boolean", booleanRenderer], |
||||
|
["attachment", attachmentRenderer], |
||||
|
["options", optionsRenderer], |
||||
|
["link", linkedRecordRenderer], |
||||
|
]) |
||||
|
|
||||
|
export function getRenderer({ type, constraints }, editable) { |
||||
|
if (renderers.get(type)) { |
||||
|
return renderers.get(type)(constraints, editable) |
||||
|
} else { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* eslint-disable no-unused-vars */ |
||||
|
function booleanRenderer(constraints, editable) { |
||||
|
return params => { |
||||
|
const toggle = e => { |
||||
|
params.value = !params.value |
||||
|
params.setValue(e.currentTarget.checked) |
||||
|
} |
||||
|
let input = document.createElement("input") |
||||
|
input.style.display = "grid" |
||||
|
input.style.placeItems = "center" |
||||
|
input.style.height = "100%" |
||||
|
input.type = "checkbox" |
||||
|
input.checked = params.value |
||||
|
if (editable) { |
||||
|
input.addEventListener("click", toggle) |
||||
|
} else { |
||||
|
input.disabled = true |
||||
|
} |
||||
|
|
||||
|
return input |
||||
|
} |
||||
|
} |
||||
|
/* eslint-disable no-unused-vars */ |
||||
|
function attachmentRenderer(constraints, editable) { |
||||
|
return params => { |
||||
|
const container = document.createElement("div") |
||||
|
|
||||
|
const attachmentInstance = new AttachmentCell({ |
||||
|
target: container, |
||||
|
props: { |
||||
|
files: params.value || [], |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
return container |
||||
|
} |
||||
|
} |
||||
|
/* eslint-disable no-unused-vars */ |
||||
|
function dateRenderer(constraints, editable) { |
||||
|
return function(params) { |
||||
|
const container = document.createElement("div") |
||||
|
const toggle = e => { |
||||
|
params.setValue(e.detail[0][0]) |
||||
|
} |
||||
|
|
||||
|
// Options need to be passed in with minTime and maxTime! Needs bbui update.
|
||||
|
|
||||
|
const datePickerInstance = new DatePicker({ |
||||
|
target: container, |
||||
|
props: { |
||||
|
value: params.value, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
return container |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function optionsRenderer({ inclusion }, editable) { |
||||
|
return params => { |
||||
|
if (!editable) return params.value |
||||
|
const container = document.createElement("div") |
||||
|
container.style.display = "grid" |
||||
|
container.style.placeItems = "center" |
||||
|
container.style.height = "100%" |
||||
|
const change = e => { |
||||
|
params.setValue(e.detail) |
||||
|
} |
||||
|
|
||||
|
const selectInstance = new Select({ |
||||
|
target: container, |
||||
|
props: { |
||||
|
value: params.value, |
||||
|
options: inclusion, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
selectInstance.$on("change", change) |
||||
|
|
||||
|
return container |
||||
|
} |
||||
|
} |
||||
|
/* eslint-disable no-unused-vars */ |
||||
|
function linkedRecordRenderer(constraints, editable) { |
||||
|
return params => { |
||||
|
let container = document.createElement("div") |
||||
|
container.style.display = "grid" |
||||
|
container.style.placeItems = "center" |
||||
|
container.style.height = "100%" |
||||
|
|
||||
|
container.innerText = params.value.length || 0 |
||||
|
|
||||
|
return container |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
// https://www.ag-grid.com/javascript-grid-value-setters/
|
||||
|
// These handles values and makes sure they adhere to the data type provided by the model
|
||||
|
export const number = params => { |
||||
|
params.data[params.colDef.field] = parseFloat(params.newValue) |
||||
|
return true |
||||
|
} |
||||
Loading…
Reference in new issue