mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
75 changed files with 1433 additions and 1471 deletions
@ -0,0 +1,25 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { makePropSafe } from "@budibase/string-templates" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { findComponentPath } from "builderStore/storeUtils" |
|||
|
|||
export let value |
|||
|
|||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId) |
|||
$: providers = path.filter( |
|||
component => |
|||
component._component === "@budibase/standard-components/dataprovider" |
|||
) |
|||
</script> |
|||
|
|||
<Select thin secondary {value} on:change> |
|||
<option value="">Choose option</option> |
|||
{#if providers} |
|||
{#each providers as component} |
|||
<option value={`{{ literal ${makePropSafe(component._id)} }}`}> |
|||
{component._instanceName} |
|||
</option> |
|||
{/each} |
|||
{/if} |
|||
</Select> |
|||
@ -1,7 +1,14 @@ |
|||
<div class="root">This action doesn't require any additional settings.</div> |
|||
<script> |
|||
import { Body } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Body small grey>This action doesn't require any additional settings.</Body> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
font-size: var(--font-size-s); |
|||
max-width: 800px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
|
|||
@ -1,7 +1,7 @@ |
|||
<script> |
|||
import DatasourceSelect from "./DatasourceSelect.svelte" |
|||
import DataSourceSelect from "./DataSourceSelect.svelte" |
|||
|
|||
const otherSources = [{ name: "Custom", label: "Custom" }] |
|||
</script> |
|||
|
|||
<DatasourceSelect on:change {...$$props} showAllQueries={true} {otherSources} /> |
|||
<DataSourceSelect on:change {...$$props} showAllQueries={true} {otherSources} /> |
|||
|
|||
@ -0,0 +1,84 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { notificationStore } from "./notification" |
|||
|
|||
export const createDataSourceStore = () => { |
|||
const store = writable([]) |
|||
|
|||
// Registers a new dataSource instance
|
|||
const registerDataSource = (dataSource, instanceId, refresh) => { |
|||
if (!dataSource || !instanceId || !refresh) { |
|||
return |
|||
} |
|||
|
|||
// Create a list of all relevant dataSource IDs which would require that
|
|||
// this dataSource is refreshed
|
|||
let dataSourceIds = [] |
|||
|
|||
// Extract table ID
|
|||
if (dataSource.type === "table" || dataSource.type === "view") { |
|||
if (dataSource.tableId) { |
|||
dataSourceIds.push(dataSource.tableId) |
|||
} |
|||
} |
|||
|
|||
// Extract both table IDs from both sides of the relationship
|
|||
else if (dataSource.type === "link") { |
|||
if (dataSource.rowTableId) { |
|||
dataSourceIds.push(dataSource.rowTableId) |
|||
} |
|||
if (dataSource.tableId) { |
|||
dataSourceIds.push(dataSource.tableId) |
|||
} |
|||
} |
|||
|
|||
// Extract the dataSource ID (not the query ID) for queries
|
|||
else if (dataSource.type === "query") { |
|||
if (dataSource.dataSourceId) { |
|||
dataSourceIds.push(dataSource.dataSourceId) |
|||
} |
|||
} |
|||
|
|||
// Store configs for each relevant dataSource ID
|
|||
if (dataSourceIds.length) { |
|||
store.update(state => { |
|||
dataSourceIds.forEach(id => { |
|||
state.push({ |
|||
dataSourceId: id, |
|||
instanceId, |
|||
refresh, |
|||
}) |
|||
}) |
|||
return state |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// Removes all registered dataSource instances belonging to a particular
|
|||
// instance ID
|
|||
const unregisterInstance = instanceId => { |
|||
store.update(state => { |
|||
return state.filter(instance => instance.instanceId !== instanceId) |
|||
}) |
|||
} |
|||
|
|||
// Invalidates a specific dataSource ID by refreshing all instances
|
|||
// which depend on data from that dataSource
|
|||
const invalidateDataSource = dataSourceId => { |
|||
const relatedInstances = get(store).filter(instance => { |
|||
return instance.dataSourceId === dataSourceId |
|||
}) |
|||
if (relatedInstances?.length) { |
|||
notificationStore.blockNotifications(1000) |
|||
} |
|||
relatedInstances?.forEach(instance => { |
|||
instance.refresh() |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
actions: { registerDataSource, unregisterInstance, invalidateDataSource }, |
|||
} |
|||
} |
|||
|
|||
export const dataSourceStore = createDataSourceStore() |
|||
@ -1,84 +0,0 @@ |
|||
import { writable, get } from "svelte/store" |
|||
import { notificationStore } from "./notification" |
|||
|
|||
export const createDatasourceStore = () => { |
|||
const store = writable([]) |
|||
|
|||
// Registers a new datasource instance
|
|||
const registerDatasource = (datasource, instanceId, refresh) => { |
|||
if (!datasource || !instanceId || !refresh) { |
|||
return |
|||
} |
|||
|
|||
// Create a list of all relevant datasource IDs which would require that
|
|||
// this datasource is refreshed
|
|||
let datasourceIds = [] |
|||
|
|||
// Extract table ID
|
|||
if (datasource.type === "table" || datasource.type === "view") { |
|||
if (datasource.tableId) { |
|||
datasourceIds.push(datasource.tableId) |
|||
} |
|||
} |
|||
|
|||
// Extract both table IDs from both sides of the relationship
|
|||
else if (datasource.type === "link") { |
|||
if (datasource.rowTableId) { |
|||
datasourceIds.push(datasource.rowTableId) |
|||
} |
|||
if (datasource.tableId) { |
|||
datasourceIds.push(datasource.tableId) |
|||
} |
|||
} |
|||
|
|||
// Extract the datasource ID (not the query ID) for queries
|
|||
else if (datasource.type === "query") { |
|||
if (datasource.datasourceId) { |
|||
datasourceIds.push(datasource.datasourceId) |
|||
} |
|||
} |
|||
|
|||
// Store configs for each relevant datasource ID
|
|||
if (datasourceIds.length) { |
|||
store.update(state => { |
|||
datasourceIds.forEach(id => { |
|||
state.push({ |
|||
datasourceId: id, |
|||
instanceId, |
|||
refresh, |
|||
}) |
|||
}) |
|||
return state |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// Removes all registered datasource instances belonging to a particular
|
|||
// instance ID
|
|||
const unregisterInstance = instanceId => { |
|||
store.update(state => { |
|||
return state.filter(instance => instance.instanceId !== instanceId) |
|||
}) |
|||
} |
|||
|
|||
// Invalidates a specific datasource ID by refreshing all instances
|
|||
// which depend on data from that datasource
|
|||
const invalidateDatasource = datasourceId => { |
|||
const relatedInstances = get(store).filter(instance => { |
|||
return instance.datasourceId === datasourceId |
|||
}) |
|||
if (relatedInstances?.length) { |
|||
notificationStore.blockNotifications(1000) |
|||
} |
|||
relatedInstances?.forEach(instance => { |
|||
instance.refresh() |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
actions: { registerDatasource, unregisterInstance, invalidateDatasource }, |
|||
} |
|||
} |
|||
|
|||
export const datasourceStore = createDatasourceStore() |
|||
@ -0,0 +1,108 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
export let dataSource |
|||
export let filter |
|||
export let sortColumn |
|||
export let sortOrder |
|||
export let limit |
|||
|
|||
const { API, styleable, Provider, ActionTypes } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
// Loading flag every time data is being fetched |
|||
let loading = false |
|||
|
|||
// Loading flag for the initial load |
|||
let loaded = false |
|||
|
|||
let allRows = [] |
|||
let schema = {} |
|||
|
|||
$: fetchData(dataSource) |
|||
$: filteredRows = filterRows(allRows, filter) |
|||
$: sortedRows = sortRows(filteredRows, sortColumn, sortOrder) |
|||
$: rows = limitRows(sortedRows, limit) |
|||
$: getSchema(dataSource) |
|||
$: actions = [ |
|||
{ |
|||
type: ActionTypes.RefreshDatasource, |
|||
callback: () => fetchData(dataSource), |
|||
metadata: { dataSource }, |
|||
}, |
|||
] |
|||
$: dataContext = { |
|||
rows, |
|||
schema, |
|||
rowsLength: rows.length, |
|||
loading, |
|||
loaded, |
|||
} |
|||
|
|||
const fetchData = async dataSource => { |
|||
loading = true |
|||
allRows = await API.fetchDatasource(dataSource) |
|||
loading = false |
|||
loaded = true |
|||
} |
|||
|
|||
const filterRows = (rows, filter) => { |
|||
if (!Object.keys(filter || {}).length) { |
|||
return rows |
|||
} |
|||
let filteredData = [...rows] |
|||
Object.entries(filter).forEach(([field, value]) => { |
|||
if (value != null && value !== "") { |
|||
filteredData = filteredData.filter(row => { |
|||
return row[field] === value |
|||
}) |
|||
} |
|||
}) |
|||
return filteredData |
|||
} |
|||
|
|||
const sortRows = (rows, sortColumn, sortOrder) => { |
|||
if (!sortColumn || !sortOrder) { |
|||
return rows |
|||
} |
|||
return rows.slice().sort((a, b) => { |
|||
const colA = a[sortColumn] |
|||
const colB = b[sortColumn] |
|||
if (sortOrder === "Descending") { |
|||
return colA > colB ? -1 : 1 |
|||
} else { |
|||
return colA > colB ? 1 : -1 |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const limitRows = (rows, limit) => { |
|||
const numLimit = parseFloat(limit) |
|||
if (isNaN(numLimit)) { |
|||
return rows |
|||
} |
|||
return rows.slice(0, numLimit) |
|||
} |
|||
|
|||
const getSchema = async dataSource => { |
|||
if (dataSource?.schema) { |
|||
schema = dataSource.schema |
|||
} else if (dataSource?.tableId) { |
|||
const definition = await API.fetchTableDefinition(dataSource.tableId) |
|||
schema = definition?.schema ?? {} |
|||
} else { |
|||
schema = {} |
|||
} |
|||
|
|||
// Ensure all schema fields have a name property |
|||
Object.entries(schema).forEach(([key, value]) => { |
|||
if (!value.name) { |
|||
value.name = key |
|||
} |
|||
}) |
|||
} |
|||
</script> |
|||
|
|||
<Provider {actions} data={dataContext}> |
|||
<slot /> |
|||
</Provider> |
|||
@ -1,83 +0,0 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
import { isEmpty } from "lodash/fp" |
|||
|
|||
export let datasource |
|||
export let noRowsMessage |
|||
export let filter |
|||
|
|||
const { API, styleable, Provider, builderStore, ActionTypes } = getContext( |
|||
"sdk" |
|||
) |
|||
const component = getContext("component") |
|||
let rows = [] |
|||
let loaded = false |
|||
|
|||
$: fetchData(datasource) |
|||
$: filteredRows = filterRows(rows, filter) |
|||
$: actions = [ |
|||
{ |
|||
type: ActionTypes.RefreshDatasource, |
|||
callback: () => fetchData(datasource), |
|||
metadata: { datasource }, |
|||
}, |
|||
] |
|||
|
|||
const fetchData = async datasource => { |
|||
if (!isEmpty(datasource)) { |
|||
rows = await API.fetchDatasource(datasource) |
|||
} |
|||
loaded = true |
|||
} |
|||
|
|||
const filterRows = (rows, filter) => { |
|||
if (!Object.keys(filter || {}).length) { |
|||
return rows |
|||
} |
|||
let filteredData = [...rows] |
|||
Object.entries(filter).forEach(([field, value]) => { |
|||
if (value != null && value !== "") { |
|||
filteredData = filteredData.filter(row => { |
|||
return row[field] === value |
|||
}) |
|||
} |
|||
}) |
|||
return filteredData |
|||
} |
|||
</script> |
|||
|
|||
<Provider {actions}> |
|||
<div use:styleable={$component.styles}> |
|||
{#if filteredRows.length > 0} |
|||
{#if $component.children === 0 && $builderStore.inBuilder} |
|||
<p><i class="ri-image-line" />Add some components to display.</p> |
|||
{:else} |
|||
{#each filteredRows as row} |
|||
<Provider data={row}> |
|||
<slot /> |
|||
</Provider> |
|||
{/each} |
|||
{/if} |
|||
{:else if loaded && noRowsMessage} |
|||
<p><i class="ri-list-check-2" />{noRowsMessage}</p> |
|||
{/if} |
|||
</div> |
|||
</Provider> |
|||
|
|||
<style> |
|||
p { |
|||
margin: 0 var(--spacing-m); |
|||
background-color: var(--grey-2); |
|||
color: var(--grey-6); |
|||
font-size: var(--font-size-s); |
|||
padding: var(--spacing-l); |
|||
border-radius: var(--border-radius-s); |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
p i { |
|||
margin-bottom: var(--spacing-m); |
|||
font-size: 1.5rem; |
|||
color: var(--grey-5); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,47 @@ |
|||
<script> |
|||
import { getContext } from "svelte" |
|||
|
|||
export let dataProvider |
|||
export let noRowsMessage |
|||
|
|||
const { API, styleable, builderStore, Provider } = getContext("sdk") |
|||
const component = getContext("component") |
|||
const context = getContext("context") |
|||
|
|||
$: rows = dataProvider?.rows ?? [] |
|||
$: loaded = dataProvider?.loaded ?? false |
|||
</script> |
|||
|
|||
<div use:styleable={$component.styles}> |
|||
{#if rows.length > 0} |
|||
{#if $component.children === 0 && $builderStore.inBuilder} |
|||
<p><i class="ri-image-line" />Add some components to display.</p> |
|||
{:else} |
|||
{#each rows as row} |
|||
<Provider data={row}> |
|||
<slot /> |
|||
</Provider> |
|||
{/each} |
|||
{/if} |
|||
{:else if loaded && noRowsMessage} |
|||
<p><i class="ri-list-check-2" />{noRowsMessage}</p> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
p { |
|||
margin: 0 var(--spacing-m); |
|||
background-color: var(--grey-2); |
|||
color: var(--grey-6); |
|||
font-size: var(--font-size-s); |
|||
padding: var(--spacing-l); |
|||
border-radius: var(--border-radius-s); |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
p i { |
|||
margin-bottom: var(--spacing-m); |
|||
font-size: 1.5rem; |
|||
color: var(--grey-5); |
|||
} |
|||
</style> |
|||
@ -1,57 +0,0 @@ |
|||
<script> |
|||
import { onMount, getContext } from "svelte" |
|||
|
|||
export let table |
|||
|
|||
const { |
|||
API, |
|||
screenStore, |
|||
routeStore, |
|||
Provider, |
|||
styleable, |
|||
ActionTypes, |
|||
} = getContext("sdk") |
|||
const component = getContext("component") |
|||
let headers = [] |
|||
let row |
|||
|
|||
const fetchFirstRow = async tableId => { |
|||
const rows = await API.fetchTableData(tableId) |
|||
return Array.isArray(rows) && rows.length ? rows[0] : { tableId } |
|||
} |
|||
|
|||
const fetchData = async (rowId, tableId) => { |
|||
if (!tableId) { |
|||
return |
|||
} |
|||
|
|||
const pathParts = window.location.pathname.split("/") |
|||
|
|||
// if srcdoc, then we assume this is the builder preview |
|||
if ((pathParts.length === 0 || pathParts[0] === "srcdoc") && tableId) { |
|||
row = await fetchFirstRow(tableId) |
|||
} else if (rowId) { |
|||
row = await API.fetchRow({ tableId, rowId }) |
|||
} else { |
|||
throw new Error("Row ID was not supplied to RowDetail") |
|||
} |
|||
} |
|||
|
|||
$: actions = [ |
|||
{ |
|||
type: ActionTypes.RefreshDatasource, |
|||
callback: () => fetchData($routeStore.routeParams.id, table), |
|||
metadata: { datasource: { type: "table", tableId: table } }, |
|||
}, |
|||
] |
|||
|
|||
onMount(() => fetchData($routeStore.routeParams.id, table)) |
|||
</script> |
|||
|
|||
{#if row} |
|||
<Provider data={row} {actions}> |
|||
<div use:styleable={$component.styles}> |
|||
<slot /> |
|||
</div> |
|||
</Provider> |
|||
{/if} |
|||
@ -1,6 +0,0 @@ |
|||
<script> |
|||
import AttachmentList from "../../attachments/AttachmentList.svelte" |
|||
export let files |
|||
</script> |
|||
|
|||
<AttachmentList {files} on:delete /> |
|||
@ -1,193 +0,0 @@ |
|||
<script> |
|||
// Import valueSetters and custom renderers |
|||
import { number } from "./valueSetters" |
|||
import { getRenderer } from "./customRenderer" |
|||
import { isEmpty } from "lodash/fp" |
|||
import { getContext } from "svelte" |
|||
import AgGrid from "@budibase/svelte-ag-grid" |
|||
import { |
|||
TextButton as DeleteButton, |
|||
Icon, |
|||
Modal, |
|||
ModalContent, |
|||
} from "@budibase/bbui" |
|||
|
|||
// These maps need to be set up to handle whatever types that are used in the tables. |
|||
const setters = new Map([["number", number]]) |
|||
const SDK = getContext("sdk") |
|||
const component = getContext("component") |
|||
const { API, styleable } = SDK |
|||
|
|||
export let datasource = {} |
|||
export let editable |
|||
export let theme = "alpine" |
|||
export let height = 500 |
|||
export let pagination |
|||
export let detailUrl |
|||
|
|||
// Add setting height as css var to allow grid to use correct height |
|||
$: gridStyles = { |
|||
...$component.styles, |
|||
normal: { |
|||
...$component.styles.normal, |
|||
["--grid-height"]: `${height}px`, |
|||
}, |
|||
} |
|||
$: fetchData(datasource) |
|||
|
|||
// These can never change at runtime so don't need to be reactive |
|||
let canEdit = editable && datasource && datasource.type !== "view" |
|||
let canAddDelete = editable && datasource && datasource.type === "table" |
|||
|
|||
let modal |
|||
let dataLoaded = false |
|||
let data |
|||
let columnDefs |
|||
let selectedRows = [] |
|||
let table |
|||
let options = { |
|||
defaultColDef: { |
|||
flex: 1, |
|||
minWidth: 150, |
|||
filter: true, |
|||
}, |
|||
rowSelection: canEdit ? "multiple" : false, |
|||
suppressFieldDotNotation: true, |
|||
suppressRowClickSelection: !canEdit, |
|||
paginationAutoPageSize: true, |
|||
pagination, |
|||
} |
|||
|
|||
async function fetchData(datasource) { |
|||
if (isEmpty(datasource)) { |
|||
return |
|||
} |
|||
data = await API.fetchDatasource(datasource) |
|||
|
|||
let schema |
|||
|
|||
// Get schema for datasource |
|||
// Views with "Calculate" applied provide their own schema. |
|||
// For everything else, use the tableId property to pull to table schema |
|||
if (datasource.schema) { |
|||
schema = datasource.schema |
|||
} else { |
|||
schema = (await API.fetchTableDefinition(datasource.tableId)).schema |
|||
} |
|||
|
|||
columnDefs = Object.keys(schema).map((key, i) => { |
|||
return { |
|||
headerCheckboxSelection: i === 0 && canEdit, |
|||
checkboxSelection: i === 0 && canEdit, |
|||
valueSetter: setters.get(schema[key].type), |
|||
headerName: key, |
|||
field: key, |
|||
hide: shouldHideField(key), |
|||
sortable: true, |
|||
editable: canEdit && schema[key].type !== "link", |
|||
cellRenderer: getRenderer(schema[key], canEdit, SDK), |
|||
autoHeight: true, |
|||
} |
|||
}) |
|||
|
|||
if (detailUrl) { |
|||
columnDefs = [ |
|||
...columnDefs, |
|||
{ |
|||
headerName: "Detail", |
|||
field: "_id", |
|||
minWidth: 100, |
|||
width: 100, |
|||
flex: 0, |
|||
editable: false, |
|||
sortable: false, |
|||
cellRenderer: getRenderer( |
|||
{ |
|||
type: "_id", |
|||
options: { detailUrl }, |
|||
}, |
|||
false, |
|||
SDK |
|||
), |
|||
autoHeight: true, |
|||
pinned: "left", |
|||
filter: false, |
|||
}, |
|||
] |
|||
} |
|||
|
|||
dataLoaded = true |
|||
} |
|||
|
|||
const shouldHideField = name => { |
|||
if (name.startsWith("_")) return true |
|||
// always 'row' |
|||
if (name === "type") return true |
|||
// tables are always tied to a single tableId, this is irrelevant |
|||
if (name === "tableId") return true |
|||
|
|||
return false |
|||
} |
|||
|
|||
const handleUpdate = ({ detail }) => { |
|||
data[detail.row] = detail.data |
|||
updateRow(detail.data) |
|||
} |
|||
|
|||
const updateRow = async row => { |
|||
await API.updateRow(row) |
|||
} |
|||
|
|||
const deleteRows = async () => { |
|||
await API.deleteRows({ rows: selectedRows, tableId: datasource.name }) |
|||
data = data.filter(row => !selectedRows.includes(row)) |
|||
selectedRows = [] |
|||
} |
|||
</script> |
|||
|
|||
<div class="container" use:styleable={gridStyles}> |
|||
{#if dataLoaded} |
|||
{#if canAddDelete} |
|||
<div class="controls"> |
|||
{#if selectedRows.length > 0} |
|||
<DeleteButton text small on:click={modal.show()}> |
|||
<Icon name="addrow" /> |
|||
Delete |
|||
{selectedRows.length} |
|||
row(s) |
|||
</DeleteButton> |
|||
{/if} |
|||
</div> |
|||
{/if} |
|||
<AgGrid |
|||
{theme} |
|||
{options} |
|||
{data} |
|||
{columnDefs} |
|||
on:update={handleUpdate} |
|||
on:select={({ detail }) => (selectedRows = detail)} /> |
|||
{/if} |
|||
<Modal bind:this={modal}> |
|||
<ModalContent |
|||
title="Confirm Row Deletion" |
|||
confirmText="Delete" |
|||
onConfirm={deleteRows}> |
|||
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span> |
|||
</ModalContent> |
|||
</Modal> |
|||
</div> |
|||
|
|||
<style> |
|||
.container :global(.ag-pinned-left-header .ag-header-cell-label) { |
|||
justify-content: center; |
|||
} |
|||
|
|||
.controls { |
|||
min-height: 15px; |
|||
margin-bottom: var(--spacing-s); |
|||
display: grid; |
|||
grid-gap: var(--spacing-s); |
|||
grid-template-columns: auto auto; |
|||
justify-content: start; |
|||
} |
|||
</style> |
|||
@ -1,37 +0,0 @@ |
|||
<script> |
|||
import { createEventDispatcher } from "svelte" |
|||
import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui" |
|||
import Modal from "./Modal.svelte" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
|
|||
let anchor |
|||
let dropdown |
|||
|
|||
export let table |
|||
</script> |
|||
|
|||
<div bind:this={anchor}> |
|||
<Button text small on:click={dropdown.show}> |
|||
<Icon name="addrow" /> |
|||
Create New Row |
|||
</Button> |
|||
</div> |
|||
<DropdownMenu bind:this={dropdown} {anchor} align="left"> |
|||
<h5>Add New Row</h5> |
|||
<Modal |
|||
{table} |
|||
onClosed={dropdown.hide} |
|||
on:newRow={() => dispatch('newRow')} /> |
|||
</DropdownMenu> |
|||
|
|||
<style> |
|||
div { |
|||
display: grid; |
|||
} |
|||
h5 { |
|||
padding: var(--spacing-xl) 0 0 var(--spacing-xl); |
|||
margin: 0; |
|||
font-weight: 500; |
|||
} |
|||
</style> |
|||
@ -1,144 +0,0 @@ |
|||
<script> |
|||
import { getContext, onMount, createEventDispatcher } from "svelte" |
|||
import { Button, Label, DatePicker, RichText } from "@budibase/bbui" |
|||
import Dropzone from "../../attachments/Dropzone.svelte" |
|||
import debounce from "lodash.debounce" |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const { fetchRow, saveRow, routeStore } = getContext("sdk") |
|||
|
|||
const DEFAULTS_FOR_TYPE = { |
|||
string: "", |
|||
boolean: false, |
|||
number: null, |
|||
link: [], |
|||
} |
|||
|
|||
export let table |
|||
export let onClosed |
|||
|
|||
let row = { tableId: table._id } |
|||
let schema = table.schema |
|||
let saved = false |
|||
let rowId |
|||
let isNew = true |
|||
let errors = {} |
|||
|
|||
$: fields = schema ? Object.keys(schema) : [] |
|||
|
|||
$: errorMessages = Object.entries(errors).map( |
|||
([field, message]) => `${field} ${message}` |
|||
) |
|||
|
|||
const save = debounce(async () => { |
|||
for (let field of fields) { |
|||
// Assign defaults to empty fields to prevent validation issues |
|||
if (!(field in row)) { |
|||
row[field] = DEFAULTS_FOR_TYPE[schema[field].type] |
|||
} |
|||
} |
|||
|
|||
const response = await saveRow(row) |
|||
|
|||
if (!response.error) { |
|||
// store.update(state => { |
|||
// state[table._id] = state[table._id] |
|||
// ? [...state[table._id], json] |
|||
// : [json] |
|||
// return state |
|||
// }) |
|||
|
|||
errors = {} |
|||
|
|||
// wipe form, if new row, otherwise update |
|||
// table to get new _rev |
|||
row = isNew ? { tableId: table._id } : response |
|||
|
|||
onClosed() |
|||
dispatch("newRow") |
|||
} else { |
|||
errors = [response.error] |
|||
} |
|||
}) |
|||
|
|||
onMount(async () => { |
|||
const routeParams = $routeStore.routeParams |
|||
rowId = |
|||
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0]) |
|||
isNew = !rowId || rowId === "new" |
|||
|
|||
if (isNew) { |
|||
row = { tableId: table } |
|||
return |
|||
} |
|||
|
|||
row = await fetchRow({ tableId: table._id, rowId }) |
|||
}) |
|||
</script> |
|||
|
|||
<div class="actions"> |
|||
{#each errorMessages as error} |
|||
<p class="error">{error}</p> |
|||
{/each} |
|||
<form on:submit|preventDefault> |
|||
{#each fields as field} |
|||
<div class="form-item"> |
|||
<Label small forAttr={'form-stacked-text'}>{field}</Label> |
|||
{#if schema[field].type === 'string' && schema[field].constraints.inclusion} |
|||
<select bind:value={row[field]}> |
|||
{#each schema[field].constraints.inclusion as opt} |
|||
<option>{opt}</option> |
|||
{/each} |
|||
</select> |
|||
{:else if schema[field].type === 'datetime'} |
|||
<DatePicker bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'boolean'} |
|||
<input class="input" type="checkbox" bind:checked={row[field]} /> |
|||
{:else if schema[field].type === 'number'} |
|||
<input class="input" type="number" bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'string'} |
|||
<input class="input" type="text" bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'longform'} |
|||
<RichText bind:value={row[field]} /> |
|||
{:else if schema[field].type === 'attachment'} |
|||
<Dropzone bind:files={row[field]} /> |
|||
{/if} |
|||
</div> |
|||
<hr /> |
|||
{/each} |
|||
</form> |
|||
</div> |
|||
<footer> |
|||
<div class="button-margin-3"> |
|||
<Button secondary on:click={onClosed}>Cancel</Button> |
|||
</div> |
|||
<div class="button-margin-4"> |
|||
<Button primary on:click={save}>Save</Button> |
|||
</div> |
|||
</footer> |
|||
|
|||
<style> |
|||
.actions { |
|||
padding: var(--spacing-l) var(--spacing-xl); |
|||
} |
|||
|
|||
footer { |
|||
padding: 20px 30px; |
|||
display: grid; |
|||
grid-template-columns: 1fr 1fr 1fr 1fr; |
|||
gap: 20px; |
|||
background: var(--grey-1); |
|||
border-bottom-left-radius: 0.5rem; |
|||
border-bottom-left-radius: 0.5rem; |
|||
} |
|||
|
|||
.button-margin-3 { |
|||
grid-column-start: 3; |
|||
display: grid; |
|||
} |
|||
|
|||
.button-margin-4 { |
|||
grid-column-start: 4; |
|||
display: grid; |
|||
} |
|||
</style> |
|||
@ -1,5 +0,0 @@ |
|||
<script> |
|||
import { DatePicker } from "@budibase/bbui" |
|||
</script> |
|||
|
|||
<DatePicker /> |
|||
@ -1,75 +0,0 @@ |
|||
<script> |
|||
import { onMount } from "svelte" |
|||
|
|||
export let columnName |
|||
export let row |
|||
export let SDK |
|||
|
|||
const { API } = SDK |
|||
|
|||
$: count = |
|||
row && columnName && Array.isArray(row[columnName]) |
|||
? row[columnName].length |
|||
: 0 |
|||
let linkedRows = [] |
|||
let displayColumn |
|||
|
|||
onMount(async () => { |
|||
linkedRows = await API.fetchRelationshipData({ |
|||
tableId: row.tableId, |
|||
rowId: row._id, |
|||
fieldName: columnName, |
|||
}) |
|||
if (linkedRows && linkedRows.length) { |
|||
const table = await API.fetchTableDefinition(linkedRows[0].tableId) |
|||
if (table && table.primaryDisplay) { |
|||
displayColumn = table.primaryDisplay |
|||
} |
|||
} |
|||
}) |
|||
|
|||
async function fetchLinkedRowsData(row, columnName) { |
|||
if (!row || !row._id) { |
|||
return [] |
|||
} |
|||
return await API.fetchRelationshipData({ |
|||
tableId: row.tableId, |
|||
rowId: row._id, |
|||
fieldName: 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> |
|||
@ -1,32 +0,0 @@ |
|||
<script> |
|||
export let columnName |
|||
export let row |
|||
|
|||
$: items = row?.[columnName] || [] |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
{#each items as item} |
|||
<div class="item">{item?.primaryDisplay ?? ''}</div> |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: var(--spacing-xs); |
|||
width: 100%; |
|||
} |
|||
|
|||
.item { |
|||
font-size: var(--font-size-xs); |
|||
padding: var(--spacing-xs) var(--spacing-s); |
|||
border: 1px solid var(--grey-5); |
|||
color: var(--grey-7); |
|||
line-height: normal; |
|||
border-radius: 4px; |
|||
} |
|||
</style> |
|||
@ -1,17 +0,0 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { createEventDispatcher } from "svelte" |
|||
const dispatch = createEventDispatcher() |
|||
|
|||
export let value |
|||
export let options |
|||
|
|||
$: dispatch("change", value) |
|||
</script> |
|||
|
|||
<Select label={false} bind:value> |
|||
<option value="">Choose an option</option> |
|||
{#each options as option} |
|||
<option value={option}>{option}</option> |
|||
{/each} |
|||
</Select> |
|||
@ -1,19 +0,0 @@ |
|||
<script> |
|||
import { Button } from "@budibase/bbui" |
|||
|
|||
export let url |
|||
export let SDK |
|||
|
|||
const { linkable } = SDK |
|||
|
|||
let link |
|||
</script> |
|||
|
|||
<a href={url} bind:this={link} use:linkable /> |
|||
<Button small translucent on:click={() => link.click()}>View</Button> |
|||
|
|||
<style> |
|||
a { |
|||
display: none; |
|||
} |
|||
</style> |
|||
@ -1,169 +0,0 @@ |
|||
// Custom renderers to handle special types
|
|||
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
|
|||
|
|||
import AttachmentCell from "./AttachmentCell/Button.svelte" |
|||
import ViewDetails from "./ViewDetails/Cell.svelte" |
|||
import Select from "./Select/Wrapper.svelte" |
|||
import DatePicker from "./DateTime/Wrapper.svelte" |
|||
import RelationshipLabel from "./Relationship/RelationshipLabel.svelte" |
|||
|
|||
const renderers = new Map([ |
|||
["boolean", booleanRenderer], |
|||
["attachment", attachmentRenderer], |
|||
["options", optionsRenderer], |
|||
["link", linkedRowRenderer], |
|||
["_id", viewDetailsRenderer], |
|||
]) |
|||
|
|||
export function getRenderer(schema, editable, SDK) { |
|||
if (renderers.get(schema.type)) { |
|||
return renderers.get(schema.type)( |
|||
schema.options, |
|||
schema.constraints, |
|||
editable, |
|||
SDK |
|||
) |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
/* eslint-disable no-unused-vars */ |
|||
function booleanRenderer(options, constraints, editable, SDK) { |
|||
return params => { |
|||
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, SDK) { |
|||
return params => { |
|||
const container = document.createElement("div") |
|||
|
|||
const attachmentInstance = new AttachmentCell({ |
|||
target: container, |
|||
props: { |
|||
files: params.value || [], |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
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, SDK) { |
|||
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, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
|
|||
function optionsRenderer(options, constraints, editable, SDK) { |
|||
return params => { |
|||
if (!editable) return params.value |
|||
const container = document.createElement("div") |
|||
container.style.display = "grid" |
|||
container.style.placeItems = "center" |
|||
container.style.height = "100%" |
|||
const change = e => { |
|||
params.setValue(e.detail) |
|||
} |
|||
|
|||
const selectInstance = new Select({ |
|||
target: container, |
|||
props: { |
|||
value: params.value, |
|||
options: constraints.inclusion, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
selectInstance.$on("change", change) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
/* eslint-disable no-unused-vars */ |
|||
function linkedRowRenderer(options, constraints, editable, SDK) { |
|||
return params => { |
|||
let container = document.createElement("div") |
|||
container.style.display = "grid" |
|||
container.style.placeItems = "center" |
|||
container.style.height = "100%" |
|||
|
|||
new RelationshipLabel({ |
|||
target: container, |
|||
props: { |
|||
row: params.data, |
|||
columnName: params.column.colId, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
|
|||
/* eslint-disable no-unused-vars */ |
|||
function viewDetailsRenderer(options, constraints, editable, SDK) { |
|||
return params => { |
|||
let container = document.createElement("div") |
|||
container.style.display = "grid" |
|||
container.style.alignItems = "center" |
|||
container.style.height = "100%" |
|||
|
|||
let url = "/" |
|||
if (options.detailUrl) { |
|||
url = options.detailUrl.replace(":id", params.data._id) |
|||
} |
|||
if (!url.startsWith("/")) { |
|||
url = `/${url}` |
|||
} |
|||
|
|||
new ViewDetails({ |
|||
target: container, |
|||
props: { |
|||
url, |
|||
SDK, |
|||
}, |
|||
}) |
|||
|
|||
return container |
|||
} |
|||
} |
|||
@ -1,6 +0,0 @@ |
|||
// https://www.ag-grid.com/javascript-grid-value-setters/
|
|||
// These handles values and makes sure they adhere to the data type provided by the table
|
|||
export const number = params => { |
|||
params.data[params.colDef.field] = parseFloat(params.newValue) |
|||
return true |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<script> |
|||
export let value |
|||
|
|||
const displayLimit = 5 |
|||
$: attachments = value?.slice(0, displayLimit) ?? [] |
|||
$: leftover = (value?.length ?? 0) - attachments.length |
|||
</script> |
|||
|
|||
{#each attachments as attachment} |
|||
{#if attachment.type.startsWith('image')} |
|||
<img src={attachment.url} alt={attachment.extension} /> |
|||
{:else} |
|||
<div class="file">{attachment.extension}</div> |
|||
{/if} |
|||
{/each} |
|||
{#if leftover} |
|||
<div>+{leftover} more</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
img { |
|||
height: 32px; |
|||
max-width: 64px; |
|||
} |
|||
.file { |
|||
height: 32px; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
padding: 0 8px; |
|||
color: var(--spectrum-global-color-gray-800); |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
border-radius: 2px; |
|||
text-transform: uppercase; |
|||
font-weight: 500; |
|||
font-size: 11px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,38 @@ |
|||
<script> |
|||
import "@spectrum-css/checkbox/dist/index-vars.css" |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<label |
|||
class="spectrum-Checkbox spectrum-Checkbox--sizeM spectrum-Checkbox--emphasized"> |
|||
<input |
|||
type="checkbox" |
|||
class="spectrum-Checkbox-input" |
|||
id="checkbox-1" |
|||
disabled |
|||
checked={!!value} /> |
|||
<span class="spectrum-Checkbox-box"> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Checkbox-checkmark" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Dash100 spectrum-Checkbox-partialCheckmark" |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Dash100" /> |
|||
</svg> |
|||
</span> |
|||
</label> |
|||
|
|||
<style> |
|||
.spectrum-Checkbox { |
|||
min-height: 0; |
|||
} |
|||
.spectrum-Checkbox-box { |
|||
margin: 0; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,27 @@ |
|||
<script> |
|||
import StringRenderer from "./StringRenderer.svelte" |
|||
import BooleanRenderer from "./BooleanRenderer.svelte" |
|||
import DateTimeRenderer from "./DateTimeRenderer.svelte" |
|||
import RelationshipRenderer from "./RelationshipRenderer.svelte" |
|||
import AttachmentRenderer from "./AttachmentRenderer.svelte" |
|||
|
|||
export let schema |
|||
export let value |
|||
|
|||
const plainTypes = ["string", "options", "number", "longform"] |
|||
$: type = schema?.type ?? "string" |
|||
</script> |
|||
|
|||
{#if value != null && value !== ''} |
|||
{#if plainTypes.includes(type)} |
|||
<StringRenderer {value} /> |
|||
{:else if type === 'boolean'} |
|||
<BooleanRenderer {value} /> |
|||
{:else if type === 'datetime'} |
|||
<DateTimeRenderer {value} /> |
|||
{:else if type === 'link'} |
|||
<RelationshipRenderer {value} /> |
|||
{:else if type === 'attachment'} |
|||
<AttachmentRenderer {value} /> |
|||
{/if} |
|||
{/if} |
|||
@ -0,0 +1,13 @@ |
|||
<script> |
|||
import dayjs from "dayjs" |
|||
|
|||
export let value |
|||
</script> |
|||
|
|||
<div>{dayjs(value).format('MMMM D YYYY, HH:mm')}</div> |
|||
|
|||
<style> |
|||
div { |
|||
width: 200px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,20 @@ |
|||
<script> |
|||
import "@spectrum-css/label/dist/index-vars.css" |
|||
|
|||
export let value |
|||
|
|||
const displayLimit = 5 |
|||
$: relationships = value?.slice(0, displayLimit) ?? [] |
|||
$: leftover = (value?.length ?? 0) - relationships.length |
|||
</script> |
|||
|
|||
{#each relationships as relationship} |
|||
{#if relationship?.primaryDisplay} |
|||
<span class="spectrum-Label spectrum-Label--grey"> |
|||
{relationship.primaryDisplay} |
|||
</span> |
|||
{/if} |
|||
{/each} |
|||
{#if leftover} |
|||
<div>+{leftover} more</div> |
|||
{/if} |
|||
@ -0,0 +1,13 @@ |
|||
<script> |
|||
export let value |
|||
</script> |
|||
|
|||
<div>{value}</div> |
|||
|
|||
<style> |
|||
div { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
width: 150px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,290 @@ |
|||
<script> |
|||
import { fade } from "svelte/transition" |
|||
import "@spectrum-css/table/dist/index-vars.css" |
|||
import { getContext } from "svelte" |
|||
import CellRenderer from "./CellRenderer.svelte" |
|||
|
|||
export let theme |
|||
export let size |
|||
export let dataProvider |
|||
export let columns |
|||
export let showAutoColumns |
|||
export let rowCount |
|||
export let quiet |
|||
|
|||
const component = getContext("component") |
|||
const { styleable, Provider } = getContext("sdk") |
|||
|
|||
// Config |
|||
const rowHeight = 55 |
|||
const headerHeight = 36 |
|||
const rowPreload = 5 |
|||
const maxRows = 100 |
|||
|
|||
// Sorting state |
|||
let sortColumn |
|||
let sortOrder |
|||
|
|||
// Table state |
|||
$: loaded = dataProvider?.loaded ?? false |
|||
$: rows = dataProvider?.rows ?? [] |
|||
$: visibleRowCount = loaded |
|||
? Math.min(rows.length, rowCount || maxRows, maxRows) |
|||
: Math.min(8, rowCount || maxRows) |
|||
$: scroll = rows.length > visibleRowCount |
|||
$: contentStyle = getContentStyle(visibleRowCount, scroll || !loaded) |
|||
$: sortedRows = sortRows(rows, sortColumn, sortOrder) |
|||
$: schema = dataProvider?.schema ?? {} |
|||
$: fields = getFields(schema, columns, showAutoColumns) |
|||
|
|||
// Scrolling state |
|||
let timeout |
|||
let nextScrollTop = 0 |
|||
let scrollTop = 0 |
|||
$: firstVisibleRow = calculateFirstVisibleRow(scrollTop) |
|||
$: lastVisibleRow = calculateLastVisibleRow( |
|||
firstVisibleRow, |
|||
visibleRowCount, |
|||
rows.length |
|||
) |
|||
|
|||
const getContentStyle = (visibleRows, useFixedHeight) => { |
|||
if (!useFixedHeight) { |
|||
return "" |
|||
} |
|||
return `height: ${headerHeight - 1 + visibleRows * (rowHeight + 1)}px;` |
|||
} |
|||
|
|||
const sortRows = (rows, sortColumn, sortOrder) => { |
|||
if (!sortColumn || !sortOrder) { |
|||
return rows |
|||
} |
|||
return rows.slice().sort((a, b) => { |
|||
const colA = a[sortColumn] |
|||
const colB = b[sortColumn] |
|||
if (sortOrder === "Descending") { |
|||
return colA > colB ? -1 : 1 |
|||
} else { |
|||
return colA > colB ? 1 : -1 |
|||
} |
|||
}) |
|||
} |
|||
|
|||
const sortBy = field => { |
|||
if (field === sortColumn) { |
|||
sortOrder = sortOrder === "Descending" ? "Ascending" : "Descending" |
|||
} else { |
|||
sortColumn = field |
|||
sortOrder = "Descending" |
|||
} |
|||
} |
|||
|
|||
const getFields = (schema, customColumns, showAutoColumns) => { |
|||
// Check for an invalid column selection |
|||
let invalid = false |
|||
customColumns?.forEach(column => { |
|||
if (schema[column] == null) { |
|||
invalid = true |
|||
} |
|||
}) |
|||
|
|||
// Use column selection if it exists |
|||
if (!invalid && customColumns?.length) { |
|||
return customColumns |
|||
} |
|||
|
|||
// Otherwise generate columns |
|||
let columns = [] |
|||
let autoColumns = [] |
|||
Object.entries(schema).forEach(([field, fieldSchema]) => { |
|||
if (!fieldSchema?.autocolumn) { |
|||
columns.push(field) |
|||
} else if (showAutoColumns) { |
|||
autoColumns.push(field) |
|||
} |
|||
}) |
|||
return columns.concat(autoColumns) |
|||
} |
|||
|
|||
const onScroll = event => { |
|||
nextScrollTop = event.target.scrollTop |
|||
if (timeout) { |
|||
return |
|||
} |
|||
timeout = setTimeout(() => { |
|||
scrollTop = nextScrollTop |
|||
timeout = null |
|||
}, 50) |
|||
} |
|||
|
|||
const calculateFirstVisibleRow = scrollTop => { |
|||
return Math.max(Math.floor(scrollTop / (rowHeight + 1)) - rowPreload, 0) |
|||
} |
|||
|
|||
const calculateLastVisibleRow = (firstRow, visibleRowCount, allRowCount) => { |
|||
return Math.min(firstRow + visibleRowCount + 2 * rowPreload, allRowCount) |
|||
} |
|||
</script> |
|||
|
|||
{#if !loaded} |
|||
<div class="content" style={contentStyle} /> |
|||
{:else} |
|||
<div use:styleable={$component.styles}> |
|||
<div |
|||
on:scroll={onScroll} |
|||
lang="en" |
|||
dir="ltr" |
|||
class:quiet |
|||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`} |
|||
class={`spectrum ${size || 'spectrum--medium'} ${theme || 'spectrum--light'}`}> |
|||
<div class="content" style={contentStyle}> |
|||
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}> |
|||
<thead class="spectrum-Table-head"> |
|||
<tr> |
|||
{#if $component.children} |
|||
<th class="spectrum-Table-headCell"> |
|||
<div class="spectrum-Table-headCell-content" /> |
|||
</th> |
|||
{/if} |
|||
{#each fields as field} |
|||
<th |
|||
class="spectrum-Table-headCell is-sortable" |
|||
class:is-sorted-desc={sortColumn === field && sortOrder === 'Descending'} |
|||
class:is-sorted-asc={sortColumn === field && sortOrder === 'Ascending'} |
|||
on:click={() => sortBy(field)}> |
|||
<div class="spectrum-Table-headCell-content"> |
|||
<div class="title">{schema[field]?.name}</div> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ArrowDown100 spectrum-Table-sortedIcon" |
|||
class:visible={sortColumn === field} |
|||
focusable="false" |
|||
aria-hidden="true"> |
|||
<use xlink:href="#spectrum-css-icon-Arrow100" /> |
|||
</svg> |
|||
</div> |
|||
</th> |
|||
{/each} |
|||
</tr> |
|||
</thead> |
|||
<tbody class="spectrum-Table-body"> |
|||
{#each sortedRows as row, idx} |
|||
<tr |
|||
class="spectrum-Table-row" |
|||
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}> |
|||
{#if idx >= firstVisibleRow && idx <= lastVisibleRow} |
|||
{#if $component.children} |
|||
<td |
|||
class="spectrum-Table-cell spectrum-Table-cell--divider"> |
|||
<div class="spectrum-Table-cell-content"> |
|||
<Provider data={row}> |
|||
<slot /> |
|||
</Provider> |
|||
</div> |
|||
</td> |
|||
{/if} |
|||
{#each fields as field} |
|||
<td class="spectrum-Table-cell"> |
|||
<div class="spectrum-Table-cell-content"> |
|||
<CellRenderer |
|||
schema={schema[field]} |
|||
value={row[field]} /> |
|||
</div> |
|||
</td> |
|||
{/each} |
|||
{/if} |
|||
</tr> |
|||
{/each} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
.spectrum { |
|||
position: relative; |
|||
overflow: auto; |
|||
border: 1px solid |
|||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; |
|||
} |
|||
.spectrum.quiet { |
|||
border: none !important; |
|||
} |
|||
table { |
|||
width: 100%; |
|||
} |
|||
|
|||
.spectrum-Table-sortedIcon { |
|||
opacity: 0; |
|||
display: block !important; |
|||
} |
|||
.spectrum-Table-sortedIcon.visible { |
|||
opacity: 1; |
|||
} |
|||
.spectrum, |
|||
th { |
|||
border-bottom: 1px solid |
|||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; |
|||
} |
|||
th { |
|||
vertical-align: middle; |
|||
height: var(--header-height); |
|||
position: sticky; |
|||
top: 0; |
|||
background-color: var(--spectrum-global-color-gray-100); |
|||
z-index: 2; |
|||
} |
|||
.spectrum-Table-headCell-content { |
|||
white-space: nowrap; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
user-select: none; |
|||
} |
|||
.spectrum-Table-headCell-content .title { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
tbody { |
|||
z-index: 1; |
|||
} |
|||
tbody tr { |
|||
height: var(--row-height); |
|||
} |
|||
tbody tr.hidden { |
|||
height: calc(var(--row-height) + 1px); |
|||
} |
|||
tbody tr.offset { |
|||
background-color: red; |
|||
display: block; |
|||
} |
|||
td { |
|||
padding-top: 0; |
|||
padding-bottom: 0; |
|||
border-bottom: none !important; |
|||
border-left: none !important; |
|||
border-right: none !important; |
|||
border-top: 1px solid |
|||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; |
|||
} |
|||
tr:first-child td { |
|||
border-top: none !important; |
|||
} |
|||
.spectrum:not(.quiet) td.spectrum-Table-cell--divider { |
|||
width: 1px; |
|||
border-right: 1px solid |
|||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important; |
|||
} |
|||
.spectrum-Table-cell-content { |
|||
height: var(--row-height); |
|||
white-space: nowrap; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
gap: 4px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1 @@ |
|||
export { default as table } from "./Table.svelte" |
|||
Loading…
Reference in new issue