Add WIP spreadsheet
This commit is contained in:
parent
ade595e4a0
commit
ae15690741
|
@ -28,6 +28,7 @@
|
||||||
"dataprovider",
|
"dataprovider",
|
||||||
"repeater",
|
"repeater",
|
||||||
"table",
|
"table",
|
||||||
|
"spreadsheet",
|
||||||
"dynamicfilter",
|
"dynamicfilter",
|
||||||
"daterangepicker"
|
"daterangepicker"
|
||||||
]
|
]
|
||||||
|
|
|
@ -5279,5 +5279,36 @@
|
||||||
"type": "schema",
|
"type": "schema",
|
||||||
"suffix": "repeater"
|
"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 "./table"
|
||||||
export * from "./blocks"
|
export * from "./blocks"
|
||||||
export * from "./dynamic-filter"
|
export * from "./dynamic-filter"
|
||||||
|
export * from "./spreadsheet"
|
||||||
|
|
||||||
// Deprecated component left for compatibility in old apps
|
// Deprecated component left for compatibility in old apps
|
||||||
export { default as navigation } from "./deprecated/Navigation.svelte"
|
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