1
0
Fork 0
mirror of synced 2024-07-11 01:06:04 +12:00

Add WIP spreadsheet

This commit is contained in:
Andrew Kingston 2022-11-24 14:12:40 +00:00
parent ade595e4a0
commit ae15690741
8 changed files with 378 additions and 0 deletions

View file

@ -28,6 +28,7 @@
"dataprovider",
"repeater",
"table",
"spreadsheet",
"dynamicfilter",
"daterangepicker"
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
export { default as spreadsheet } from "./Spreadsheet.svelte"