rename data design, ag grid mvp

This commit is contained in:
Martin McKeaveney 2020-10-21 11:15:02 +01:00
parent ce5dca72b4
commit 1add16147d
29 changed files with 636 additions and 100 deletions

View File

@ -66,6 +66,7 @@
"@budibase/bbui": "^1.44.1", "@budibase/bbui": "^1.44.1",
"@budibase/client": "^0.2.5", "@budibase/client": "^0.2.5",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@budibase/svelte-ag-grid": "^0.0.13",
"@fortawesome/fontawesome-free": "^5.14.0", "@fortawesome/fontawesome-free": "^5.14.0",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0", "@svelteschool/svelte-forms": "^0.7.0",

View File

@ -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>

View File

@ -8,8 +8,6 @@
import api from "builderStore/api" import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui" import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte"
import CreateEditRowModal from "./modals/CreateEditRowModal.svelte" import CreateEditRowModal from "./modals/CreateEditRowModal.svelte"
import RowPopover from "./buttons/CreateRowButton.svelte" import RowPopover from "./buttons/CreateRowButton.svelte"
import ColumnPopover from "./buttons/CreateColumnButton.svelte" import ColumnPopover from "./buttons/CreateColumnButton.svelte"
@ -19,7 +17,13 @@
import CalculationPopover from "./buttons/CalculateButton.svelte" import CalculationPopover from "./buttons/CalculateButton.svelte"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
const ITEMS_PER_PAGE = 10 // New
import AgGrid from "@budibase/svelte-ag-grid"
import { getRenderer, editRowRenderer } from "./cells/cellRenderers";
import TableHeader from "./TableHeader"
// const ITEMS_PER_PAGE = 10
export let schema = [] export let schema = []
export let data = [] export let data = []
@ -27,18 +31,61 @@
export let allowEditing = false export let allowEditing = false
export let loading = false export let loading = false
let currentPage = 0 // New stuff
export let theme = "alpine"
$: columns = schema ? Object.keys(schema) : [] let columnDefs = []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data let options = {
$: paginatedData = defaultColDef: {
sorted && sorted.length flex: 1,
? sorted.slice( minWidth: 150,
currentPage * ITEMS_PER_PAGE, filter: true,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE },
) rowSelection: "multiple",
: [] suppressRowClickSelection: false,
paginationAutoPageSize: true,
}
// 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
// )
// : []
// TODO: refactor
$: {
let result = []
if (allowEditing) {
result.push({
headerName: "Edit",
sortable: false,
resizable: false,
suppressMovable: true,
width: 10,
cellRenderer: editRowRenderer
})
}
columnDefs = [...result, ...Object.keys(schema).map(key => ({
// headerCheckboxSelection: i === 0 && canEdit,
// checkboxSelection: i === 0 && canEdit,
// valueSetter: setters.get(schema[key].type),
headerComponent: TableHeader,
headerName: key,
field: key,
// hide: shouldHideField(key),
sortable: true,
// editable: canEdit && schema[key].type !== "link",
cellRenderer: getRenderer(schema[key], true),
autoHeight: true,
resizable: true,
}))]
}
$: tableId = data?.length ? data[0].tableId : null $: tableId = data?.length ? data[0].tableId : null
function selectRelationship(row, fieldName) { function selectRelationship(row, fieldName) {
@ -46,9 +93,24 @@
return return
} }
$goto( $goto(
`/${$params.application}/backend/table/${tableId}/relationship/${row._id}/${fieldName}` `/${$params.application}/data/table/${tableId}/relationship/${row._id}/${fieldName}`
) )
} }
// New stuff
const deleteRows = async () => {
const response = await api.post(`/api/${tableId}/rows`, {
rows: selectedRows,
type: "delete",
})
data = data.filter(row => !selectedRows.includes(row))
selectedRows = []
}
const handleUpdate = ({ detail }) => {
data[detail.row] = detail.data
updateRow(detail.data)
}
</script> </script>
<section> <section>
@ -65,7 +127,14 @@
<slot /> <slot />
</div> </div>
</div> </div>
<table class="bb-table"> <AgGrid
{theme}
{options}
{data}
{columnDefs}
on:update={handleUpdate}
on:select={({ detail }) => (console.log(detail))} />
<!-- <table class="bb-table">
<thead> <thead>
<tr> <tr>
{#if allowEditing} {#if allowEditing}
@ -119,12 +188,12 @@
</tr> </tr>
{/each} {/each}
</tbody> </tbody>
</table> </table> -->
<TablePagination <!-- <TablePagination
{data} {data}
bind:currentPage bind:currentPage
pageItemCount={paginatedData.length} pageItemCount={paginatedData.length}
{ITEMS_PER_PAGE} /> {ITEMS_PER_PAGE} /> -->
</section> </section>
<style> <style>
@ -142,64 +211,6 @@
margin-right: var(--spacing-xs); 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 { .table-controls {
width: 100%; width: 100%;
} }
@ -212,22 +223,4 @@
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
margin-bottom: var(--spacing-xl); 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> </style>

View File

@ -0,0 +1,12 @@
<script>
export let params
console.log("in svelte", params)
</script>
<h1>Fackle</h1>
<style>
h1 {
color: rebeccapurple;
}
</style>

View File

@ -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")
}
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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]
}

