mirror of https://github.com/Budibase/budibase.git
26 changed files with 8727 additions and 177 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