forked from tsai/budibase
16 changed files with 141 additions and 630 deletions
@ -1,233 +0,0 @@ |
|||||
<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,29 @@ |
|||||
|
<script> |
||||
|
import Spinner from "components/common/Spinner.svelte" |
||||
|
import { fade } from "svelte/transition" |
||||
|
</script> |
||||
|
|
||||
|
<div class="ag-overlay-loading-center loading-container"> |
||||
|
<div transition:fade class="loading-overlay"> |
||||
|
<img |
||||
|
height="30" |
||||
|
width="30" |
||||
|
src="/_builder/assets/bb-logo.svg" |
||||
|
alt="Budibase icon" /> |
||||
|
<span> Loading Your Data </span> |
||||
|
<Spinner size="12" /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.loading-overlay { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
font-family: var(--font-sans); |
||||
|
font-weight: 500; |
||||
|
} |
||||
|
|
||||
|
.loading-overlay > * { |
||||
|
margin-right: var(--spacing-m); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,17 @@ |
|||||
|
import LoadingOverlay from "./LoadingOverlay.svelte" |
||||
|
|
||||
|
export default class LoadingOverlayWrapper { |
||||
|
init(params) { |
||||
|
this.gui = document.createElement("div") |
||||
|
new LoadingOverlay({ |
||||
|
target: this.gui, |
||||
|
props: { |
||||
|
params, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
getGui() { |
||||
|
return this.gui |
||||
|
} |
||||
|
} |
||||
@ -1,136 +0,0 @@ |
|||||
<script> |
|
||||
export let data |
|
||||
export let currentPage = 0 |
|
||||
export let pageItemCount |
|
||||
export let ITEMS_PER_PAGE |
|
||||
|
|
||||
let numPages = 0 |
|
||||
$: numPages = Math.ceil((data?.length ?? 0) / ITEMS_PER_PAGE) |
|
||||
$: displayAllPages = numPages <= 10 |
|
||||
$: pagesAroundCurrent = getPagesAroundCurrent(currentPage, numPages) |
|
||||
|
|
||||
const next = () => { |
|
||||
if (currentPage + 1 === numPages) return |
|
||||
currentPage = currentPage + 1 |
|
||||
} |
|
||||
|
|
||||
const previous = () => { |
|
||||
if (currentPage == 0) return |
|
||||
currentPage = currentPage - 1 |
|
||||
} |
|
||||
|
|
||||
const selectPage = page => { |
|
||||
currentPage = page |
|
||||
} |
|
||||
|
|
||||
function getPagesAroundCurrent(current, max) { |
|
||||
const start = Math.max(current - 2, 1) |
|
||||
const end = Math.min(current + 2, max - 2) |
|
||||
let pages = [] |
|
||||
for (let i = start; i <= end; i++) { |
|
||||
pages.push(i) |
|
||||
} |
|
||||
return pages |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="pagination"> |
|
||||
<div class="pagination__buttons"> |
|
||||
<button on:click={previous} disabled={currentPage === 0}><</button> |
|
||||
{#if displayAllPages} |
|
||||
{#each Array(numPages) as _, idx} |
|
||||
<button |
|
||||
class:selected={idx === currentPage} |
|
||||
on:click={() => selectPage(idx)}> |
|
||||
{idx + 1} |
|
||||
</button> |
|
||||
{/each} |
|
||||
{:else} |
|
||||
<button class:selected={currentPage === 0} on:click={() => selectPage(0)}> |
|
||||
1 |
|
||||
</button> |
|
||||
{#if currentPage > 3}<button disabled>...</button>{/if} |
|
||||
{#each pagesAroundCurrent as idx} |
|
||||
<button |
|
||||
class:selected={idx === currentPage} |
|
||||
on:click={() => selectPage(idx)}> |
|
||||
{idx + 1} |
|
||||
</button> |
|
||||
{/each} |
|
||||
{#if currentPage < numPages - 4}<button disabled>...</button>{/if} |
|
||||
<button |
|
||||
class:selected={currentPage === numPages - 1} |
|
||||
on:click={() => selectPage(numPages - 1)}> |
|
||||
{numPages} |
|
||||
</button> |
|
||||
{/if} |
|
||||
<button |
|
||||
on:click={next} |
|
||||
disabled={currentPage === numPages - 1 || numPages === 0}> |
|
||||
> |
|
||||
</button> |
|
||||
</div> |
|
||||
|
|
||||
<p> |
|
||||
{#if numPages > 1} |
|
||||
Showing |
|
||||
{ITEMS_PER_PAGE * currentPage + 1} |
|
||||
- |
|
||||
{ITEMS_PER_PAGE * currentPage + pageItemCount} |
|
||||
of |
|
||||
{data.length} |
|
||||
rows |
|
||||
{:else if numPages === 1}Showing all {data.length} row(s){/if} |
|
||||
</p> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.pagination { |
|
||||
width: 100%; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
} |
|
||||
|
|
||||
.pagination__buttons { |
|
||||
display: flex; |
|
||||
border: 1px solid var(--grey-4); |
|
||||
border-radius: var(--border-radius-s); |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
|
|
||||
.pagination__buttons button { |
|
||||
display: inline-block; |
|
||||
padding: var(--spacing-s) 0; |
|
||||
text-align: center; |
|
||||
margin: 0; |
|
||||
background: #fff; |
|
||||
border: none; |
|
||||
outline: none; |
|
||||
border-right: 1px solid var(--grey-4); |
|
||||
text-transform: capitalize; |
|
||||
min-width: 20px; |
|
||||
transition: 0.3s background-color; |
|
||||
font-family: var(--font-sans); |
|
||||
color: var(--grey-6); |
|
||||
width: 40px; |
|
||||
} |
|
||||
.pagination__buttons button:last-child { |
|
||||
border-right: none; |
|
||||
} |
|
||||
.pagination__buttons button:hover { |
|
||||
cursor: pointer; |
|
||||
background-color: var(--grey-1); |
|
||||
} |
|
||||
.pagination__buttons button.selected { |
|
||||
background: var(--blue); |
|
||||
color: white; |
|
||||
} |
|
||||
|
|
||||
p { |
|
||||
font-size: var(--font-size-s); |
|
||||
margin: var(--spacing-xl) 0; |
|
||||
color: var(--grey-6); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,145 +0,0 @@ |
|||||
<script> |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" |
|
||||
import { FIELDS } from "constants/backend" |
|
||||
import CreateEditColumnPopover from "./CreateEditColumnPopover.svelte" |
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
|
||||
import { notifier } from "../../../../builderStore/store/notifications" |
|
||||
|
|
||||
export let field |
|
||||
|
|
||||
let anchor |
|
||||
let dropdown |
|
||||
let editing |
|
||||
let confirmDeleteDialog |
|
||||
|
|
||||
$: sortColumn = $backendUiStore.sort && $backendUiStore.sort.column |
|
||||
$: sortDirection = $backendUiStore.sort && $backendUiStore.sort.direction |
|
||||
$: type = field?.type |
|
||||
|
|
||||
function showEditor() { |
|
||||
editing = true |
|
||||
} |
|
||||
|
|
||||
function hideEditor() { |
|
||||
dropdown.hide() |
|
||||
editing = false |
|
||||
} |
|
||||
|
|
||||
function showDelete() { |
|
||||
dropdown.hide() |
|
||||
confirmDeleteDialog.show() |
|
||||
} |
|
||||
|
|
||||
function deleteColumn() { |
|
||||
if (field.name === $backendUiStore.selectedTable.primaryDisplay) { |
|
||||
notifier.danger("You cannot delete the display column") |
|
||||
} else { |
|
||||
backendUiStore.actions.tables.deleteField(field) |
|
||||
notifier.success("Column deleted") |
|
||||
} |
|
||||
hideEditor() |
|
||||
} |
|
||||
|
|
||||
function sort(direction, column) { |
|
||||
backendUiStore.update(state => { |
|
||||
if (direction !== "none") { |
|
||||
state.sort = { direction, column } |
|
||||
} else { |
|
||||
state.sort = undefined |
|
||||
} |
|
||||
return state |
|
||||
}) |
|
||||
hideEditor() |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="container" bind:this={anchor} on:click={dropdown.show}> |
|
||||
<span>{field.name}</span> |
|
||||
<Icon name="arrowdown" /> |
|
||||
</div> |
|
||||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|
||||
{#if editing} |
|
||||
<h5>Edit Column</h5> |
|
||||
<CreateEditColumnPopover onClosed={hideEditor} {field} /> |
|
||||
{:else} |
|
||||
<ul> |
|
||||
{#if type !== 'link'} |
|
||||
<li data-cy="edit-column-header" on:click={showEditor}> |
|
||||
<Icon name="edit" /> |
|
||||
Edit |
|
||||
</li> |
|
||||
{/if} |
|
||||
<li data-cy="delete-column-header" on:click={showDelete}> |
|
||||
<Icon name="delete" /> |
|
||||
Delete |
|
||||
</li> |
|
||||
{#if sortDirection === 'desc' || sortDirection === 'asc'} |
|
||||
<li on:click={() => sort('none', field.name)}> |
|
||||
<Icon name="close" /> |
|
||||
Remove sort |
|
||||
</li> |
|
||||
{/if} |
|
||||
{#if sortDirection === 'desc' || sortColumn !== field.name} |
|
||||
<li on:click={() => sort('asc', field.name)}> |
|
||||
<Icon name="sortascending" /> |
|
||||
Sort A - Z |
|
||||
</li> |
|
||||
{/if} |
|
||||
{#if sortDirection === 'asc' || sortColumn !== field.name} |
|
||||
<li on:click={() => sort('desc', field.name)}> |
|
||||
<Icon name="sortdescending" /> |
|
||||
Sort Z - A |
|
||||
</li> |
|
||||
{/if} |
|
||||
</ul> |
|
||||
{/if} |
|
||||
</DropdownMenu> |
|
||||
<ConfirmDialog |
|
||||
bind:this={confirmDeleteDialog} |
|
||||
body={`Are you sure you wish to delete this column? Your data will be deleted and this action cannot be undone.`} |
|
||||
okText="Delete Column" |
|
||||
onOk={deleteColumn} |
|
||||
title="Confirm Delete" /> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: flex-start; |
|
||||
align-items: center; |
|
||||
gap: var(--spacing-xs); |
|
||||
} |
|
||||
|
|
||||
h5 { |
|
||||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|
||||
margin: 0; |
|
||||
font-weight: 500; |
|
||||
} |
|
||||
|
|
||||
ul { |
|
||||
list-style: none; |
|
||||
padding-left: 0; |
|
||||
margin: 0; |
|
||||
padding: var(--spacing-s) 0; |
|
||||
} |
|
||||
|
|
||||
li { |
|
||||
display: flex; |
|
||||
font-family: var(--font-sans); |
|
||||
font-size: var(--font-size-xs); |
|
||||
color: var(--ink); |
|
||||
padding: var(--spacing-s) var(--spacing-m); |
|
||||
margin: auto 0px; |
|
||||
align-items: center; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
li:hover { |
|
||||
background-color: var(--grey-2); |
|
||||
} |
|
||||
|
|
||||
li:active { |
|
||||
color: var(--blue); |
|
||||
} |
|
||||
</style> |
|
||||
Loading…
Reference in new issue