Add WIP spreadsheet
This commit is contained in:
parent
ade595e4a0
commit
ae15690741
|
@ -28,6 +28,7 @@
|
|||
"dataprovider",
|
||||
"repeater",
|
||||
"table",
|
||||
"spreadsheet",
|
||||
"dynamicfilter",
|
||||
"daterangepicker"
|
||||
]
|
||||
|
|
|
@ -5279,5 +5279,36 @@
|
|||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
},
|
||||
"spreadsheet": {
|
||||
"name": "Spreadsheet",
|
||||
"icon": "ViewGrid",
|
||||
"settings": [
|
||||
{
|
||||
"key": "table",
|
||||
"type": "table",
|
||||
"label": "Table"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "filter"
|
||||
},
|
||||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort Column",
|
||||
"key": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort Order",
|
||||
"key": "sortOrder",
|
||||
"options": [
|
||||
"Ascending",
|
||||
"Descending"
|
||||
],
|
||||
"defaultValue": "Ascending"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -41,6 +41,7 @@ export * from "./forms"
|
|||
export * from "./table"
|
||||
export * from "./blocks"
|
||||
export * from "./dynamic-filter"
|
||||
export * from "./spreadsheet"
|
||||
|
||||
// Deprecated component left for compatibility in old apps
|
||||
export { default as navigation } from "./deprecated/Navigation.svelte"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import dayjs from "dayjs"
|
||||
|
||||
export let value
|
||||
|
||||
$: parsedValue = !value ? "" : dayjs(value).format("D/M/YYYY")
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{parsedValue}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
export let value
|
||||
export let schema
|
||||
|
||||
const colors = [
|
||||
"rgb(207, 223, 255)",
|
||||
"rgb(208, 240, 253)",
|
||||
"rgb(194, 245, 233)",
|
||||
"rgb(209, 247, 196)",
|
||||
"rgb(255, 234, 182)",
|
||||
"rgb(254, 226, 213)",
|
||||
"rgb(255, 220, 229)",
|
||||
"rgb(255, 218, 246)",
|
||||
"rgb(237, 226, 254)",
|
||||
]
|
||||
|
||||
$: idx = schema?.constraints?.inclusion?.indexOf(value)
|
||||
$: color = value && idx === -1 ? null : colors[idx % colors.length]
|
||||
</script>
|
||||
|
||||
<div style="--color: {color}" class:valid={!!color}>
|
||||
{value || ""}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
div.valid {
|
||||
margin: 0 8px;
|
||||
padding: 2px 8px;
|
||||
background: var(--color);
|
||||
border-radius: 8px;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,269 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { fetchData, LuceneUtils } from "@budibase/frontend-core"
|
||||
import { Icon, ActionButton } from "@budibase/bbui"
|
||||
import TextCell from "./TextCell.svelte"
|
||||
import OptionsCell from "./OptionsCell.svelte"
|
||||
import DateCell from "./DateCell.svelte"
|
||||
|
||||
export let table
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
|
||||
const { styleable, API } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
// Config
|
||||
const limit = 100
|
||||
const defaultWidth = 200
|
||||
let widths
|
||||
let hoveredRow
|
||||
let selectedCell
|
||||
let horizontallyScrolled = false
|
||||
|
||||
$: query = LuceneUtils.buildLuceneQuery(filter)
|
||||
$: fetch = createFetch(table)
|
||||
$: fetch.update({
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
query,
|
||||
limit,
|
||||
})
|
||||
$: fields = Object.keys($fetch.schema || {})
|
||||
$: initWidths(fields)
|
||||
$: gridStyles = getGridStyles(widths)
|
||||
$: schema = $fetch.schema
|
||||
|
||||
const createFetch = datasource => {
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
options: {
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
query,
|
||||
limit,
|
||||
paginate: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const initWidths = fields => {
|
||||
widths = fields.map(() => defaultWidth)
|
||||
}
|
||||
|
||||
const getGridStyles = widths => {
|
||||
if (!widths?.length) {
|
||||
return "--grid: 1fr;"
|
||||
}
|
||||
return `--grid: 60px ${widths.map(x => `${x}px`).join(" ")};`
|
||||
}
|
||||
|
||||
const handleScroll = e => {
|
||||
const nextHorizontallyScrolled = e.target.scrollLeft > 0
|
||||
if (nextHorizontallyScrolled !== horizontallyScrolled) {
|
||||
horizontallyScrolled = nextHorizontallyScrolled
|
||||
}
|
||||
}
|
||||
|
||||
const getCellForField = field => {
|
||||
const type = schema?.[field]?.type
|
||||
if (type === "options") {
|
||||
return OptionsCell
|
||||
} else if (type === "datetime") {
|
||||
return DateCell
|
||||
}
|
||||
return TextCell
|
||||
}
|
||||
|
||||
const getIconForField = field => {
|
||||
const type = schema?.[field]?.type
|
||||
if (type === "options") {
|
||||
return "ChevronDown"
|
||||
} else if (type === "datetime") {
|
||||
return "Date"
|
||||
}
|
||||
return "Text"
|
||||
}
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
<div class="wrapper">
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<ActionButton icon="Filter" size="S">Filter</ActionButton>
|
||||
<ActionButton icon="Group" size="S">Group</ActionButton>
|
||||
<ActionButton icon="SortOrderDown" size="S">Sort</ActionButton>
|
||||
<ActionButton icon="VisibilityOff" size="S">Hide fields</ActionButton>
|
||||
</div>
|
||||
<div class="title">Sales Records</div>
|
||||
<div class="search">
|
||||
<Icon
|
||||
name="Search"
|
||||
size="S"
|
||||
color="var(--spectrum-global-color-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spreadsheet" on:scroll={handleScroll} style={gridStyles}>
|
||||
<div class="header cell label">
|
||||
<input type="checkbox" />
|
||||
</div>
|
||||
{#each fields as field, fieldIdx}
|
||||
<div
|
||||
class="header cell"
|
||||
class:sticky={fieldIdx === 0}
|
||||
class:shadow={horizontallyScrolled}
|
||||
>
|
||||
<Icon
|
||||
size="S"
|
||||
name={getIconForField(field)}
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
{field}
|
||||
</div>
|
||||
{/each}
|
||||
{#each $fetch.rows as row, rowIdx}
|
||||
<div class="cell label" class:hovered={hoveredRow === rowIdx}>
|
||||
{rowIdx + 1}
|
||||
</div>
|
||||
{#each fields as field, fieldIdx}
|
||||
{@const cellIdx = rowIdx * fields.length + fieldIdx}
|
||||
<div
|
||||
class="cell"
|
||||
class:sticky={fieldIdx === 0}
|
||||
class:hovered={hoveredRow === rowIdx}
|
||||
class:selected={selectedCell === cellIdx}
|
||||
class:shadow={horizontallyScrolled}
|
||||
on:focus
|
||||
on:mouseover={() => (hoveredRow = rowIdx)}
|
||||
on:click={() => (selectedCell = cellIdx)}
|
||||
>
|
||||
<svelte:component
|
||||
this={getCellForField(field)}
|
||||
value={row[field]}
|
||||
schema={schema[field]}
|
||||
selected={selectedCell === cellIdx}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
border: 1px solid var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
.spreadsheet {
|
||||
display: grid;
|
||||
grid-template-columns: var(--grid);
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
overflow: auto;
|
||||
height: 800px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
.title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell {
|
||||
height: 32px;
|
||||
border-bottom: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-right: 1px solid var(--spectrum-global-color-gray-300);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
background: var(--spectrum-global-color-gray-50);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
}
|
||||
.cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.cell.hovered {
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.cell.selected {
|
||||
box-shadow: inset 0 0 0 2px var(--primaryColorHover);
|
||||
z-index: 1;
|
||||
}
|
||||
.cell:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.cell.sticky {
|
||||
position: sticky;
|
||||
left: 60px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0 8px;
|
||||
z-index: 3;
|
||||
border-color: var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
.header.sticky {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.sticky.shadow:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
left: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 16px;
|
||||
border-right: none;
|
||||
color: var(--spectrum-global-color-gray-500);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
.label.header {
|
||||
z-index: 4;
|
||||
}
|
||||
.label.header input {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,16 @@
|
|||
<script>
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div class="text-cell">
|
||||
{value || ""}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.text-cell {
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1 @@
|
|||
export { default as spreadsheet } from "./Spreadsheet.svelte"
|
Loading…
Reference in New Issue