View File

@ -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 -->

View File

@ -718,10 +718,10 @@
svelte-flatpickr "^2.4.0" svelte-flatpickr "^2.4.0"
svelte-portal "^1.0.0" svelte-portal "^1.0.0"
"@budibase/client@^0.2.4": "@budibase/client@^0.2.5":
version "0.2.4" version "0.2.5"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.2.4.tgz#da958faa50c59f6a9c41c692b7a19d6a6ea98bc1" resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.2.5.tgz#d4f451384e88277dad16069c1d4742d5010ac1c3"
integrity sha512-MsFbWcsh3t1lyLgTb4UMccjshy6jd3A77lqs1CpXjHr+2LmXwvIriLgruycAvFrtqZzYG+dGe0rWwX0auwaaZw== integrity sha512-EFfy3g44fiHnzm/kqHskeXsbVXWHQbtKe4fPP0q+wwDkp1eP2ri9oJ3kMJg3A1SZPHFGmgeRvSMoLSux+7QhCw==
dependencies: dependencies:
deep-equal "^2.0.1" deep-equal "^2.0.1"
mustache "^4.0.1" mustache "^4.0.1"
@ -731,6 +731,13 @@
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810" resolved "https://registry.yarnpkg.com/@budibase/colorpicker/-/colorpicker-1.0.1.tgz#940c180e7ebba0cb0756c4c8ef13f5dfab58e810"
"@budibase/svelte-ag-grid@^0.0.13":
version "0.0.13"
resolved "https://registry.yarnpkg.com/@budibase/svelte-ag-grid/-/svelte-ag-grid-0.0.13.tgz#cbb49e2c8770dd9de51bf222423d0dede3f39207"
integrity sha512-pLIUsbQ57gFQThu7/MwuPXPTI1AnDrEzw2IHeHvli4VKq7DYcXbkAFs6h8pDPvbFOC6LaiVjRCnsEpEQI1x7og==
dependencies:
ag-grid-community "^24.0.0"
"@cnakazawa/watch@^1.0.3": "@cnakazawa/watch@^1.0.3":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
@ -1306,6 +1313,11 @@ acorn@^7.1.1:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
ag-grid-community@^24.0.0:
version "24.1.0"
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-24.1.0.tgz#1e3cab51211822e08d56f03a491b7c0deaa398e6"
integrity sha512-pWnWphuDcejZ8ahf6C734EpCx3XQ6dHEZWMWTlCdHNT0mZBLJ4YKCGACX+ttAEtSX2MGM3G13JncvuratUlYag==
ajv@^6.5.5: ajv@^6.5.5:
version "6.12.2" version "6.12.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"