mirror of https://github.com/Budibase/budibase.git
20 changed files with 187 additions and 820 deletions
@ -1,6 +0,0 @@ |
|||
<script> |
|||
import AttachmentList from "../../attachments/AttachmentList.svelte" |
|||
export let files |
|||
</script> |
|||
|
|||
<AttachmentList {files} on:delete /> |
|||
@ -1,180 +0,0 @@ |
|||
<script> |
|||
// Import valueSetters and custom renderers |
|||
import { number } from "./valueSetters" |
|||
import { getRenderer } from "./customRenderer" |
|||
import { isEmpty } from "lodash/fp" |
|||
import { getContext } from "svelte" |
|||
import AgGrid from "@budibase/svelte-ag-grid" |
|||
import { |
|||
TextButton as DeleteButton, |
|||
Icon, |
|||
Modal, |
|||
ModalContent, |
|||
} from "@budibase/bbui" |
|||
|
|||
// These maps need to be set up to handle whatever types that are used in the tables. |
|||
const setters = new Map([["number", number]]) |
|||
const SDK = getContext("sdk") |
|||
const component = getContext("component") |
|||
const { API, styleable } = SDK |
|||
|
|||
export let dataProvider |
|||
export let editable |
|||
export let theme = "alpine" |
|||
export let height = 500 |
|||
export let pagination |
|||
export let detailUrl |
|||
|
|||
// Add setting height as css var to allow grid to use correct height |
|||
$: gridStyles = { |
|||
...$component.styles, |
|||
normal: { |
|||
...$component.styles.normal, |
|||
["--grid-height"]: `${height}px`, |
|||
}, |
|||
} |
|||
$: setUpGrid(dataProvider) |
|||
$: dataLoaded = dataProvider?.loaded |
|||
$: data = dataProvider?.rows |
|||
|
|||
// These can never change at runtime so don't need to be reactive |
|||
let canEdit = editable && datasource && datasource.type !== "view" |
|||
let canAddDelete = editable && datasource && datasource.type === "table" |
|||
|
|||
let modal |
|||
let columnDefs |
|||
let selectedRows = [] |
|||
let table |
|||
let options = { |
|||
defaultColDef: { |
|||
flex: 1, |
|||
minWidth: 150, |
|||
filter: true, |
|||
}, |
|||
rowSelection: canEdit ? "multiple" : false, |
|||
suppressFieldDotNotation: true, |
|||
suppressRowClickSelection: !canEdit, |
|||
paginationAutoPageSize: true, |
|||
pagination, |
|||
} |
|||
|
|||
async function setUpGrid(dataProvider) { |
|||
if (!dataProvider) { |
|||
return |
|||
} |
|||
|
|||
const { schema } = dataProvider |
|||
columnDefs = Object.keys(schema).map((key, i) => { |
|||
return { |
|||
headerCheckboxSelection: i === 0 && canEdit, |
|||
checkboxSelection: i === 0 && canEdit, |
|||
valueSetter: setters.get(schema[key].type), |
|||
headerName: key, |
|||
field: key, |
|||
hide: shouldHideField(key), |
|||
sortable: true, |
|||
editable: canEdit && schema[key].type !== "link", |
|||
cellRenderer: getRenderer(schema[key], canEdit, SDK), |
|||
autoHeight: true, |
|||
} |
|||
}) |
|||
|
|||
if (detailUrl) { |
|||
columnDefs = [ |
|||
...columnDefs, |
|||
{ |
|||
headerName: "Detail", |
|||
field: "_id", |
|||
minWidth: 100, |
|||
width: 100, |
|||
flex: 0, |
|||
editable: false, |
|||
sortable: false, |
|||
cellRenderer: getRenderer( |
|||
{ |
|||
type: "_id", |
|||
options: { detailUrl }, |
|||
}, |
|||
false, |
|||
SDK |
|||
), |
|||
autoHeight: true, |
|||
pinned: "left", |
|||
filter: false, |
|||
}, |
|||
] |
|||
} |
|||
} |
|||
|
|||
const shouldHideField = name => { |
|||
if (name.startsWith("_")) return true |
|||
// always 'row' |
|||
if (name === "type") return true |
|||
// tables are always tied to a single tableId, this is irrelevant |
|||
if (name === "tableId") return true |
|||
|
|||
return false |
|||
} |
|||
|
|||
const handleUpdate = ({ detail }) => { |
|||
data[detail.row] = detail.data |
|||
updateRow(detail.data) |
|||
} |
|||
|
|||
const updateRow = async row => { |
|||
await API.updateRow(row) |
|||
} |
|||
|
|||
const deleteRows = async () => { |
|||
await API.deleteRows({ rows: selectedRows, tableId: datasource.name }) |
|||
data = data.filter(row => !selectedRows.includes(row)) |
|||
selectedRows = [] |
|||
} |
|||
</script> |
|||
|
|||
<div class="container" use:styleable={gridStyles}> |
|||
{#if dataLoaded} |
|||
{#if canAddDelete} |
|||
<div class="controls"> |
|||
{#if selectedRows.length > 0} |
|||
<DeleteButton text small on:click={modal.show()}> |
|||
<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} |
|||
<Modal bind:this={modal}> |
|||
<ModalContent |
|||
title="Confirm Row Deletion" |
|||
confirmText="Delete" |
|||
onConfirm={deleteRows}> |
|||
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span> |
|||
</ModalContent> |
|||
</Modal> |
|||
</div> |
|||
|
|||
<style> |
|||
.container :global(.ag-pinned-left-header .ag-header-cell-label) { |
|||
justify-content: center; |
|||
} |
|||
|
|||
.controls { |
|||
min-height: 15px; |
|||
margin-bottom: var(--spacing-s); |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
grid-template-columns: auto auto; |
|||
justify-content: start; |
|||
} |
|||
</style> |
|||
@ -1,37 +0,0 @@ |
|||
<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 table |
|||
</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 |
|||
{table} |
|||
onClosed={dropdown.hide} |
|||
on:newRow={() => dispatch('newRow')} /> |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
div { |
|||
display: grid; |
|||
} |
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
</style> |
|||
@ -1,144 +0,0 @@ |
|||
<script> |
|||
import { getContext, onMount, createEventDispatcher } from "svelte" |
|||
import { Button, Label, DatePicker, RichText } from "@budibase/bbui" |
|||
import Dropzone from "../../attachments/Dropzone.svelte" |
|||
import debounce from "lodash.debounce" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const { fetchRow, saveRow, routeStore } = getContext("sdk") |
|||
|
|||
const DEFAULTS_FOR_TYPE = { |
|||
string: "", |
|||
boolean: false, |
|||
number: null, |
|||
link: [], |
|||
} |
|||
|
|||
export let table |
|||
export let onClosed |
|||
|
|||
let row = { tableId: table._id } |
|||
let schema = table.schema |
|||
let saved = false |
|||
let rowId |
|||
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 row)) { |
|||
row[field] = DEFAULTS_FOR_TYPE[schema[field].type] |
|||
} |
|||
} |
|||
|
|||
const response = await saveRow(row) |
|||
|
|||
if (!response.error) { |
|||
// store.update(state => { |
|||
// state[table._id] = state[table._id] |
|||
// ? [...state[table._id], json] |
|||
// : [json] |
|||
// return state |
|||
// }) |
|||
|
|||
errors = {} |
|||
|
|||
// wipe form, if new row, otherwise update |
|||
// table to get new _rev |
|||
row = isNew ? { tableId: table._id } : response |
|||
|
|||
onClosed() |
|||
dispatch("newRow") |
|||
} else { |
|||
errors = [response.error] |
|||
} |
|||
}) |
|||
|
|||
onMount(async () => { |
|||
const routeParams = $routeStore.routeParams |
|||
rowId = |
|||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) |
|||
isNew = !rowId || rowId === "new" |
|||
|
|||
if (isNew) { |
|||
row = { tableId: table } |
|||
return |
|||
} |
|||
|
|||
row = await fetchRow({ tableId: table._id, rowId }) |
|||
}) |
|||
</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={row[field]}> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</select> |
|||
{:else if schema[field].type === 'datetime'} |
|||
<DatePicker bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'boolean'} |
|||
<input class="input" type="checkbox" bind:checked={row[field]} /> |
|||
{:else if schema[field].type === 'number'} |
|||
<input class="input" type="number" bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'string'} |
|||
<input class="input" type="text" bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'longform'} |
|||
<RichText bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'attachment'} |
|||
<Dropzone bind:files={row[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> |
|||
@ -1,5 +0,0 @@ |
|||
<script> |
|||
import { DatePicker } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<DatePicker /> |
|||
@ -1,75 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
|
|||
export let columnName |
|||
export let row |
|||
export let SDK |
|||
|
|||
const { API } = SDK |
|||
|
|||
$: count = |
|||
row && columnName && Array.isArray(row[columnName]) |
|||
? row[columnName].length |
|||
: 0 |
|||
let linkedRows = [] |
|||
let displayColumn |
|||
|
|||
onMount(async () => { |
|||
linkedRows = await API.fetchRelationshipData({ |
|||
tableId: row.tableId, |
|||
rowId: row._id, |
|||
fieldName: columnName, |
|||
}) |
|||
if (linkedRows && linkedRows.length) { |
|||
const table = await API.fetchTableDefinition(linkedRows[0].tableId) |
|||
if (table && table.primaryDisplay) { |
|||
displayColumn = table.primaryDisplay |
|||
} |
|||
} |
|||
}) |
|||
|
|||
async function fetchLinkedRowsData(row, columnName) { |
|||
if (!row || !row._id) { |
|||
return [] |
|||
} |
|||
return await API.fetchRelationshipData({ |
|||
tableId: row.tableId, |
|||
rowId: row._id, |
|||
fieldName: columnName, |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
{#if linkedRows && linkedRows.length && displayColumn} |
|||
{#each linkedRows as linkedRow} |
|||
{#if linkedRow[displayColumn] != null && linkedRow[displayColumn] !== ''} |
|||
<div class="linked-row">{linkedRow[displayColumn]}</div> |
|||
{/if} |
|||
{/each} |
|||
{:else}{count} related row(s){/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
width: 100%; |
|||
} |
|||
|
|||
/* This styling is opinionated to ensure these always look consistent */ |
|||
.linked-row { |
|||
color: white; |
|||
background-color: #616161; |
|||
border-radius: var(--border-radius-xs); |
|||
padding: var(--spacing-xs) var(--spacing-s) calc(var(--spacing-xs) + 1px) |
|||
var(--spacing-s); |
|||
line-height: 1; |
|||
font-size: 0.8em; |
|||
font-family: var(--font-sans); |
|||
font-weight: 500; |
|||
} |
|||
</style> |
|||
@ -1,32 +0,0 @@ |
|||
<script> |
|||
export let columnName |
|||
export let row |
|||
|
|||
$: items = row?.[columnName] || [] |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
{#each items as item} |
|||
<div class="item">{item?.primaryDisplay ?? ''}</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
width: 100%; |
|||
} |
|||
|
|||
.item { |
|||
font-size: var(--font-size-xs); |
|||
padding: var(--spacing-xs) var(--spacing-s); |
|||
border: 1px solid var(--grey-5); |
|||
color: var(--grey-7); |
|||
line-height: normal; |
|||
border-radius: 4px; |
|||
} |
|||
</style> |
|||
@ -1,17 +0,0 @@ |
|||
<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> |
|||
@ -1,19 +0,0 @@ |
|||
<script> |
|||
import { Button } from "@budibase/bbui" |
|||
|
|||
export let url |
|||
export let SDK |
|||
|
|||
const { linkable } = SDK |
|||
|
|||
let link |
|||
</script> |
|||
|
|||
<a href={url} bind:this={link} use:linkable /> |
|||
<Button small translucent on:click={() => link.click()}>View</Button> |
|||
|
|||
<style> |
|||
a { |
|||
display: none; |
|||
} |
|||
</style> |
|||
@ -1,169 +0,0 @@ |
|||
// Custom renderers to handle special types
|
|||
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
|
|||
|
|||
import AttachmentCell from "./AttachmentCell/Button.svelte" |
|||
import ViewDetails from "./ViewDetails/Cell.svelte" |
|||
import Select from "./Select/Wrapper.svelte" |
|||
import DatePicker from "./DateTime/Wrapper.svelte" |
|||
import RelationshipLabel from "./Relationship/RelationshipLabel.svelte" |
|||
|
|||
const renderers = new Map([ |
|||
["boolean", booleanRenderer], |
|||
["attachment", attachmentRenderer], |
|||
["options", optionsRenderer], |
|||
["link", linkedRowRenderer], |
|||
["_id", viewDetailsRenderer], |
|||
]) |
|||
|
|||
export function getRenderer(schema, editable, SDK) { |
|||
if (renderers.get(schema.type)) { |
|||
return renderers.get(schema.type)( |
|||
schema.options, |
|||
schema.constraints, |
|||
editable, |
|||
SDK |
|||
) |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/* eslint-disable no-unused-vars */ |
|||
function booleanRenderer(options, constraints, editable, SDK) { |
|||
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(options, constraints, editable, SDK) { |
|||
return params => { |
|||
const container = document.createElement("div") |
|||
|
|||
const attachmentInstance = new AttachmentCell({ |
|||
target: container, |
|||
props: { |
|||
files: params.value || [], |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
const deleteFile = event => { |
|||
const newFilesArray = params.value.filter(file => file !== event.detail) |
|||
params.setValue(newFilesArray) |
|||
} |
|||
|
|||
attachmentInstance.$on("delete", deleteFile) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
/* eslint-disable no-unused-vars */ |
|||
function dateRenderer(options, constraints, editable, SDK) { |
|||
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.
|
|||
new DatePicker({ |
|||
target: container, |
|||
props: { |
|||
value: params.value, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
|
|||
function optionsRenderer(options, constraints, editable, SDK) { |
|||
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: constraints.inclusion, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
selectInstance.$on("change", change) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
/* eslint-disable no-unused-vars */ |
|||
function linkedRowRenderer(options, constraints, editable, SDK) { |
|||
return params => { |
|||
let container = document.createElement("div") |
|||
container.style.display = "grid" |
|||
container.style.placeItems = "center" |
|||
container.style.height = "100%" |
|||
|
|||
new RelationshipLabel({ |
|||
target: container, |
|||
props: { |
|||
row: params.data, |
|||
columnName: params.column.colId, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
|
|||
/* eslint-disable no-unused-vars */ |
|||
function viewDetailsRenderer(options, constraints, editable, SDK) { |
|||
return params => { |
|||
let container = document.createElement("div") |
|||
container.style.display = "grid" |
|||
container.style.alignItems = "center" |
|||
container.style.height = "100%" |
|||
|
|||
let url = "/" |
|||
if (options.detailUrl) { |
|||
url = options.detailUrl.replace(":id", params.data._id) |
|||
} |
|||
if (!url.startsWith("/")) { |
|||
url = `/${url}` |
|||
} |
|||
|
|||
new ViewDetails({ |
|||
target: container, |
|||
props: { |
|||
url, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
@ -1,6 +0,0 @@ |
|||
// https://www.ag-grid.com/javascript-grid-value-setters/
|
|||
// These handles values and makes sure they adhere to the data type provided by the table
|
|||
export const number = params => { |
|||
params.data[params.colDef.field] = parseFloat(params.newValue) |
|||
return true |
|||
} |
|||
Loading…
Reference in new issue