forked from tsai/budibase
29 changed files with 637 additions and 101 deletions
@ -0,0 +1,233 @@ |
|||||
|
<script> |
||||
|
import { goto, params } from "@sveltech/routify" |
||||
|
import { onMount } from "svelte" |
||||
|
import { fade } from "svelte/transition" |
||||
|
import fsort from "fast-sort" |
||||
|
import getOr from "lodash/fp/getOr" |
||||
|
import { store, backendUiStore } from "builderStore" |
||||
|
import api from "builderStore/api" |
||||
|
import { Button, Icon } from "@budibase/bbui" |
||||
|
import ActionButton from "components/common/ActionButton.svelte" |
||||
|
import AttachmentList from "./AttachmentList.svelte" |
||||
|
import TablePagination from "./TablePagination.svelte" |
||||
|
import CreateEditRowModal from "./modals/CreateEditRowModal.svelte" |
||||
|
import RowPopover from "./buttons/CreateRowButton.svelte" |
||||
|
import ColumnPopover from "./buttons/CreateColumnButton.svelte" |
||||
|
import ViewPopover from "./buttons/CreateViewButton.svelte" |
||||
|
import ColumnHeaderPopover from "./popovers/ColumnPopover.svelte" |
||||
|
import EditRowPopover from "./popovers/RowPopover.svelte" |
||||
|
import CalculationPopover from "./buttons/CalculateButton.svelte" |
||||
|
import Spinner from "components/common/Spinner.svelte" |
||||
|
|
||||
|
const ITEMS_PER_PAGE = 10 |
||||
|
|
||||
|
export let schema = [] |
||||
|
export let data = [] |
||||
|
export let title |
||||
|
export let allowEditing = false |
||||
|
export let loading = false |
||||
|
|
||||
|
let currentPage = 0 |
||||
|
|
||||
|
$: columns = schema ? Object.keys(schema) : [] |
||||
|
$: sort = $backendUiStore.sort |
||||
|
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data |
||||
|
$: paginatedData = |
||||
|
sorted && sorted.length |
||||
|
? sorted.slice( |
||||
|
currentPage * ITEMS_PER_PAGE, |
||||
|
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE |
||||
|
) |
||||
|
: [] |
||||
|
$: tableId = data?.length ? data[0].tableId : null |
||||
|
|
||||
|
function selectRelationship(row, fieldName) { |
||||
|
if (!row?.[fieldName]?.length) { |
||||
|
return |
||||
|
} |
||||
|
$goto( |
||||
|
`/${$params.application}/backend/table/${tableId}/relationship/${row._id}/${fieldName}` |
||||
|
) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<section> |
||||
|
<div class="table-controls"> |
||||
|
<h2 class="title"> |
||||
|
<span>{title}</span> |
||||
|
{#if loading} |
||||
|
<div transition:fade> |
||||
|
<Spinner size="10" /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</h2> |
||||
|
<div class="popovers"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<table class="bb-table"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
{#if allowEditing} |
||||
|
<th class="edit-header"> |
||||
|
<div>Edit</div> |
||||
|
</th> |
||||
|
{/if} |
||||
|
{#each columns as header} |
||||
|
<th> |
||||
|
{#if allowEditing} |
||||
|
<ColumnHeaderPopover field={schema[header]} /> |
||||
|
{:else} |
||||
|
<div class="header">{header}</div> |
||||
|
{/if} |
||||
|
</th> |
||||
|
{/each} |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{#if paginatedData.length === 0} |
||||
|
{#if allowEditing} |
||||
|
<td class="no-border">No data.</td> |
||||
|
{/if} |
||||
|
{#each columns as header, idx} |
||||
|
<td class="no-border"> |
||||
|
{#if idx === 0 && !allowEditing}No data.{/if} |
||||
|
</td> |
||||
|
{/each} |
||||
|
{/if} |
||||
|
{#each paginatedData as row} |
||||
|
<tr> |
||||
|
{#if allowEditing} |
||||
|
<td> |
||||
|
<EditRowPopover {row} /> |
||||
|
</td> |
||||
|
{/if} |
||||
|
{#each columns as header} |
||||
|
<td> |
||||
|
{#if schema[header].type === 'link'} |
||||
|
<div |
||||
|
class:link={row[header] && row[header].length} |
||||
|
on:click={() => selectRelationship(row, header)}> |
||||
|
{row[header] ? row[header].length : 0} |
||||
|
related row(s) |
||||
|
</div> |
||||
|
{:else if schema[header].type === 'attachment'} |
||||
|
<AttachmentList files={row[header] || []} /> |
||||
|
{:else}{getOr('', header, row)}{/if} |
||||
|
</td> |
||||
|
{/each} |
||||
|
</tr> |
||||
|
{/each} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
<TablePagination |
||||
|
{data} |
||||
|
bind:currentPage |
||||
|
pageItemCount={paginatedData.length} |
||||
|
{ITEMS_PER_PAGE} /> |
||||
|
</section> |
||||
|
|
||||
|
<style> |
||||
|
.title { |
||||
|
font-size: 24px; |
||||
|
font-weight: 600; |
||||
|
text-rendering: optimizeLegibility; |
||||
|
margin-top: 0; |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: flex-start; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.title > span { |
||||
|
margin-right: var(--spacing-xs); |
||||
|
} |
||||
|
|
||||
|
table { |
||||
|
border: 1px solid var(--grey-4); |
||||
|
background: #fff; |
||||
|
border-collapse: collapse; |
||||
|
margin-top: 0; |
||||
|
} |
||||
|
|
||||
|
thead { |
||||
|
background: var(--grey-3); |
||||
|
border: 1px solid var(--grey-4); |
||||
|
} |
||||
|
|
||||
|
thead th { |
||||
|
color: var(--ink); |
||||
|
font-weight: 500; |
||||
|
font-size: 14px; |
||||
|
text-rendering: optimizeLegibility; |
||||
|
transition: 0.5s all; |
||||
|
vertical-align: middle; |
||||
|
height: 48px; |
||||
|
padding-top: 0; |
||||
|
padding-bottom: 0; |
||||
|
} |
||||
|
|
||||
|
thead th:hover { |
||||
|
color: var(--blue); |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.header { |
||||
|
text-transform: capitalize; |
||||
|
} |
||||
|
|
||||
|
td { |
||||
|
max-width: 200px; |
||||
|
text-overflow: ellipsis; |
||||
|
overflow: hidden; |
||||
|
border: 1px solid var(--grey-4); |
||||
|
white-space: nowrap; |
||||
|
box-sizing: border-box; |
||||
|
padding: var(--spacing-l) var(--spacing-m); |
||||
|
font-size: var(--font-size-xs); |
||||
|
} |
||||
|
|
||||
|
td.no-border { |
||||
|
border: none; |
||||
|
} |
||||
|
|
||||
|
tbody tr { |
||||
|
border-bottom: 1px solid var(--grey-4); |
||||
|
transition: 0.3s background-color; |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
tbody tr:hover { |
||||
|
background: var(--grey-1); |
||||
|
} |
||||
|
|
||||
|
.table-controls { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.popovers { |
||||
|
display: flex; |
||||
|
} |
||||
|
|
||||
|
:global(.popovers > div) { |
||||
|
margin-right: var(--spacing-m); |
||||
|
margin-bottom: var(--spacing-xl); |
||||
|
} |
||||
|
|
||||
|
.edit-header { |
||||
|
width: 60px; |
||||
|
} |
||||
|
|
||||
|
.edit-header:hover { |
||||
|
cursor: default; |
||||
|
color: var(--ink); |
||||
|
} |
||||
|
|
||||
|
.link { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
|
||||
|
.link:hover { |
||||
|
color: var(--grey-6); |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,12 @@ |
|||||
|
<script> |
||||
|
export let params |
||||
|
console.log("in svelte", params) |
||||
|
</script> |
||||
|
|
||||
|
<h1>Fackle</h1> |
||||
|
|
||||
|
<style> |
||||
|
h1 { |
||||
|
color: rebeccapurple; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,67 @@ |
|||||
|
// // the column the header is for
|
||||
|
// column: Column;
|
||||
|
|
||||
|
// // the name to display for the column. if the column is using a headerValueGetter,
|
||||
|
// // the displayName will take this into account.
|
||||
|
// displayName: string;
|
||||
|
|
||||
|
// // whether sorting is enabled for the column. only put sort logic into
|
||||
|
// // your header if this is true.
|
||||
|
// enableSorting: boolean;
|
||||
|
|
||||
|
// // whether menu is enabled for the column. only display a menu button
|
||||
|
// // in your header if this is true.
|
||||
|
// enableMenu: boolean;
|
||||
|
|
||||
|
// // callback to progress the sort for this column.
|
||||
|
// // the grid will decide the next sort direction eg ascending, descending or 'no sort'.
|
||||
|
// // pass multiSort=true if you want to do a multi sort (eg user has shift held down when
|
||||
|
// // they click)
|
||||
|
// progressSort(multiSort: boolean): void;
|
||||
|
|
||||
|
// // callback to set the sort for this column.
|
||||
|
// // pass the sort direction to use ignoring the current sort eg one of 'asc', 'desc' or null
|
||||
|
// // (for no sort). pass multiSort=true if you want to do a multi sort (eg user has shift held
|
||||
|
// // down when they click)
|
||||
|
// setSort(sort: string, multiSort?: boolean): void;
|
||||
|
|
||||
|
// // callback to request the grid to show the column menu.
|
||||
|
// // pass in the html element of the column menu to have the
|
||||
|
// // grid position the menu over the button.
|
||||
|
// showColumnMenu(menuButton: HTMLElement): void;
|
||||
|
|
||||
|
// // The grid API
|
||||
|
// api: any;
|
||||
|
import TableHeader from "./TableHeader.svelte" |
||||
|
|
||||
|
export default class TableHeaderWrapper { |
||||
|
constructor() { |
||||
|
// foo
|
||||
|
} |
||||
|
|
||||
|
init(params) { |
||||
|
console.log("init", params) |
||||
|
this.agParams = params |
||||
|
const container = document.createElement("div") |
||||
|
new TableHeader({ |
||||
|
target: container, |
||||
|
props: params, |
||||
|
}) |
||||
|
this.eGui = container |
||||
|
} |
||||
|
|
||||
|
// can get called more than once, you should return the HTML element
|
||||
|
getGui() { |
||||
|
return this.eGui |
||||
|
} |
||||
|
|
||||
|
// gets called when a new Column Definition has been set for this header
|
||||
|
refresh(params) { |
||||
|
console.log("Refreshing", params) |
||||
|
} |
||||
|
|
||||
|
// optional method, gets called once, when component is destroyed
|
||||
|
destroy() { |
||||
|
console.log("Destroy") |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import api from "builderStore/api" |
||||
|
import { getTable } from "./tableCache" |
||||
|
|
||||
|
export let columnName |
||||
|
export let row |
||||
|
|
||||
|
$: count = |
||||
|
row && columnName && Array.isArray(row[columnName]) |
||||
|
? row[columnName].length |
||||
|
: 0 |
||||
|
let linkedRows = [] |
||||
|
let displayColumn |
||||
|
|
||||
|
onMount(async () => { |
||||
|
linkedRows = await fetchLinkedRowsData(row, columnName) |
||||
|
if (linkedRows && linkedRows.length) { |
||||
|
const table = await getTable(linkedRows[0].tableId) |
||||
|
if (table && table.primaryDisplay) { |
||||
|
displayColumn = table.primaryDisplay |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
async function fetchLinkedRowsData(row, columnName) { |
||||
|
if (!row || !row._id) { |
||||
|
return [] |
||||
|
} |
||||
|
const QUERY_URL = `/api/${row.tableId}/${row._id}/enrich` |
||||
|
const response = await api.get(QUERY_URL) |
||||
|
const enrichedRow = await response.json() |
||||
|
return enrichedRow[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> |
||||
@ -0,0 +1,129 @@ |
|||||
|
import AttachmentList from "./AttachmentCell.svelte" |
||||
|
import EditRowPopover from "../popovers/RowPopover.svelte" |
||||
|
import RelationshipDisplay from "./RelationshipCell.svelte" |
||||
|
// import BooleanCell from "./BooleanCell.svelte"
|
||||
|
|
||||
|
const renderers = { |
||||
|
attachment: attachmentRenderer, |
||||
|
link: linkedRowRenderer, |
||||
|
boolean: booleanRenderer, |
||||
|
} |
||||
|
|
||||
|
export function getRenderer(schema, editable) { |
||||
|
if (renderers[schema.type]) { |
||||
|
return renderers[schema.type](schema.options, schema.constraints, editable) |
||||
|
} else { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function editRowRenderer(params) { |
||||
|
const container = document.createElement("div") |
||||
|
|
||||
|
new EditRowPopover({ |
||||
|
target: container, |
||||
|
props: { |
||||
|
row: params.data, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
return container |
||||
|
} |
||||
|
|
||||
|
/* eslint-disable no-unused-vars */ |
||||
|
function booleanRenderer(options, constraints) { |
||||
|
return params => { |
||||
|
let container = document.createElement("input") |
||||
|
|
||||
|
// TODO: implement
|
||||
|
|
||||
|
return container |
||||
|
|
||||
|
// 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) { |
||||
|
return params => { |
||||
|
const container = document.createElement("div") |
||||
|
|
||||
|
const attachmentInstance = new AttachmentList({ |
||||
|
target: container, |
||||
|
props: { |
||||
|
files: params.value || [], |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
// 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) {
|
||||
|
// 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,
|
||||
|
// },
|
||||
|
// })
|
||||
|
|
||||
|
// return container
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
function optionsRenderer(options, constraints, editable) { |
||||
|
return params => { |
||||
|
const container = document.createElement("div") |
||||
|
// TODO: show a pill
|
||||
|
return container |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* eslint-disable no-unused-vars */ |
||||
|
function linkedRowRenderer(options, constraints, editable) { |
||||
|
return params => { |
||||
|
let container = document.createElement("div") |
||||
|
container.style.display = "grid" |
||||
|
container.style.placeItems = "center" |
||||
|
container.style.height = "100%" |
||||
|
|
||||
|
new RelationshipDisplay({ |
||||
|
target: container, |
||||
|
props: { |
||||
|
row: params.data, |
||||
|
columnName: params.column.colId, |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
return container |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
let cache = {} |
||||
|
|
||||
|
async function fetchTable(id) { |
||||
|
const FETCH_TABLE_URL = `/api/tables/${id}` |
||||
|
const response = await api.get(FETCH_TABLE_URL) |
||||
|
return await response.json() |
||||
|
} |
||||
|
|
||||
|
export async function getTable(tableId) { |
||||
|
if (!tableId) { |
||||
|
return null |
||||
|
} |
||||
|
if (!cache[tableId]) { |
||||
|
cache[tableId] = fetchTable(tableId) |
||||
|
cache[tableId] = await cache[tableId] |
||||
|
} |
||||
|
return await cache[tableId] |
||||
|
} |
||||
@ -1,6 +1,6 @@ |
|||||
<script> |
<script> |
||||
import { goto } from "@sveltech/routify" |
import { goto } from "@sveltech/routify" |
||||
$goto("../backend") |
$goto("../data") |
||||
</script> |
</script> |
||||
|
|
||||
<!-- routify:options index=false --> |
<!-- routify:options index=false --> |
||||
|
|||||
Loading…
Reference in new issue