mirror of https://github.com/Budibase/budibase.git
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> |
|||
import { goto } from "@sveltech/routify" |
|||
$goto("../backend") |
|||
$goto("../data") |
|||
</script> |
|||
|
|||
<!-- routify:options index=false --> |
|||
|
|||
Loading…
Reference in new issue