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