This reverts commit 55ce83c444
.
This commit is contained in:
parent
55ce83c444
commit
f8184c947c
|
@ -84,7 +84,7 @@
|
||||||
"@spectrum-css/vars": "3.0.1",
|
"@spectrum-css/vars": "3.0.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"easymde": "^2.16.1",
|
"easymde": "^2.16.1",
|
||||||
"svelte-flatpickr": "^3.3.2",
|
"svelte-flatpickr": "^3.2.3",
|
||||||
"svelte-portal": "^1.0.0"
|
"svelte-portal": "^1.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-Icon--sizeS"
|
class="spectrum-Icon spectrum-Icon--size{size}"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
aria-label={icon}
|
aria-label={icon}
|
||||||
|
|
|
@ -6,9 +6,6 @@ let clickHandlers = []
|
||||||
*/
|
*/
|
||||||
const handleClick = event => {
|
const handleClick = event => {
|
||||||
// Ignore click if this is an ignored class
|
// Ignore click if this is an ignored class
|
||||||
if (event.target.closest('[data-ignore-click-outside="true"]')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for (let className of ignoredClasses) {
|
for (let className of ignoredClasses) {
|
||||||
if (event.target.closest(className)) {
|
if (event.target.closest(className)) {
|
||||||
return
|
return
|
||||||
|
@ -32,7 +29,6 @@ const handleClick = event => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
document.documentElement.addEventListener("click", handleClick, true)
|
document.documentElement.addEventListener("click", handleClick, true)
|
||||||
document.documentElement.addEventListener("contextmenu", handleClick, true)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or updates a click handler
|
* Adds or updates a click handler
|
||||||
|
|
|
@ -138,7 +138,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container" class:compact>
|
<div class="container">
|
||||||
{#if selectedImage}
|
{#if selectedImage}
|
||||||
{#if gallery}
|
{#if gallery}
|
||||||
<div class="gallery">
|
<div class="gallery">
|
||||||
|
@ -355,9 +355,6 @@
|
||||||
input[type="file"] {
|
input[type="file"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.compact .spectrum-Dropzone {
|
|
||||||
padding: 6px 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gallery {
|
.gallery {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -382,17 +379,6 @@
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
margin: 20px 30px;
|
margin: 20px 30px;
|
||||||
}
|
}
|
||||||
.compact .placeholder,
|
|
||||||
.compact img {
|
|
||||||
margin: 10px 16px;
|
|
||||||
}
|
|
||||||
.compact img {
|
|
||||||
height: 90px;
|
|
||||||
}
|
|
||||||
.compact .gallery {
|
|
||||||
padding: 6px 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -461,13 +447,6 @@
|
||||||
.disabled .spectrum-Heading--sizeL {
|
.disabled .spectrum-Heading--sizeL {
|
||||||
color: var(--spectrum-alias-text-color-disabled);
|
color: var(--spectrum-alias-text-color-disabled);
|
||||||
}
|
}
|
||||||
.compact .spectrum-Dropzone {
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
.compact .spectrum-IllustratedMessage-description {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
|
|
@ -20,13 +20,12 @@
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: arrayValue = Array.isArray(value) ? value : [value].filter(x => !!x)
|
$: selectedLookupMap = getSelectedLookupMap(value)
|
||||||
$: selectedLookupMap = getSelectedLookupMap(arrayValue)
|
|
||||||
$: optionLookupMap = getOptionLookupMap(options)
|
$: optionLookupMap = getOptionLookupMap(options)
|
||||||
|
|
||||||
$: fieldText = getFieldText(arrayValue, optionLookupMap, placeholder)
|
$: fieldText = getFieldText(value, optionLookupMap, placeholder)
|
||||||
$: isOptionSelected = optionValue => selectedLookupMap[optionValue] === true
|
$: isOptionSelected = optionValue => selectedLookupMap[optionValue] === true
|
||||||
$: toggleOption = makeToggleOption(selectedLookupMap, arrayValue)
|
$: toggleOption = makeToggleOption(selectedLookupMap, value)
|
||||||
|
|
||||||
const getFieldText = (value, map, placeholder) => {
|
const getFieldText = (value, map, placeholder) => {
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
|
@ -85,7 +84,7 @@
|
||||||
{readonly}
|
{readonly}
|
||||||
{fieldText}
|
{fieldText}
|
||||||
{options}
|
{options}
|
||||||
isPlaceholder={!arrayValue.length}
|
isPlaceholder={!value?.length}
|
||||||
{autocomplete}
|
{autocomplete}
|
||||||
bind:fetchTerm
|
bind:fetchTerm
|
||||||
{useFetch}
|
{useFetch}
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
export let fileTags = []
|
export let fileTags = []
|
||||||
export let maximum = undefined
|
export let maximum = undefined
|
||||||
export let compact = false
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -38,7 +37,6 @@
|
||||||
{gallery}
|
{gallery}
|
||||||
{fileTags}
|
{fileTags}
|
||||||
{maximum}
|
{maximum}
|
||||||
{compact}
|
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
export let dismissible = true
|
export let dismissible = true
|
||||||
export let offset = 5
|
export let offset = 5
|
||||||
export let customHeight
|
export let customHeight
|
||||||
export let animate = true
|
|
||||||
|
|
||||||
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
$: target = portalTarget || getContext(Context.PopoverRoot) || ".spectrum"
|
||||||
|
|
||||||
|
@ -79,7 +78,7 @@
|
||||||
class="spectrum-Popover is-open"
|
class="spectrum-Popover is-open"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
style="height: {customHeight}"
|
style="height: {customHeight}"
|
||||||
transition:fly|local={{ y: -20, duration: animate ? 200 : 0 }}
|
transition:fly|local={{ y: -20, duration: 200 }}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,9 +5,8 @@
|
||||||
|
|
||||||
const displayLimit = 5
|
const displayLimit = 5
|
||||||
|
|
||||||
$: arrayValue = Array.isArray(value) ? value : [value].filter(x => !!x)
|
$: badges = Array.isArray(value) ? value.slice(0, displayLimit) : []
|
||||||
$: badges = arrayValue.slice(0, displayLimit)
|
$: leftover = (value?.length ?? 0) - badges.length
|
||||||
$: leftover = arrayValue.length - badges.length
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each badges as badge}
|
{#each badges as badge}
|
||||||
|
|
|
@ -143,7 +143,7 @@
|
||||||
}
|
}
|
||||||
fields?.forEach(field => {
|
fields?.forEach(field => {
|
||||||
const fieldSchema = schema[field]
|
const fieldSchema = schema[field]
|
||||||
if (fieldSchema.width && typeof fieldSchema.width === "string") {
|
if (fieldSchema.width) {
|
||||||
style += ` ${fieldSchema.width}`
|
style += ` ${fieldSchema.width}`
|
||||||
} else {
|
} else {
|
||||||
style += " minmax(auto, 1fr)"
|
style += " minmax(auto, 1fr)"
|
||||||
|
|
|
@ -97,22 +97,4 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom theme additions */
|
|
||||||
.spectrum--darkest {
|
|
||||||
--drop-shadow: rgba(0, 0, 0, 0.6);
|
|
||||||
--spectrum-global-color-blue-100: rgb(28, 33, 43);
|
|
||||||
}
|
|
||||||
.spectrum--dark {
|
|
||||||
--drop-shadow: rgba(0, 0, 0, 0.3);
|
|
||||||
--spectrum-global-color-blue-100: rgb(42, 47, 57);
|
|
||||||
}
|
|
||||||
.spectrum--light {
|
|
||||||
--drop-shadow: rgba(0, 0, 0, 0.075);
|
|
||||||
--spectrum-global-color-blue-100: rgb(240, 245, 255);
|
|
||||||
}
|
|
||||||
.spectrum--lightest {
|
|
||||||
--drop-shadow: rgba(0, 0, 0, 0.05);
|
|
||||||
--spectrum-global-color-blue-100: rgb(240, 244, 255);
|
|
||||||
}
|
|
|
@ -1,74 +1,286 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { fade } from "svelte/transition"
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
|
import CreateRowButton from "./buttons/CreateRowButton.svelte"
|
||||||
|
import CreateColumnButton from "./buttons/CreateColumnButton.svelte"
|
||||||
|
import CreateViewButton from "./buttons/CreateViewButton.svelte"
|
||||||
|
import ExistingRelationshipButton from "./buttons/ExistingRelationshipButton.svelte"
|
||||||
|
import ExportButton from "./buttons/ExportButton.svelte"
|
||||||
|
import ImportButton from "./buttons/ImportButton.svelte"
|
||||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||||
|
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||||
|
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||||
|
import TableFilterButton from "./buttons/TableFilterButton.svelte"
|
||||||
|
import Table from "./Table.svelte"
|
||||||
import { TableNames } from "constants"
|
import { TableNames } from "constants"
|
||||||
import { Grid } from "@budibase/frontend-core"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { fetchData } from "@budibase/frontend-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GridAddColumnModal from "components/backend/DataTable/modals/grid/GridCreateColumnModal.svelte"
|
|
||||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
|
||||||
import GridEditUserModal from "components/backend/DataTable/modals/grid/GridEditUserModal.svelte"
|
|
||||||
import GridCreateViewButton from "components/backend/DataTable/buttons/grid/GridCreateViewButton.svelte"
|
|
||||||
import GridImportButton from "components/backend/DataTable/buttons/grid/GridImportButton.svelte"
|
|
||||||
import GridExportButton from "components/backend/DataTable/buttons/grid/GridExportButton.svelte"
|
|
||||||
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
|
|
||||||
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
|
|
||||||
import GridRelationshipButton from "components/backend/DataTable/buttons/grid/GridRelationshipButton.svelte"
|
|
||||||
import GridEditColumnModal from "components/backend/DataTable/modals/grid/GridEditColumnModal.svelte"
|
|
||||||
|
|
||||||
const userSchemaOverrides = {
|
let hideAutocolumns = true
|
||||||
firstName: { name: "First name", disabled: true },
|
let filters
|
||||||
lastName: { name: "Last name", disabled: true },
|
|
||||||
email: { name: "Email", disabled: true },
|
$: isUsersTable = $tables.selected?._id === TableNames.USERS
|
||||||
roleId: { name: "Role", disabled: true },
|
$: type = $tables.selected?.type
|
||||||
status: { name: "Status", disabled: true },
|
$: isInternal = type !== "external"
|
||||||
|
$: schema = $tables.selected?.schema
|
||||||
|
$: enrichedSchema = enrichSchema($tables.selected?.schema)
|
||||||
|
$: id = $tables.selected?._id
|
||||||
|
$: fetch = createFetch(id)
|
||||||
|
$: hasCols = checkHasCols(schema)
|
||||||
|
$: hasRows = !!$fetch.rows?.length
|
||||||
|
$: showError($fetch.error)
|
||||||
|
$: id, (filters = null)
|
||||||
|
|
||||||
|
let appliedFilter
|
||||||
|
let rawFilter
|
||||||
|
let appliedSort
|
||||||
|
let selectedRows = []
|
||||||
|
|
||||||
|
$: enrichedSchema,
|
||||||
|
() => {
|
||||||
|
appliedFilter = null
|
||||||
|
rawFilter = null
|
||||||
|
appliedSort = null
|
||||||
|
selectedRows = []
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (Number.isInteger($fetch.pageNumber)) {
|
||||||
|
selectedRows = []
|
||||||
}
|
}
|
||||||
|
|
||||||
$: id = $tables.selected?._id
|
const showError = error => {
|
||||||
$: isUsersTable = id === TableNames.USERS
|
if (error) {
|
||||||
$: isInternal = $tables.selected?.type !== "external"
|
notifications.error(error?.message || "Unable to fetch data.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichSchema = schema => {
|
||||||
|
let tempSchema = { ...schema }
|
||||||
|
tempSchema._id = {
|
||||||
|
type: "internal",
|
||||||
|
editable: false,
|
||||||
|
displayName: "ID",
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
if (isInternal) {
|
||||||
|
tempSchema._rev = {
|
||||||
|
type: "internal",
|
||||||
|
editable: false,
|
||||||
|
displayName: "Revision",
|
||||||
|
autocolumn: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkHasCols = schema => {
|
||||||
|
if (!schema || Object.keys(schema).length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let fields = Object.values(schema)
|
||||||
|
for (let field of fields) {
|
||||||
|
if (!field.autocolumn) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches new data whenever the table changes
|
||||||
|
const createFetch = tableId => {
|
||||||
|
return fetchData({
|
||||||
|
API,
|
||||||
|
datasource: {
|
||||||
|
tableId,
|
||||||
|
type: "table",
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
schema,
|
||||||
|
limit: 10,
|
||||||
|
paginate: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever sorting option changes
|
||||||
|
const onSort = async e => {
|
||||||
|
const sort = {
|
||||||
|
sortColumn: e.detail.column,
|
||||||
|
sortOrder: e.detail.order,
|
||||||
|
}
|
||||||
|
await fetch.update(sort)
|
||||||
|
appliedSort = { ...sort }
|
||||||
|
appliedSort.sortOrder = appliedSort.sortOrder.toLowerCase()
|
||||||
|
selectedRows = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever filters change
|
||||||
|
const onFilter = e => {
|
||||||
|
filters = e.detail
|
||||||
|
fetch.update({
|
||||||
|
filter: filters,
|
||||||
|
})
|
||||||
|
appliedFilter = e.detail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever schema changes
|
||||||
|
const onUpdateColumns = () => {
|
||||||
|
selectedRows = []
|
||||||
|
fetch.refresh()
|
||||||
|
tables.fetchTable(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data whenever rows are modified. Unfortunately we have to lose
|
||||||
|
// our pagination place, as our bookmarks will have shifted.
|
||||||
|
const onUpdateRows = () => {
|
||||||
|
selectedRows = []
|
||||||
|
fetch.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// When importing new rows it is better to reinitialise request/paging data.
|
||||||
|
// Not doing so causes inconsistency in paging behaviour and content.
|
||||||
|
const onImportData = () => {
|
||||||
|
fetch.getInitialData()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="wrapper">
|
<div>
|
||||||
<Grid
|
<Table
|
||||||
{API}
|
title={$tables.selected?.name}
|
||||||
|
schema={enrichedSchema}
|
||||||
|
{type}
|
||||||
tableId={id}
|
tableId={id}
|
||||||
allowAddRows={!isUsersTable}
|
data={$fetch.rows}
|
||||||
allowDeleteRows={!isUsersTable}
|
bind:hideAutocolumns
|
||||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
loading={!$fetch.loaded}
|
||||||
on:updatetable={e => tables.updateTable(e.detail)}
|
on:sort={onSort}
|
||||||
|
allowEditing
|
||||||
|
disableSorting
|
||||||
|
on:updatecolumns={onUpdateColumns}
|
||||||
|
on:updaterows={onUpdateRows}
|
||||||
|
on:selectionUpdated={e => {
|
||||||
|
selectedRows = e.detail
|
||||||
|
}}
|
||||||
|
customPlaceholder
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="controls">
|
<div class="buttons">
|
||||||
{#if isInternal}
|
<div class="left-buttons">
|
||||||
<GridCreateViewButton />
|
<CreateColumnButton
|
||||||
{/if}
|
highlighted={$fetch.loaded && (!hasCols || !hasRows)}
|
||||||
<GridManageAccessButton />
|
on:updatecolumns={onUpdateColumns}
|
||||||
{#if isUsersTable}
|
/>
|
||||||
<EditRolesButton />
|
{#if !isUsersTable}
|
||||||
{/if}
|
<CreateRowButton
|
||||||
{#if !isInternal}
|
on:updaterows={onUpdateRows}
|
||||||
<GridRelationshipButton />
|
title={"Create row"}
|
||||||
{/if}
|
modalContentComponent={CreateEditRow}
|
||||||
<GridImportButton disabled={isUsersTable} />
|
disabled={!hasCols}
|
||||||
<GridExportButton />
|
highlighted={$fetch.loaded && hasCols && !hasRows}
|
||||||
<GridFilterButton />
|
/>
|
||||||
<GridAddColumnModal />
|
{/if}
|
||||||
<GridEditColumnModal />
|
{#if isInternal}
|
||||||
{#if isUsersTable}
|
<CreateViewButton disabled={!hasCols || !hasRows} />
|
||||||
<GridEditUserModal />
|
{/if}
|
||||||
{:else}
|
</div>
|
||||||
<GridCreateEditRowModal />
|
<div class="right-buttons">
|
||||||
{/if}
|
<ManageAccessButton resourceId={$tables.selected?._id} />
|
||||||
</svelte:fragment>
|
{#if isUsersTable}
|
||||||
</Grid>
|
<EditRolesButton />
|
||||||
|
{/if}
|
||||||
|
{#if !isInternal}
|
||||||
|
<ExistingRelationshipButton
|
||||||
|
table={$tables.selected}
|
||||||
|
on:updatecolumns={onUpdateColumns}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<HideAutocolumnButton bind:hideAutocolumns />
|
||||||
|
<ImportButton
|
||||||
|
disabled={$tables.selected?._id === "ta_users"}
|
||||||
|
tableId={$tables.selected?._id}
|
||||||
|
on:importrows={onImportData}
|
||||||
|
/>
|
||||||
|
<ExportButton
|
||||||
|
disabled={!hasRows || !hasCols}
|
||||||
|
view={$tables.selected?._id}
|
||||||
|
filters={appliedFilter}
|
||||||
|
sorting={appliedSort}
|
||||||
|
{selectedRows}
|
||||||
|
/>
|
||||||
|
{#key id}
|
||||||
|
<TableFilterButton
|
||||||
|
{schema}
|
||||||
|
{filters}
|
||||||
|
on:change={onFilter}
|
||||||
|
disabled={!hasCols}
|
||||||
|
tableId={id}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div slot="placeholder">
|
||||||
|
<Layout gap="S">
|
||||||
|
{#if !hasCols}
|
||||||
|
<Heading>Let's create some columns</Heading>
|
||||||
|
<Body>
|
||||||
|
Start building out your table structure<br />
|
||||||
|
by adding some columns
|
||||||
|
</Body>
|
||||||
|
{:else}
|
||||||
|
<Heading>Now let's add a row</Heading>
|
||||||
|
<Body>
|
||||||
|
Add some data to your table<br />
|
||||||
|
by adding some rows
|
||||||
|
</Body>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Table>
|
||||||
|
{#key id}
|
||||||
|
<div in:fade={{ delay: 200, duration: 100 }}>
|
||||||
|
<div class="pagination">
|
||||||
|
<Pagination
|
||||||
|
page={$fetch.pageNumber + 1}
|
||||||
|
hasPrevPage={$fetch.hasPrevPage}
|
||||||
|
hasNextPage={$fetch.hasNextPage}
|
||||||
|
goToPrevPage={$fetch.loading ? null : fetch.prevPage}
|
||||||
|
goToNextPage={$fetch.loading ? null : fetch.nextPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper {
|
.pagination {
|
||||||
flex: 1 1 auto;
|
|
||||||
margin: -28px -40px -40px -40px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
background: var(--background);
|
justify-content: flex-end;
|
||||||
overflow: hidden;
|
align-items: center;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.left-buttons,
|
||||||
|
.right-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { fade } from "svelte/transition"
|
import { fade } from "svelte/transition"
|
||||||
import { goto, params } from "@roxi/routify"
|
import { goto, params } from "@roxi/routify"
|
||||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
import { Table, Modal, Heading, notifications, Layout } from "@budibase/bbui"
|
||||||
|
import { API } from "api"
|
||||||
import Spinner from "components/common/Spinner.svelte"
|
import Spinner from "components/common/Spinner.svelte"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import DeleteRowsButton from "./buttons/DeleteRowsButton.svelte"
|
||||||
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
import CreateEditRow from "./modals/CreateEditRow.svelte"
|
||||||
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
import CreateEditUser from "./modals/CreateEditUser.svelte"
|
||||||
|
import CreateEditColumn from "./modals/CreateEditColumn.svelte"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
import {
|
import {
|
||||||
TableNames,
|
TableNames,
|
||||||
UNEDITABLE_USER_FIELDS,
|
UNEDITABLE_USER_FIELDS,
|
||||||
|
@ -17,6 +22,7 @@
|
||||||
export let data = []
|
export let data = []
|
||||||
export let tableId
|
export let tableId
|
||||||
export let title
|
export let title
|
||||||
|
export let allowEditing = false
|
||||||
export let loading = false
|
export let loading = false
|
||||||
export let hideAutocolumns
|
export let hideAutocolumns
|
||||||
export let rowCount
|
export let rowCount
|
||||||
|
@ -26,7 +32,12 @@
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let selectedRows = []
|
let selectedRows = []
|
||||||
|
let editableColumn
|
||||||
|
let editableRow
|
||||||
|
let editRowModal
|
||||||
|
let editColumnModal
|
||||||
let customRenderers = []
|
let customRenderers = []
|
||||||
|
let confirmDelete
|
||||||
|
|
||||||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||||
$: isUsersTable = tableId === TableNames.USERS
|
$: isUsersTable = tableId === TableNames.USERS
|
||||||
|
@ -81,6 +92,36 @@
|
||||||
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
|
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteRows = async targetRows => {
|
||||||
|
try {
|
||||||
|
await API.deleteRows({
|
||||||
|
tableId,
|
||||||
|
rows: targetRows,
|
||||||
|
})
|
||||||
|
|
||||||
|
const deletedRowIds = targetRows.map(row => row._id)
|
||||||
|
data = data.filter(row => deletedRowIds.indexOf(row._id))
|
||||||
|
|
||||||
|
notifications.success(`Successfully deleted ${targetRows.length} rows`)
|
||||||
|
} catch (error) {
|
||||||
|
notifications.error("Error deleting rows")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editRow = row => {
|
||||||
|
editableRow = row
|
||||||
|
if (row) {
|
||||||
|
editRowModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editColumn = field => {
|
||||||
|
editableColumn = cloneDeep(schema?.[field])
|
||||||
|
if (editableColumn) {
|
||||||
|
editColumnModal.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding gap="S">
|
<Layout noPadding gap="S">
|
||||||
|
@ -97,6 +138,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
<div class="popovers">
|
<div class="popovers">
|
||||||
<slot />
|
<slot />
|
||||||
|
{#if !isUsersTable && selectedRows.length > 0}
|
||||||
|
<DeleteRowsButton
|
||||||
|
on:updaterows
|
||||||
|
{selectedRows}
|
||||||
|
deleteRows={async rows => {
|
||||||
|
await deleteRows(rows)
|
||||||
|
resetSelectedRows()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
{#key tableId}
|
{#key tableId}
|
||||||
|
@ -109,7 +160,13 @@
|
||||||
{rowCount}
|
{rowCount}
|
||||||
{disableSorting}
|
{disableSorting}
|
||||||
{customPlaceholder}
|
{customPlaceholder}
|
||||||
|
bind:selectedRows
|
||||||
|
allowSelectRows={allowEditing && !isUsersTable}
|
||||||
|
allowEditRows={allowEditing}
|
||||||
|
allowEditColumns={allowEditing}
|
||||||
showAutoColumns={!hideAutocolumns}
|
showAutoColumns={!hideAutocolumns}
|
||||||
|
on:editcolumn={e => editColumn(e.detail)}
|
||||||
|
on:editrow={e => editRow(e.detail)}
|
||||||
on:clickrelationship={e => selectRelationship(e.detail)}
|
on:clickrelationship={e => selectRelationship(e.detail)}
|
||||||
on:sort
|
on:sort
|
||||||
>
|
>
|
||||||
|
@ -119,6 +176,42 @@
|
||||||
{/key}
|
{/key}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<Modal bind:this={editRowModal}>
|
||||||
|
<svelte:component
|
||||||
|
this={editRowComponent}
|
||||||
|
on:updaterows
|
||||||
|
on:deleteRows={() => {
|
||||||
|
confirmDelete.show()
|
||||||
|
}}
|
||||||
|
row={editableRow}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={confirmDelete}
|
||||||
|
okText="Delete"
|
||||||
|
onOk={async () => {
|
||||||
|
if (editableRow) {
|
||||||
|
await deleteRows([editableRow])
|
||||||
|
}
|
||||||
|
editableRow = undefined
|
||||||
|
}}
|
||||||
|
onCancel={async () => {
|
||||||
|
editRow(editableRow)
|
||||||
|
}}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
>
|
||||||
|
Are you sure you want to delete this row?
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<Modal bind:this={editColumnModal}>
|
||||||
|
<CreateEditColumn
|
||||||
|
field={editableColumn}
|
||||||
|
on:updatecolumns
|
||||||
|
onClosed={editColumnModal.hide}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.table-title {
|
.table-title {
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
{data}
|
{data}
|
||||||
{loading}
|
{loading}
|
||||||
{type}
|
{type}
|
||||||
|
allowEditing={false}
|
||||||
rowCount={10}
|
rowCount={10}
|
||||||
bind:hideAutocolumns
|
bind:hideAutocolumns
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="Calculator"
|
icon="Calculator"
|
||||||
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
active={view.field && view.calculation}
|
active={view.field && view.calculation}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton, Modal } from "@budibase/bbui"
|
||||||
|
import CreateEditColumn from "../modals/CreateEditColumn.svelte"
|
||||||
|
|
||||||
|
export let highlighted = false
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
|
let modal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
selected={highlighted}
|
||||||
|
emphasized={highlighted}
|
||||||
|
icon="TableColumnAddRight"
|
||||||
|
quiet
|
||||||
|
size="S"
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
|
Create column
|
||||||
|
</ActionButton>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateEditColumn on:updatecolumns />
|
||||||
|
</Modal>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
import { ActionButton, Modal } from "@budibase/bbui"
|
||||||
|
import CreateEditRow from "../modals/CreateEditRow.svelte"
|
||||||
|
|
||||||
|
export let modalContentComponent = CreateEditRow
|
||||||
|
export let title = "Create row"
|
||||||
|
export let disabled = false
|
||||||
|
export let highlighted = false
|
||||||
|
|
||||||
|
let modal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
emphasized={highlighted}
|
||||||
|
selected={highlighted}
|
||||||
|
icon="TableRowAddBottom"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</ActionButton>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<svelte:component this={modalContentComponent} on:updaterows />
|
||||||
|
</Modal>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { Modal, ActionButton } from "@budibase/bbui"
|
||||||
|
import CreateViewModal from "../modals/CreateViewModal.svelte"
|
||||||
|
|
||||||
|
export let disabled = false
|
||||||
|
|
||||||
|
let modal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
icon="CollectionAdd"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
|
Create view
|
||||||
|
</ActionButton>
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateViewModal />
|
||||||
|
</Modal>
|
|
@ -19,7 +19,7 @@
|
||||||
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
$: text = `${item}${selectedRows?.length === 1 ? "" : "s"}`
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button icon="Delete" warning quiet on:click={modal.show}>
|
<Button icon="Delete" size="s" warning quiet on:click={modal.show}>
|
||||||
Delete
|
Delete
|
||||||
{selectedRows.length}
|
{selectedRows.length}
|
||||||
{text}
|
{text}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import { ActionButton, Modal } from "@budibase/bbui"
|
import { Button, Modal } from "@budibase/bbui"
|
||||||
import EditRolesModal from "../modals/EditRoles.svelte"
|
import EditRolesModal from "../modals/EditRoles.svelte"
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="UsersLock" quiet on:click={modal.show}>
|
<div>
|
||||||
Edit roles
|
<Button icon="UsersLock" primary size="S" quiet on:click={modal.show}>
|
||||||
</ActionButton>
|
Edit roles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<EditRolesModal />
|
<EditRolesModal />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -7,23 +7,15 @@
|
||||||
export let table
|
export let table
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: datasource = findDatasource(table?._id)
|
|
||||||
$: plusTables = datasource?.plus
|
$: plusTables = datasource?.plus
|
||||||
? Object.values(datasource?.entities || {})
|
? Object.values(datasource?.entities || {})
|
||||||
: []
|
: []
|
||||||
|
$: datasource = $datasources.list.find(
|
||||||
|
source => source._id === table?.sourceId
|
||||||
|
)
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
|
|
||||||
const findDatasource = tableId => {
|
|
||||||
return $datasources.list.find(datasource => {
|
|
||||||
return (
|
|
||||||
Object.values(datasource.entities || {}).find(entity => {
|
|
||||||
return entity._id === tableId
|
|
||||||
}) != null
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveRelationship() {
|
async function saveRelationship() {
|
||||||
try {
|
try {
|
||||||
// Create datasource
|
// Create datasource
|
||||||
|
@ -36,9 +28,15 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if datasource}
|
{#if table.sourceId}
|
||||||
<div>
|
<div>
|
||||||
<ActionButton icon="DataCorrelated" primary quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
icon="DataCorrelated"
|
||||||
|
primary
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Define existing relationship
|
Define existing relationship
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,13 @@
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton {disabled} icon="DataDownload" quiet on:click={modal.show}>
|
<ActionButton
|
||||||
|
{disabled}
|
||||||
|
icon="DataDownload"
|
||||||
|
size="S"
|
||||||
|
quiet
|
||||||
|
on:click={modal.show}
|
||||||
|
>
|
||||||
Export
|
Export
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<Button
|
<Button
|
||||||
icon="Group"
|
icon="Group"
|
||||||
primary
|
primary
|
||||||
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
active={!!view.groupBy}
|
active={!!view.groupBy}
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
|
icon={hideAutocolumns ? "VisibilityOff" : "Visibility"}
|
||||||
primary
|
primary
|
||||||
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
on:click={hideOrUnhide}
|
on:click={hideOrUnhide}
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
let modal
|
let modal
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="DataUpload" quiet on:click={modal.show} {disabled}>
|
<ActionButton icon="DataUpload" size="S" quiet on:click={modal.show} {disabled}>
|
||||||
Import
|
Import
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
|
import ManageAccessModal from "../modals/ManageAccessModal.svelte"
|
||||||
|
|
||||||
export let resourceId
|
export let resourceId
|
||||||
export let disabled = false
|
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
let resourcePermissions
|
let resourcePermissions
|
||||||
|
@ -15,7 +14,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
<ActionButton icon="LockClosed" size="S" quiet on:click={openDropdown}>
|
||||||
Manage access
|
Manage access
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
|
|
|
@ -18,10 +18,11 @@
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="Filter"
|
icon="Filter"
|
||||||
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
selected={tempValue?.length > 0}
|
active={tempValue?.length > 0}
|
||||||
>
|
>
|
||||||
Filter
|
Filter
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
icon="Filter"
|
icon="Filter"
|
||||||
|
size="S"
|
||||||
quiet
|
quiet
|
||||||
on:click={modal.show}
|
on:click={modal.show}
|
||||||
active={view.filters?.length}
|
active={view.filters?.length}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { Modal, ActionButton } from "@budibase/bbui"
|
|
||||||
import CreateViewModal from "../../modals/CreateViewModal.svelte"
|
|
||||||
|
|
||||||
const { rows, columns } = getContext("grid")
|
|
||||||
|
|
||||||
let modal
|
|
||||||
|
|
||||||
$: disabled = !$columns.length || !$rows.length
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
|
||||||
Create view
|
|
||||||
</ActionButton>
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<CreateViewModal />
|
|
||||||
</Modal>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<script>
|
|
||||||
import ExportButton from "../ExportButton.svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { rows, columns, tableId, sort, selectedRows, filter } =
|
|
||||||
getContext("grid")
|
|
||||||
|
|
||||||
$: disabled = !$rows.length || !$columns.length
|
|
||||||
$: selectedRowArray = Object.keys($selectedRows).map(id => ({ _id: id }))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ExportButton
|
|
||||||
{disabled}
|
|
||||||
view={$tableId}
|
|
||||||
filters={$filter}
|
|
||||||
sorting={{
|
|
||||||
sortColumn: $sort.column,
|
|
||||||
sortOrder: $sort.order,
|
|
||||||
}}
|
|
||||||
selectedRows={selectedRowArray}
|
|
||||||
/>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<script>
|
|
||||||
import TableFilterButton from "../TableFilterButton.svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { columns, config, filter, table } = getContext("grid")
|
|
||||||
|
|
||||||
const onFilter = e => {
|
|
||||||
filter.set(e.detail || [])
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key $config.tableId}
|
|
||||||
<TableFilterButton
|
|
||||||
schema={$table?.schema}
|
|
||||||
filters={$filter}
|
|
||||||
on:change={onFilter}
|
|
||||||
disabled={!$columns.length}
|
|
||||||
tableId={$config.tableId}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
|
@ -1,14 +0,0 @@
|
||||||
<script>
|
|
||||||
import ImportButton from "../ImportButton.svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
export let disabled = false
|
|
||||||
|
|
||||||
const { rows, tableId } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ImportButton
|
|
||||||
{disabled}
|
|
||||||
tableId={$tableId}
|
|
||||||
on:importrows={rows.actions.refreshData}
|
|
||||||
/>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<script>
|
|
||||||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { config } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ManageAccessButton resourceId={$config.tableId} />
|
|
|
@ -1,13 +0,0 @@
|
||||||
<script>
|
|
||||||
import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { table, rows } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $table}
|
|
||||||
<ExistingRelationshipButton
|
|
||||||
table={$table}
|
|
||||||
on:updatecolumns={() => rows.actions.refreshData()}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
|
@ -182,12 +182,8 @@
|
||||||
indexes,
|
indexes,
|
||||||
})
|
})
|
||||||
dispatch("updatecolumns")
|
dispatch("updatecolumns")
|
||||||
if (originalName) {
|
|
||||||
notifications.success("Column updated successfully")
|
|
||||||
} else {
|
|
||||||
notifications.success("Column created successfully")
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
notifications.error(`Error saving column: ${err.message}`)
|
notifications.error(`Error saving column: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,7 +199,7 @@
|
||||||
notifications.error("You cannot delete the display column")
|
notifications.error("You cannot delete the display column")
|
||||||
} else {
|
} else {
|
||||||
await tables.deleteField(editableColumn)
|
await tables.deleteField(editableColumn)
|
||||||
notifications.success(`Column ${editableColumn.name} deleted`)
|
notifications.success(`Column ${editableColumn.name} deleted.`)
|
||||||
confirmDeleteDialog.hide()
|
confirmDeleteDialog.hide()
|
||||||
hide()
|
hide()
|
||||||
deletion = false
|
deletion = false
|
||||||
|
|
|
@ -23,9 +23,9 @@
|
||||||
async function saveRow() {
|
async function saveRow() {
|
||||||
errors = []
|
errors = []
|
||||||
try {
|
try {
|
||||||
const res = await API.saveRow({ ...row, tableId: table._id })
|
await API.saveRow({ ...row, tableId: table._id })
|
||||||
notifications.success("Row saved successfully")
|
notifications.success("Row saved successfully")
|
||||||
dispatch("updaterows", res._id)
|
dispatch("updaterows")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const response = error.json
|
const response = error.json
|
||||||
if (error.handled && response?.errors) {
|
if (error.handled && response?.errors) {
|
||||||
|
|
|
@ -55,9 +55,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await API.saveRow({ ...row, tableId: table._id })
|
await API.saveRow({ ...row, tableId: table._id })
|
||||||
notifications.success("User saved successfully")
|
notifications.success("User saved successfully")
|
||||||
dispatch("updaterows", res.id)
|
dispatch("updaterows")
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.handled) {
|
if (error.handled) {
|
||||||
const response = error.json
|
const response = error.json
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { Modal } from "@budibase/bbui"
|
|
||||||
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
|
|
||||||
|
|
||||||
const { rows, subscribe } = getContext("grid")
|
|
||||||
|
|
||||||
let modal
|
|
||||||
|
|
||||||
onMount(() => subscribe("add-column", modal.show))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />
|
|
||||||
</Modal>
|
|
|
@ -1,28 +0,0 @@
|
||||||
<script>
|
|
||||||
import CreateEditRow from "../../modals/CreateEditRow.svelte"
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { Modal } from "@budibase/bbui"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
|
|
||||||
const { subscribe, rows } = getContext("grid")
|
|
||||||
|
|
||||||
let modal
|
|
||||||
let row
|
|
||||||
|
|
||||||
onMount(() =>
|
|
||||||
subscribe("add-row", () => {
|
|
||||||
row = {}
|
|
||||||
modal.show()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
onMount(() =>
|
|
||||||
subscribe("edit-row", rowToEdit => {
|
|
||||||
row = cloneDeep(rowToEdit)
|
|
||||||
modal.show()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<CreateEditRow {row} on:updaterows={e => rows.actions.refreshRow(e.detail)} />
|
|
||||||
</Modal>
|
|
|
@ -1,24 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { Modal } from "@budibase/bbui"
|
|
||||||
import CreateEditColumn from "../CreateEditColumn.svelte"
|
|
||||||
|
|
||||||
const { rows, subscribe } = getContext("grid")
|
|
||||||
|
|
||||||
let editableColumn
|
|
||||||
let editColumnModal
|
|
||||||
|
|
||||||
const editColumn = column => {
|
|
||||||
editableColumn = column
|
|
||||||
editColumnModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => subscribe("edit-column", editColumn))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={editColumnModal}>
|
|
||||||
<CreateEditColumn
|
|
||||||
field={editableColumn}
|
|
||||||
on:updatecolumns={rows.actions.refreshData}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script>
|
|
||||||
import CreateEditUser from "../../modals/CreateEditUser.svelte"
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { Modal } from "@budibase/bbui"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
|
|
||||||
const { subscribe, rows } = getContext("grid")
|
|
||||||
|
|
||||||
let modal
|
|
||||||
let row
|
|
||||||
|
|
||||||
onMount(() =>
|
|
||||||
subscribe("edit-row", rowToEdit => {
|
|
||||||
row = cloneDeep(rowToEdit)
|
|
||||||
modal.show()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<CreateEditUser
|
|
||||||
{row}
|
|
||||||
on:updaterows={e => rows.actions.refreshRow(e.detail)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
|
@ -13,13 +13,6 @@
|
||||||
$: sortedTables = $tables.list
|
$: sortedTables = $tables.list
|
||||||
.filter(table => table.sourceId === sourceId)
|
.filter(table => table.sourceId === sourceId)
|
||||||
.sort(alphabetical)
|
.sort(alphabetical)
|
||||||
|
|
||||||
const selectTable = tableId => {
|
|
||||||
tables.select(tableId)
|
|
||||||
if (!$isActive("./table/:tableId")) {
|
|
||||||
$goto(`./table/${tableId}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $database?._id}
|
{#if $database?._id}
|
||||||
|
@ -32,7 +25,7 @@
|
||||||
text={table.name}
|
text={table.name}
|
||||||
selected={$isActive("./table/:tableId") &&
|
selected={$isActive("./table/:tableId") &&
|
||||||
$tables.selected?._id === table._id}
|
$tables.selected?._id === table._id}
|
||||||
on:click={() => selectTable(table._id)}
|
on:click={() => $goto(`./table/${table._id}`)}
|
||||||
>
|
>
|
||||||
{#if table._id !== TableNames.USERS}
|
{#if table._id !== TableNames.USERS}
|
||||||
<EditTablePopover {table} />
|
<EditTablePopover {table} />
|
||||||
|
|
|
@ -325,4 +325,9 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -59,7 +59,9 @@
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
<AppLockModal {app} buttonSize="M" />
|
<AppLockModal {app} buttonSize="M" />
|
||||||
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
<Button size="S" secondary on:click={goToOverview}>Manage</Button>
|
||||||
<Button size="S" primary on:click={goToBuilder}>Edit</Button>
|
<Button size="S" primary disabled={app.lockedOther} on:click={goToBuilder}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,7 @@ a {
|
||||||
background: var(--spectrum-alias-background-color-default);
|
background: var(--spectrum-alias-background-color-default);
|
||||||
}
|
}
|
||||||
html * {
|
html * {
|
||||||
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--spectrum-global-color-gray-400)
|
scrollbar-color: var(--spectrum-global-color-gray-400)
|
||||||
var(--spectrum-alias-background-color-default);
|
var(--spectrum-alias-background-color-default);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
"dataprovider",
|
"dataprovider",
|
||||||
"repeater",
|
"repeater",
|
||||||
"table",
|
"table",
|
||||||
"spreadsheet",
|
|
||||||
"dynamicfilter",
|
"dynamicfilter",
|
||||||
"daterangepicker"
|
"daterangepicker"
|
||||||
]
|
]
|
||||||
|
|
|
@ -135,24 +135,6 @@ export function createTablesStore() {
|
||||||
await save(draft)
|
await save(draft)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTable = table => {
|
|
||||||
const index = get(store).list.findIndex(x => x._id === table._id)
|
|
||||||
if (index === -1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function has to merge state as there discrepancies with the table
|
|
||||||
// API endpoints. The table list endpoint and get table endpoint use the
|
|
||||||
// "type" property to mean different things.
|
|
||||||
store.update(state => {
|
|
||||||
state.list[index] = {
|
|
||||||
...table,
|
|
||||||
type: state.list[index].type,
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
fetch,
|
fetch,
|
||||||
|
@ -163,7 +145,6 @@ export function createTablesStore() {
|
||||||
delete: deleteTable,
|
delete: deleteTable,
|
||||||
saveField,
|
saveField,
|
||||||
deleteField,
|
deleteField,
|
||||||
updateTable,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5161,36 +5161,5 @@
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
{#if $builderStore.usedPlugins?.length}
|
{#if $builderStore.usedPlugins?.length}
|
||||||
{#each $builderStore.usedPlugins as plugin (plugin.hash)}
|
{#each $builderStore.usedPlugins as plugin}
|
||||||
<script src={`${plugin.jsUrl}`}></script>
|
<script src={`${plugin.jsUrl}`}></script>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import {
|
||||||
environmentStore,
|
environmentStore,
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
confirmationStore,
|
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { styleable } from "utils/styleable"
|
import { styleable } from "utils/styleable"
|
||||||
import { linkable } from "utils/linkable"
|
import { linkable } from "utils/linkable"
|
||||||
|
@ -36,7 +35,6 @@ export default {
|
||||||
sidePanelStore,
|
sidePanelStore,
|
||||||
dndIsDragging,
|
dndIsDragging,
|
||||||
currentRole,
|
currentRole,
|
||||||
confirmationStore,
|
|
||||||
styleable,
|
styleable,
|
||||||
linkable,
|
linkable,
|
||||||
getAction,
|
getAction,
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
export const domDebounce = (callback, extractParams = x => x) => {
|
export const domDebounce = callback => {
|
||||||
let active = false
|
let active = false
|
||||||
let lastParams
|
return e => {
|
||||||
return (...params) => {
|
|
||||||
lastParams = extractParams(...params)
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
active = true
|
window.requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
callback(e)
|
||||||
callback(lastParams)
|
|
||||||
active = false
|
active = false
|
||||||
})
|
})
|
||||||
|
active = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "2.5.6-alpha.6",
|
"@budibase/bbui": "2.5.6-alpha.6",
|
||||||
"@budibase/shared-core": "2.5.6-alpha.6",
|
"@budibase/shared-core": "2.5.6-alpha.6",
|
||||||
"dayjs": "^1.11.7",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"socket.io-client": "^4.6.1",
|
|
||||||
"svelte": "^3.46.2"
|
"svelte": "^3.46.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,153 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { Dropzone, notifications } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let focused = false
|
|
||||||
export let onChange
|
|
||||||
export let readonly = false
|
|
||||||
export let api
|
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
const { API } = getContext("grid")
|
|
||||||
const imageExtensions = ["png", "tiff", "gif", "raw", "jpg", "jpeg"]
|
|
||||||
|
|
||||||
let isOpen = false
|
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
$: {
|
|
||||||
if (!focused) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = () => {
|
|
||||||
return isOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
isOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
isOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isImage = extension => {
|
|
||||||
return imageExtensions.includes(extension?.toLowerCase())
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileTooLarge = fileSizeLimit => {
|
|
||||||
notifications.error(
|
|
||||||
`Files cannot exceed ${
|
|
||||||
fileSizeLimit / 1000000
|
|
||||||
}MB. Please try again with smaller files.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const processFiles = async fileList => {
|
|
||||||
let data = new FormData()
|
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
|
||||||
data.append("file", fileList[i])
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await API.uploadBuilderAttachment(data)
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Failed to upload attachment")
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteAttachments = async fileList => {
|
|
||||||
try {
|
|
||||||
return await API.deleteBuilderAttachments(fileList)
|
|
||||||
} catch (error) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
api = {
|
|
||||||
focus: () => open(),
|
|
||||||
blur: () => close(),
|
|
||||||
onKeyDown,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="attachment-cell" class:editable on:click={editable ? open : null}>
|
|
||||||
{#each value || [] as attachment}
|
|
||||||
{#if isImage(attachment.extension)}
|
|
||||||
<img src={attachment.url} alt={attachment.extension} />
|
|
||||||
{:else}
|
|
||||||
<div class="file" title={attachment.name}>
|
|
||||||
{attachment.extension}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if isOpen}
|
|
||||||
<div class="dropzone" class:invertX class:invertY>
|
|
||||||
<Dropzone
|
|
||||||
{value}
|
|
||||||
compact
|
|
||||||
on:change={e => onChange(e.detail)}
|
|
||||||
{processFiles}
|
|
||||||
{deleteAttachments}
|
|
||||||
{handleFileTooLarge}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.attachment-cell {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
align-self: stretch;
|
|
||||||
overflow: hidden;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.attachment-cell.editable:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.file {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
color: var(--spectrum-global-color-gray-700);
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
border-radius: 4px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 12px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.dropzone {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
width: 320px;
|
|
||||||
background: var(--background);
|
|
||||||
border: var(--cell-border);
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
.dropzone.invertX {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.dropzone.invertY {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,44 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import { Checkbox } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let focused = false
|
|
||||||
export let onChange
|
|
||||||
export let readonly = false
|
|
||||||
export let api
|
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
onChange(e.detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = e => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
onChange(!value)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
api = {
|
|
||||||
onKeyDown,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="boolean-cell" class:editable>
|
|
||||||
<Checkbox {value} on:change={handleChange} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.boolean-cell {
|
|
||||||
padding: 2px var(--cell-padding);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.boolean-cell.editable {
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,86 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import GridCell from "./GridCell.svelte"
|
|
||||||
import { getCellRenderer } from "../lib/renderers"
|
|
||||||
import { derived, writable } from "svelte/store"
|
|
||||||
|
|
||||||
const { rows, focusedCellId, focusedCellAPI, menu, config, validation } =
|
|
||||||
getContext("grid")
|
|
||||||
|
|
||||||
export let highlighted
|
|
||||||
export let selected
|
|
||||||
export let rowFocused
|
|
||||||
export let rowIdx
|
|
||||||
export let focused
|
|
||||||
export let selectedUser
|
|
||||||
export let column
|
|
||||||
export let row
|
|
||||||
export let cellId
|
|
||||||
export let updateValue = rows.actions.updateValue
|
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
const emptyError = writable(null)
|
|
||||||
|
|
||||||
let api
|
|
||||||
|
|
||||||
// Get the error for this cell if the row is focused
|
|
||||||
$: error = getErrorStore(rowFocused, cellId)
|
|
||||||
|
|
||||||
// Determine if the cell is editable
|
|
||||||
$: readonly =
|
|
||||||
column.schema.autocolumn ||
|
|
||||||
column.schema.disabled ||
|
|
||||||
(!$config.allowEditRows && row._id)
|
|
||||||
|
|
||||||
// Register this cell API if the row is focused
|
|
||||||
$: {
|
|
||||||
if (focused) {
|
|
||||||
focusedCellAPI.set(cellAPI)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorStore = (selected, cellId) => {
|
|
||||||
if (!selected) {
|
|
||||||
return emptyError
|
|
||||||
}
|
|
||||||
return derived(validation, $validation => $validation[cellId])
|
|
||||||
}
|
|
||||||
|
|
||||||
const cellAPI = {
|
|
||||||
focus: () => api?.focus(),
|
|
||||||
blur: () => api?.blur(),
|
|
||||||
onKeyDown: (...params) => api?.onKeyDown(...params),
|
|
||||||
isReadonly: () => readonly,
|
|
||||||
getType: () => column.schema.type,
|
|
||||||
getValue: () => row[column.name],
|
|
||||||
setValue: value => {
|
|
||||||
validation.actions.setError(cellId, null)
|
|
||||||
updateValue(row._id, column.name, value)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<GridCell
|
|
||||||
{highlighted}
|
|
||||||
{selected}
|
|
||||||
{rowIdx}
|
|
||||||
{focused}
|
|
||||||
{selectedUser}
|
|
||||||
error={$error}
|
|
||||||
on:click={() => focusedCellId.set(cellId)}
|
|
||||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
|
||||||
width={column.width}
|
|
||||||
>
|
|
||||||
<svelte:component
|
|
||||||
this={getCellRenderer(column)}
|
|
||||||
bind:api
|
|
||||||
value={row[column.name]}
|
|
||||||
schema={column.schema}
|
|
||||||
onChange={cellAPI.setValue}
|
|
||||||
{focused}
|
|
||||||
{readonly}
|
|
||||||
{invertY}
|
|
||||||
{invertX}
|
|
||||||
/>
|
|
||||||
</GridCell>
|
|
|
@ -1,77 +0,0 @@
|
||||||
<script>
|
|
||||||
import dayjs from "dayjs"
|
|
||||||
import { CoreDatePicker, Icon } from "@budibase/bbui"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let schema
|
|
||||||
export let onChange
|
|
||||||
export let focused = false
|
|
||||||
export let readonly = false
|
|
||||||
|
|
||||||
// adding the 0- will turn a string like 00:00:00 into a valid ISO
|
|
||||||
// date, but will make actual ISO dates invalid
|
|
||||||
$: time = new Date(`0-${value}`)
|
|
||||||
$: timeOnly = !isNaN(time) || schema?.timeOnly
|
|
||||||
$: dateOnly = schema?.dateOnly
|
|
||||||
$: format = timeOnly
|
|
||||||
? "HH:mm:ss"
|
|
||||||
: dateOnly
|
|
||||||
? "MMM D YYYY"
|
|
||||||
: "MMM D YYYY, HH:mm"
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="value">
|
|
||||||
{#if value}
|
|
||||||
{dayjs(timeOnly ? time : value).format(format)}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if editable}
|
|
||||||
<Icon name="Calendar" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if editable}
|
|
||||||
<div class="picker">
|
|
||||||
<CoreDatePicker
|
|
||||||
{value}
|
|
||||||
on:change={e => onChange(e.detail)}
|
|
||||||
appendTo={document.documentElement}
|
|
||||||
enableTime={!dateOnly}
|
|
||||||
{timeOnly}
|
|
||||||
time24hr
|
|
||||||
ignoreTimezones={schema.ignoreTimezones}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.picker {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.picker :global(.flatpickr) {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.picker :global(.spectrum-Textfield-input) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import TextCell from "./TextCell.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TextCell {...$$props} readonly />
|
|
|
@ -1,159 +0,0 @@
|
||||||
<script>
|
|
||||||
export let focused = false
|
|
||||||
export let selected = false
|
|
||||||
export let highlighted = false
|
|
||||||
export let width = ""
|
|
||||||
export let selectedUser = null
|
|
||||||
export let error = null
|
|
||||||
export let rowIdx
|
|
||||||
export let defaultHeight = false
|
|
||||||
export let center = false
|
|
||||||
|
|
||||||
$: style = getStyle(width, selectedUser)
|
|
||||||
|
|
||||||
const getStyle = (width, selectedUser) => {
|
|
||||||
let style = `flex: 0 0 ${width}px;`
|
|
||||||
if (selectedUser) {
|
|
||||||
style += `--cell-color:${selectedUser.color};`
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="cell"
|
|
||||||
class:selected
|
|
||||||
class:highlighted
|
|
||||||
class:focused
|
|
||||||
class:error
|
|
||||||
class:center
|
|
||||||
class:default-height={defaultHeight}
|
|
||||||
class:selected-other={selectedUser != null}
|
|
||||||
on:focus
|
|
||||||
on:mousedown
|
|
||||||
on:mouseup
|
|
||||||
on:click
|
|
||||||
on:contextmenu
|
|
||||||
{style}
|
|
||||||
data-row={rowIdx}
|
|
||||||
>
|
|
||||||
{#if error}
|
|
||||||
<div class="label">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<slot />
|
|
||||||
{#if selectedUser && !focused}
|
|
||||||
<div class="label">
|
|
||||||
{selectedUser.label}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Cells */
|
|
||||||
.cell {
|
|
||||||
height: var(--row-height);
|
|
||||||
border-bottom: var(--cell-border);
|
|
||||||
border-right: var(--cell-border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
color: var(--spectrum-global-color-gray-800);
|
|
||||||
font-size: var(--cell-font-size);
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
background: var(--cell-background);
|
|
||||||
position: relative;
|
|
||||||
width: 0;
|
|
||||||
--cell-color: transparent;
|
|
||||||
}
|
|
||||||
.cell.default-height {
|
|
||||||
height: var(--default-row-height);
|
|
||||||
}
|
|
||||||
.cell.center {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cell border */
|
|
||||||
.cell.focused:after,
|
|
||||||
.cell.error:after,
|
|
||||||
.cell.selected-other:not(.focused):after {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border: 2px solid var(--cell-color);
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cell border for cells with labels */
|
|
||||||
.cell.error:after,
|
|
||||||
.cell.selected-other:not(.focused):after {
|
|
||||||
border-radius: 0 2px 2px 2px;
|
|
||||||
}
|
|
||||||
.cell[data-row="0"].error:after,
|
|
||||||
.cell[data-row="0"].selected-other:not(.focused):after {
|
|
||||||
border-radius: 2px 2px 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cell z-index */
|
|
||||||
.cell.error,
|
|
||||||
.cell.selected-other:not(.focused) {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.cell.focused {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.cell.focused {
|
|
||||||
--cell-color: var(--spectrum-global-color-blue-400);
|
|
||||||
}
|
|
||||||
.cell.error {
|
|
||||||
--cell-color: var(--spectrum-global-color-red-500);
|
|
||||||
}
|
|
||||||
.cell:not(.focused) {
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.cell:hover {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
.cell.highlighted:not(.focused) {
|
|
||||||
--cell-background: var(--cell-background-hover);
|
|
||||||
}
|
|
||||||
.cell.selected:not(.focused) {
|
|
||||||
--cell-background: var(--spectrum-global-color-blue-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Label for additional text */
|
|
||||||
.label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%;
|
|
||||||
left: 0;
|
|
||||||
padding: 1px 4px 3px 4px;
|
|
||||||
margin: 0 0 -2px 0;
|
|
||||||
background: var(--user-color);
|
|
||||||
border-radius: 2px;
|
|
||||||
display: block;
|
|
||||||
color: white;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.cell[data-row="0"] .label {
|
|
||||||
bottom: auto;
|
|
||||||
top: 100%;
|
|
||||||
border-radius: 0 2px 2px 2px;
|
|
||||||
padding: 2px 4px 2px 4px;
|
|
||||||
margin: -2px 0 0 0;
|
|
||||||
}
|
|
||||||
.error .label {
|
|
||||||
background: var(--spectrum-global-color-red-500);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,237 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import GridCell from "./GridCell.svelte"
|
|
||||||
import { Icon, Popover, Menu, MenuItem } from "@budibase/bbui"
|
|
||||||
import { getColumnIcon } from "../lib/utils"
|
|
||||||
|
|
||||||
export let column
|
|
||||||
export let idx
|
|
||||||
export let orderable = true
|
|
||||||
|
|
||||||
const {
|
|
||||||
reorder,
|
|
||||||
isReordering,
|
|
||||||
isResizing,
|
|
||||||
rand,
|
|
||||||
sort,
|
|
||||||
renderedColumns,
|
|
||||||
dispatch,
|
|
||||||
config,
|
|
||||||
ui,
|
|
||||||
columns,
|
|
||||||
} = getContext("grid")
|
|
||||||
const bannedDisplayColumnTypes = [
|
|
||||||
"link",
|
|
||||||
"array",
|
|
||||||
"attachment",
|
|
||||||
"boolean",
|
|
||||||
"formula",
|
|
||||||
"json",
|
|
||||||
]
|
|
||||||
|
|
||||||
let anchor
|
|
||||||
let open = false
|
|
||||||
let timeout
|
|
||||||
|
|
||||||
$: sortedBy = column.name === $sort.column
|
|
||||||
$: canMoveLeft = orderable && idx > 0
|
|
||||||
$: canMoveRight = orderable && idx < $renderedColumns.length - 1
|
|
||||||
|
|
||||||
const editColumn = () => {
|
|
||||||
dispatch("edit-column", column.schema)
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseDown = e => {
|
|
||||||
if (e.button === 0 && orderable) {
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
reorder.actions.startReordering(column.name, e)
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMouseUp = e => {
|
|
||||||
if (e.button === 0 && orderable) {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onContextMenu = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
ui.actions.blur()
|
|
||||||
open = !open
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortAscending = () => {
|
|
||||||
sort.set({
|
|
||||||
column: column.name,
|
|
||||||
order: "ascending",
|
|
||||||
})
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortDescending = () => {
|
|
||||||
sort.set({
|
|
||||||
column: column.name,
|
|
||||||
order: "descending",
|
|
||||||
})
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveLeft = () => {
|
|
||||||
reorder.actions.moveColumnLeft(column.name)
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const moveRight = () => {
|
|
||||||
reorder.actions.moveColumnRight(column.name)
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeDisplayColumn = () => {
|
|
||||||
columns.actions.changePrimaryDisplay(column.name)
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="header-cell"
|
|
||||||
class:open
|
|
||||||
style="flex: 0 0 {column.width}px;"
|
|
||||||
bind:this={anchor}
|
|
||||||
class:disabled={$isReordering || $isResizing}
|
|
||||||
class:sorted={sortedBy}
|
|
||||||
>
|
|
||||||
<GridCell
|
|
||||||
on:mousedown={onMouseDown}
|
|
||||||
on:mouseup={onMouseUp}
|
|
||||||
on:contextmenu={onContextMenu}
|
|
||||||
width={column.width}
|
|
||||||
left={column.left}
|
|
||||||
defaultHeight
|
|
||||||
center
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name={getColumnIcon(column)}
|
|
||||||
color={`var(--spectrum-global-color-gray-600)`}
|
|
||||||
/>
|
|
||||||
<div class="name">
|
|
||||||
{column.label}
|
|
||||||
</div>
|
|
||||||
{#if sortedBy}
|
|
||||||
<div class="sort-indicator">
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name={$sort.order === "descending" ? "SortOrderDown" : "SortOrderUp"}
|
|
||||||
color="var(--spectrum-global-color-gray-600)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div
|
|
||||||
class="more"
|
|
||||||
on:mousedown|stopPropagation
|
|
||||||
on:click={() => (open = true)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name="MoreVertical"
|
|
||||||
color="var(--spectrum-global-color-gray-600)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</GridCell>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
bind:open
|
|
||||||
{anchor}
|
|
||||||
align="right"
|
|
||||||
offset={0}
|
|
||||||
popoverTarget={document.getElementById(`grid-${rand}`)}
|
|
||||||
animate={false}
|
|
||||||
>
|
|
||||||
<Menu>
|
|
||||||
<MenuItem
|
|
||||||
icon="Edit"
|
|
||||||
on:click={editColumn}
|
|
||||||
disabled={!$config.allowEditColumns || column.schema.disabled}
|
|
||||||
>
|
|
||||||
Edit column
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="Label"
|
|
||||||
on:click={makeDisplayColumn}
|
|
||||||
disabled={idx === "sticky" ||
|
|
||||||
!$config.allowEditColumns ||
|
|
||||||
bannedDisplayColumnTypes.includes(column.schema.type)}
|
|
||||||
>
|
|
||||||
Use as display column
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="SortOrderUp"
|
|
||||||
on:click={sortAscending}
|
|
||||||
disabled={column.name === $sort.column && $sort.order === "ascending"}
|
|
||||||
>
|
|
||||||
Sort A-Z
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="SortOrderDown"
|
|
||||||
on:click={sortDescending}
|
|
||||||
disabled={column.name === $sort.column && $sort.order === "descending"}
|
|
||||||
>
|
|
||||||
Sort Z-A
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
|
|
||||||
Move left
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
|
|
||||||
Move right
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.header-cell {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.header-cell.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.header-cell :global(.cell) {
|
|
||||||
padding: 0 var(--cell-padding);
|
|
||||||
gap: calc(2 * var(--cell-spacing));
|
|
||||||
background: var(--spectrum-global-color-gray-100);
|
|
||||||
}
|
|
||||||
.header-cell.sorted :global(.cell) {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more {
|
|
||||||
display: none;
|
|
||||||
padding: 4px;
|
|
||||||
margin: 0 -4px;
|
|
||||||
}
|
|
||||||
.header-cell.open .more,
|
|
||||||
.header-cell:hover .more {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.more:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.more:hover :global(.spectrum-Icon) {
|
|
||||||
color: var(--spectrum-global-color-gray-800) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-cell.open .sort-indicator,
|
|
||||||
.header-cell:hover .sort-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,36 +0,0 @@
|
||||||
<script>
|
|
||||||
import LongFormCell from "./LongFormCell.svelte"
|
|
||||||
|
|
||||||
export let onChange
|
|
||||||
export let value
|
|
||||||
export let api
|
|
||||||
|
|
||||||
$: stringified = getStringifiedValue(value)
|
|
||||||
|
|
||||||
const getStringifiedValue = value => {
|
|
||||||
if (!value) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value, null, 2)
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parse = value => {
|
|
||||||
const trimmed = value?.trim()
|
|
||||||
if (!trimmed) {
|
|
||||||
onChange(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed)
|
|
||||||
onChange(parsed)
|
|
||||||
} catch (error) {
|
|
||||||
// Swallow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<LongFormCell {...$$props} bind:api value={stringified} onChange={parse} />
|
|
|
@ -1,117 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount, tick } from "svelte"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let focused = false
|
|
||||||
export let onChange
|
|
||||||
export let readonly = false
|
|
||||||
export let api
|
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
let textarea
|
|
||||||
let isOpen = false
|
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
$: {
|
|
||||||
if (!focused) {
|
|
||||||
isOpen = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = () => {
|
|
||||||
return isOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = async () => {
|
|
||||||
isOpen = true
|
|
||||||
await tick()
|
|
||||||
textarea.focus()
|
|
||||||
textarea.setSelectionRange(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
textarea?.blur()
|
|
||||||
isOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
api = {
|
|
||||||
focus: () => open(),
|
|
||||||
blur: () => close(),
|
|
||||||
onKeyDown,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if isOpen}
|
|
||||||
<textarea
|
|
||||||
class:invertX
|
|
||||||
class:invertY
|
|
||||||
bind:this={textarea}
|
|
||||||
value={value || ""}
|
|
||||||
on:change={handleChange}
|
|
||||||
on:wheel|stopPropagation
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="long-form-cell" on:click={editable ? open : null} class:editable>
|
|
||||||
<div class="value">
|
|
||||||
{value || ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.long-form-cell {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
align-self: stretch;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.long-form-cell.editable:hover {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: var(--content-lines);
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
margin: 0;
|
|
||||||
border: 2px solid var(--cell-color);
|
|
||||||
background: var(--cell-background);
|
|
||||||
font-size: var(--cell-font-size);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
color: inherit;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: calc(100% + var(--max-cell-render-width-overflow));
|
|
||||||
height: var(--max-cell-render-height);
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: 2px;
|
|
||||||
resize: none;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
textarea.invertX {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
textarea.invertY {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
top: calc(100% + 1px);
|
|
||||||
}
|
|
||||||
textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import OptionsCell from "./OptionsCell.svelte"
|
|
||||||
|
|
||||||
export let api
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<OptionsCell bind:api {...$$props} multi />
|
|
|
@ -1,7 +0,0 @@
|
||||||
<script>
|
|
||||||
import TextCell from "./TextCell.svelte"
|
|
||||||
|
|
||||||
export let api
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TextCell {...$$props} bind:api type="number" />
|
|
|
@ -1,239 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Icon } from "@budibase/bbui"
|
|
||||||
import { getColor } from "../lib/utils"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let schema
|
|
||||||
export let onChange
|
|
||||||
export let focused = false
|
|
||||||
export let multi = false
|
|
||||||
export let readonly = false
|
|
||||||
export let api
|
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
let isOpen = false
|
|
||||||
let focusedOptionIdx = null
|
|
||||||
|
|
||||||
$: options = schema?.constraints?.inclusion || []
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
|
|
||||||
$: {
|
|
||||||
// Close when deselected
|
|
||||||
if (!focused) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
isOpen = true
|
|
||||||
focusedOptionIdx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
isOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOptionColor = value => {
|
|
||||||
const index = value ? options.indexOf(value) : null
|
|
||||||
return getColor(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleOption = option => {
|
|
||||||
if (!multi) {
|
|
||||||
onChange(option === value ? null : option)
|
|
||||||
close()
|
|
||||||
} else {
|
|
||||||
const sanitizedValues = values.filter(x => options.includes(x))
|
|
||||||
if (sanitizedValues.includes(option)) {
|
|
||||||
onChange(sanitizedValues.filter(x => x !== option))
|
|
||||||
} else {
|
|
||||||
onChange([...sanitizedValues, option])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = e => {
|
|
||||||
if (!isOpen) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
focusedOptionIdx = Math.min(focusedOptionIdx + 1, options.length - 1)
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
focusedOptionIdx = Math.max(focusedOptionIdx - 1, 0)
|
|
||||||
} else if (e.key === "Enter") {
|
|
||||||
toggleOption(options[focusedOptionIdx])
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
api = {
|
|
||||||
focus: open,
|
|
||||||
blur: close,
|
|
||||||
onKeyDown,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="container"
|
|
||||||
class:multi
|
|
||||||
class:editable
|
|
||||||
class:open
|
|
||||||
on:click|self={editable ? open : null}
|
|
||||||
>
|
|
||||||
<div class="values" on:click={editable ? open : null}>
|
|
||||||
{#each values as val}
|
|
||||||
{@const color = getOptionColor(val)}
|
|
||||||
{#if color}
|
|
||||||
<div class="badge text" style="--color: {color}">
|
|
||||||
<span>
|
|
||||||
{val}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="text">
|
|
||||||
{val || ""}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if editable}
|
|
||||||
<div class="arrow" on:click={open}>
|
|
||||||
<Icon name="ChevronDown" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if isOpen}
|
|
||||||
<div
|
|
||||||
class="options"
|
|
||||||
class:invertX
|
|
||||||
class:invertY
|
|
||||||
on:wheel={e => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{#each options as option, idx}
|
|
||||||
{@const color = getOptionColor(option)}
|
|
||||||
<div
|
|
||||||
class="option"
|
|
||||||
on:click={() => toggleOption(option)}
|
|
||||||
class:focused={focusedOptionIdx === idx}
|
|
||||||
on:mouseenter={() => (focusedOptionIdx = idx)}
|
|
||||||
>
|
|
||||||
<div class="badge text" style="--color: {color}">
|
|
||||||
{option}
|
|
||||||
</div>
|
|
||||||
{#if values.includes(option)}
|
|
||||||
<Icon
|
|
||||||
name="Checkmark"
|
|
||||||
color="var(--spectrum-global-color-blue-400)"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-self: stretch;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.container.editable:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.values {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
grid-column-gap: var(--cell-spacing);
|
|
||||||
grid-row-gap: var(--cell-padding);
|
|
||||||
overflow: hidden;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.text {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.multi .text {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
padding: 0 var(--cell-padding);
|
|
||||||
background: var(--color);
|
|
||||||
border-radius: var(--cell-padding);
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
height: 20px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
.badge span {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.arrow {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
bottom: 2px;
|
|
||||||
padding: 0 6px 0 16px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent 0%,
|
|
||||||
var(--cell-background) 40%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.options {
|
|
||||||
min-width: calc(100% + 2px);
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: -1px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
max-height: var(--max-cell-render-height);
|
|
||||||
overflow-y: auto;
|
|
||||||
border: var(--cell-border);
|
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
.options.invertX {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
.options.invertY {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
.option {
|
|
||||||
flex: 0 0 var(--default-row-height);
|
|
||||||
padding: 0 var(--cell-padding);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
background-color: var(--background);
|
|
||||||
}
|
|
||||||
.option:hover,
|
|
||||||
.option.focused {
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,503 +0,0 @@
|
||||||
<script context="module">
|
|
||||||
// We can create a module level cache for all relationship cells to avoid
|
|
||||||
// having to fetch the table definition one time for each cell
|
|
||||||
let primaryDisplayCache = {}
|
|
||||||
|
|
||||||
const getPrimaryDisplayForTableId = async (API, tableId) => {
|
|
||||||
if (primaryDisplayCache[tableId]) {
|
|
||||||
return primaryDisplayCache[tableId]
|
|
||||||
}
|
|
||||||
const definition = await API.fetchTableDefinition(tableId)
|
|
||||||
const primaryDisplay =
|
|
||||||
definition?.primaryDisplay || definition?.schema?.[0]?.name
|
|
||||||
primaryDisplayCache[tableId] = primaryDisplay
|
|
||||||
return primaryDisplay
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { getColor } from "../lib/utils"
|
|
||||||
import { onMount, getContext } from "svelte"
|
|
||||||
import { Icon, Input, ProgressCircle } from "@budibase/bbui"
|
|
||||||
import { debounce } from "../../../utils/utils"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let api
|
|
||||||
export let readonly
|
|
||||||
export let focused
|
|
||||||
export let schema
|
|
||||||
export let onChange
|
|
||||||
export let invertX = false
|
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
const { API, dispatch } = getContext("grid")
|
|
||||||
const color = getColor(0)
|
|
||||||
|
|
||||||
let isOpen = false
|
|
||||||
let searchResults
|
|
||||||
let searchString
|
|
||||||
let lastSearchString
|
|
||||||
let primaryDisplay
|
|
||||||
let candidateIndex
|
|
||||||
let lastSearchId
|
|
||||||
let searching = false
|
|
||||||
|
|
||||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
$: lookupMap = buildLookupMap(value, isOpen)
|
|
||||||
$: debouncedSearch(searchString)
|
|
||||||
$: {
|
|
||||||
if (!focused) {
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds a lookup map to quickly check which rows are selected
|
|
||||||
const buildLookupMap = (value, isOpen) => {
|
|
||||||
let map = {}
|
|
||||||
if (!isOpen || !value?.length) {
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
|
||||||
map[value[i]._id] = true
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if a certain row is currently selected
|
|
||||||
const isRowSelected = row => {
|
|
||||||
if (!row?._id) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return lookupMap?.[row._id] === true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for rows based on the search string
|
|
||||||
const search = async (searchString, force = false) => {
|
|
||||||
// Avoid update state at all if we've already handled the update and this is
|
|
||||||
// a wasted search due to svelte reactivity
|
|
||||||
if (!force && !searchString && !lastSearchString) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state if this search is invalid
|
|
||||||
if (!schema?.tableId || !isOpen) {
|
|
||||||
lastSearchString = null
|
|
||||||
candidateIndex = null
|
|
||||||
searchResults = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for results, using IDs to track invocations and ensure we're
|
|
||||||
// handling the latest update
|
|
||||||
lastSearchId = Math.random()
|
|
||||||
searching = true
|
|
||||||
const thisSearchId = lastSearchId
|
|
||||||
const results = await API.searchTable({
|
|
||||||
paginate: false,
|
|
||||||
tableId: schema.tableId,
|
|
||||||
limit: 20,
|
|
||||||
query: {
|
|
||||||
string: {
|
|
||||||
[`1:${primaryDisplay}`]: searchString || "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
searching = false
|
|
||||||
|
|
||||||
// In case searching takes longer than our debounced update, abandon these
|
|
||||||
// results
|
|
||||||
if (thisSearchId !== lastSearchId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort and process results
|
|
||||||
searchResults = sortRows(
|
|
||||||
results.rows?.map(row => ({
|
|
||||||
...row,
|
|
||||||
primaryDisplay: row[primaryDisplay],
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
candidateIndex = searchResults?.length ? 0 : null
|
|
||||||
lastSearchString = searchString
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounced version of searching
|
|
||||||
const debouncedSearch = debounce(search, 250)
|
|
||||||
|
|
||||||
// Alphabetically sorts rows by their primary display column
|
|
||||||
const sortRows = rows => {
|
|
||||||
if (!rows?.length) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return rows.slice().sort((a, b) => {
|
|
||||||
return a.primaryDisplay < b.primaryDisplay ? -1 : 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = async () => {
|
|
||||||
isOpen = true
|
|
||||||
|
|
||||||
// Find the primary display for the related table
|
|
||||||
if (!primaryDisplay) {
|
|
||||||
searching = true
|
|
||||||
primaryDisplay = await getPrimaryDisplayForTableId(API, schema.tableId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show initial list of results
|
|
||||||
await search(null, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
isOpen = false
|
|
||||||
searchResults = []
|
|
||||||
searchString = null
|
|
||||||
lastSearchString = null
|
|
||||||
candidateIndex = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = e => {
|
|
||||||
if (!isOpen) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (e.key === "ArrowDown") {
|
|
||||||
// Select next result on down arrow
|
|
||||||
e.preventDefault()
|
|
||||||
if (candidateIndex == null) {
|
|
||||||
candidateIndex = 0
|
|
||||||
} else {
|
|
||||||
candidateIndex = Math.min(searchResults.length - 1, candidateIndex + 1)
|
|
||||||
}
|
|
||||||
} else if (e.key === "ArrowUp") {
|
|
||||||
// Select previous result on up array
|
|
||||||
e.preventDefault()
|
|
||||||
if (candidateIndex === 0) {
|
|
||||||
candidateIndex = null
|
|
||||||
} else if (candidateIndex != null) {
|
|
||||||
candidateIndex = Math.max(0, candidateIndex - 1)
|
|
||||||
}
|
|
||||||
} else if (e.key === "Enter") {
|
|
||||||
// Toggle the highlighted result on enter press
|
|
||||||
if (candidateIndex != null && searchResults[candidateIndex] != null) {
|
|
||||||
toggleRow(searchResults[candidateIndex])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggles whether a row is included in the relationship or not
|
|
||||||
const toggleRow = async row => {
|
|
||||||
if (value?.some(x => x._id === row._id)) {
|
|
||||||
// If the row is already included, remove it and update the candidate
|
|
||||||
// row to be the the same position if possible
|
|
||||||
if (oneRowOnly) {
|
|
||||||
await onChange([])
|
|
||||||
} else {
|
|
||||||
const newValue = value.filter(x => x._id !== row._id)
|
|
||||||
if (!newValue.length) {
|
|
||||||
candidateIndex = null
|
|
||||||
} else {
|
|
||||||
candidateIndex = Math.min(candidateIndex, newValue.length - 1)
|
|
||||||
}
|
|
||||||
await onChange(newValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If we don't have this row, include it
|
|
||||||
if (oneRowOnly) {
|
|
||||||
await onChange([row])
|
|
||||||
} else {
|
|
||||||
await onChange(sortRows([...(value || []), row]))
|
|
||||||
}
|
|
||||||
candidateIndex = null
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showRelationship = async id => {
|
|
||||||
const relatedRow = await API.fetchRow({
|
|
||||||
tableId: schema.tableId,
|
|
||||||
rowId: id,
|
|
||||||
})
|
|
||||||
dispatch("edit-row", relatedRow)
|
|
||||||
}
|
|
||||||
|
|
||||||
const readable = value => {
|
|
||||||
if (value == null) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if (value instanceof Object) {
|
|
||||||
return JSON.stringify(value)
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
api = {
|
|
||||||
focus: open,
|
|
||||||
blur: close,
|
|
||||||
onKeyDown,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="wrapper" class:editable class:focused style="--color:{color};">
|
|
||||||
<div class="container">
|
|
||||||
<div class="values" on:wheel={e => (focused ? e.stopPropagation() : null)}>
|
|
||||||
{#each value || [] as relationship, idx}
|
|
||||||
{#if relationship.primaryDisplay}
|
|
||||||
<div class="badge">
|
|
||||||
<span
|
|
||||||
on:click={editable
|
|
||||||
? () => showRelationship(relationship._id)
|
|
||||||
: null}
|
|
||||||
>
|
|
||||||
{readable(relationship.primaryDisplay)}
|
|
||||||
</span>
|
|
||||||
{#if editable}
|
|
||||||
<Icon
|
|
||||||
name="Close"
|
|
||||||
size="XS"
|
|
||||||
hoverable
|
|
||||||
on:click={() => toggleRow(relationship)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{#if editable}
|
|
||||||
<div class="add" on:click={open}>
|
|
||||||
<Icon name="Add" size="S" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if value?.length}
|
|
||||||
<div class="count">
|
|
||||||
{value?.length || 0}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if isOpen}
|
|
||||||
<div class="dropdown" class:invertX class:invertY on:wheel|stopPropagation>
|
|
||||||
<div class="search">
|
|
||||||
<Input
|
|
||||||
autofocus
|
|
||||||
quiet
|
|
||||||
type="text"
|
|
||||||
bind:value={searchString}
|
|
||||||
placeholder={primaryDisplay ? `Search by ${primaryDisplay}` : null}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{#if searching}
|
|
||||||
<div class="searching">
|
|
||||||
<ProgressCircle size="S" />
|
|
||||||
</div>
|
|
||||||
{:else if searchResults?.length}
|
|
||||||
<div class="results">
|
|
||||||
{#each searchResults as row, idx}
|
|
||||||
<div
|
|
||||||
class="result"
|
|
||||||
on:click={() => toggleRow(row)}
|
|
||||||
class:candidate={idx === candidateIndex}
|
|
||||||
on:mouseenter={() => (candidateIndex = idx)}
|
|
||||||
>
|
|
||||||
<div class="badge">
|
|
||||||
<span>
|
|
||||||
{readable(row.primaryDisplay)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{#if isRowSelected(row)}
|
|
||||||
<Icon
|
|
||||||
size="S"
|
|
||||||
name="Checkmark"
|
|
||||||
color="var(--spectrum-global-color-blue-400)"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.wrapper {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
align-self: flex-start;
|
|
||||||
min-height: var(--row-height);
|
|
||||||
max-height: var(--row-height);
|
|
||||||
overflow: hidden;
|
|
||||||
--max-relationship-height: 120px;
|
|
||||||
}
|
|
||||||
.wrapper.focused {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--cell-background);
|
|
||||||
z-index: 1;
|
|
||||||
max-height: none;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
min-height: var(--row-height);
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.focused .container {
|
|
||||||
overflow-y: auto;
|
|
||||||
border-radius: 2px;
|
|
||||||
max-height: var(--max-relationship-height);
|
|
||||||
}
|
|
||||||
.focused .container:after {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
border: 2px solid var(--cell-color);
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.values {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
grid-column-gap: var(--cell-spacing);
|
|
||||||
grid-row-gap: var(--cell-padding);
|
|
||||||
overflow: hidden;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.count {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
color: var(--spectrum-global-color-gray-500);
|
|
||||||
padding: var(--cell-padding) var(--cell-padding) var(--cell-padding) 20px;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
transparent 0%,
|
|
||||||
var(--cell-background) 40%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.wrapper:hover:not(.focused) .count {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
padding: 0 var(--cell-padding);
|
|
||||||
background: var(--color);
|
|
||||||
border-radius: var(--cell-padding);
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
height: 20px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
.badge span {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.editable .values .badge span:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add {
|
|
||||||
background: var(--spectrum-global-color-gray-200);
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.add :global(.spectrum-Icon) {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
.add:hover {
|
|
||||||
background: var(--spectrum-global-color-gray-300);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
max-height: calc(
|
|
||||||
var(--max-cell-render-height) + var(--row-height) -
|
|
||||||
var(--max-relationship-height)
|
|
||||||
);
|
|
||||||
background: var(--background);
|
|
||||||
border: var(--cell-border);
|
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
padding: 0 0 8px 0;
|
|
||||||
}
|
|
||||||
.dropdown.invertY {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
.dropdown.invertX {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searching {
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.results {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.result {
|
|
||||||
padding: 0 var(--cell-padding);
|
|
||||||
flex: 0 0 var(--default-row-height);
|
|
||||||
display: flex;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.result.candidate {
|
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.result .badge {
|
|
||||||
max-width: calc(100% - 30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
|
||||||
flex: 0 0 calc(var(--default-row-height) - 1px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 4px var(--cell-padding);
|
|
||||||
width: calc(100% - 2 * var(--cell-padding));
|
|
||||||
}
|
|
||||||
.search :global(.spectrum-Textfield) {
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.search :global(.spectrum-Textfield-input) {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.search :global(.spectrum-Form-item) {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,110 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let focused = false
|
|
||||||
export let onChange
|
|
||||||
export let type = "text"
|
|
||||||
export let readonly = false
|
|
||||||
export let api
|
|
||||||
|
|
||||||
let input
|
|
||||||
let active = false
|
|
||||||
|
|
||||||
$: editable = focused && !readonly
|
|
||||||
|
|
||||||
const handleChange = e => {
|
|
||||||
onChange(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyDown = e => {
|
|
||||||
if (!active) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
input?.blur()
|
|
||||||
const event = new KeyboardEvent("keydown", { key: "ArrowDown" })
|
|
||||||
document.dispatchEvent(event)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
api = {
|
|
||||||
focus: () => input?.focus(),
|
|
||||||
blur: () => input?.blur(),
|
|
||||||
onKeyDown,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if editable}
|
|
||||||
<input
|
|
||||||
bind:this={input}
|
|
||||||
on:focus={() => (active = true)}
|
|
||||||
on:blur={() => (active = false)}
|
|
||||||
{type}
|
|
||||||
value={value || ""}
|
|
||||||
on:change={handleChange}
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="text-cell" class:number={type === "number"}>
|
|
||||||
<div class="value">
|
|
||||||
{value || ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.text-cell {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
align-self: stretch;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.text-cell.number {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.value {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: var(--content-lines);
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.number .value {
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
border: none;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: none;
|
|
||||||
font-size: var(--cell-font-size);
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
color: inherit;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
input[type="number"] {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide arrows for number fields */
|
|
||||||
input::-webkit-outer-spin-button,
|
|
||||||
input::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
input[type="number"] {
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ActionButton } from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { config, dispatch } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
icon="TableColumnAddRight"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => dispatch("add-column")}
|
|
||||||
disabled={!$config.allowAddColumns}
|
|
||||||
>
|
|
||||||
Create column
|
|
||||||
</ActionButton>
|
|
|
@ -1,18 +0,0 @@
|
||||||
<script>
|
|
||||||
import { ActionButton } from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { dispatch, columns, stickyColumn, config, loaded } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
icon="TableRowAddBottom"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => dispatch("add-row")}
|
|
||||||
disabled={!loaded ||
|
|
||||||
!$config.allowAddRows ||
|
|
||||||
(!$columns.length && !$stickyColumn)}
|
|
||||||
>
|
|
||||||
Create row
|
|
||||||
</ActionButton>
|
|
|
@ -1,46 +0,0 @@
|
||||||
<script>
|
|
||||||
import { Button } from "@budibase/bbui"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="beta-background" />
|
|
||||||
<div class="beta">
|
|
||||||
Enjoying the Grid?
|
|
||||||
<Button
|
|
||||||
size="M"
|
|
||||||
cta
|
|
||||||
on:click={() => window.open("https://t.maze.co/156382627", "_blank")}
|
|
||||||
>
|
|
||||||
Give Feedback
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.beta {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 32px;
|
|
||||||
right: 32px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
.beta :global(.spectrum-Button) {
|
|
||||||
background: var(--spectrum-global-color-magenta-400);
|
|
||||||
border-color: var(--spectrum-global-color-magenta-400);
|
|
||||||
}
|
|
||||||
.beta-background {
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
bottom: -230px;
|
|
||||||
right: -105px;
|
|
||||||
width: 1400px;
|
|
||||||
height: 320px;
|
|
||||||
transform: rotate(-22deg);
|
|
||||||
background: linear-gradient(
|
|
||||||
to top,
|
|
||||||
var(--cell-background) 20%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,72 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ActionButton,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { selectedRows, rows, config } = getContext("grid")
|
|
||||||
|
|
||||||
let modal
|
|
||||||
|
|
||||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
|
||||||
$: rowsToDelete = Object.entries($selectedRows)
|
|
||||||
.map(entry => {
|
|
||||||
if (entry[1] === true) {
|
|
||||||
return $rows.find(x => x._id === entry[0])
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(x => x != null)
|
|
||||||
|
|
||||||
// Deletion callback when confirmed
|
|
||||||
const performDeletion = async () => {
|
|
||||||
const count = rowsToDelete.length
|
|
||||||
await rows.actions.deleteRows(rowsToDelete)
|
|
||||||
notifications.success(`Deleted ${count} row${count === 1 ? "" : "s"}`)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if selectedRowCount}
|
|
||||||
<div class="delete-button" data-ignore-click-outside="true">
|
|
||||||
<ActionButton
|
|
||||||
icon="Delete"
|
|
||||||
size="S"
|
|
||||||
on:click={modal.show}
|
|
||||||
disabled={!$config.allowEditRows}
|
|
||||||
>
|
|
||||||
Delete {selectedRowCount} row{selectedRowCount === 1 ? "" : "s"}
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
|
||||||
<ModalContent
|
|
||||||
title="Delete rows"
|
|
||||||
confirmText="Continue"
|
|
||||||
cancelText="Cancel"
|
|
||||||
onConfirm={performDeletion}
|
|
||||||
size="M"
|
|
||||||
>
|
|
||||||
Are you sure you want to delete {selectedRowCount}
|
|
||||||
row{selectedRowCount === 1 ? "" : "s"}?
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.delete-button :global(.spectrum-ActionButton:not(:disabled) *) {
|
|
||||||
color: var(--spectrum-global-color-red-400);
|
|
||||||
}
|
|
||||||
.delete-button :global(.spectrum-ActionButton:not(:disabled)) {
|
|
||||||
border-color: var(--spectrum-global-color-red-400);
|
|
||||||
}
|
|
||||||
/*.delete-button.disabled :global(.spectrum-ActionButton *) {*/
|
|
||||||
/* color: var(--spectrum-global-color-gray-600);*/
|
|
||||||
/*}*/
|
|
||||||
/*.delete-button.disabled :global(.spectrum-ActionButton) {*/
|
|
||||||
/* border-color: var(--spectrum-global-color-gray-400);*/
|
|
||||||
/*}*/
|
|
||||||
</style>
|
|
|
@ -1,91 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { ActionButton, Popover, Toggle } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const { columns } = getContext("grid")
|
|
||||||
|
|
||||||
let open = false
|
|
||||||
let anchor
|
|
||||||
|
|
||||||
$: anyHidden = $columns.some(col => !col.visible)
|
|
||||||
|
|
||||||
const toggleVisibility = (column, visible) => {
|
|
||||||
columns.update(state => {
|
|
||||||
const index = state.findIndex(col => col.name === column.name)
|
|
||||||
state[index].visible = visible
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showAll = () => {
|
|
||||||
columns.update(state => {
|
|
||||||
return state.map(col => ({
|
|
||||||
...col,
|
|
||||||
visible: true,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideAll = () => {
|
|
||||||
columns.update(state => {
|
|
||||||
return state.map(col => ({
|
|
||||||
...col,
|
|
||||||
visible: false,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<ActionButton
|
|
||||||
icon="VisibilityOff"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => (open = !open)}
|
|
||||||
selected={open || anyHidden}
|
|
||||||
disabled={!$columns.length}
|
|
||||||
>
|
|
||||||
Hide columns
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
|
||||||
<div class="content">
|
|
||||||
<div class="columns">
|
|
||||||
{#each $columns as column}
|
|
||||||
<Toggle
|
|
||||||
size="S"
|
|
||||||
value={column.visible}
|
|
||||||
on:change={e => toggleVisibility(column, e.detail)}
|
|
||||||
/>
|
|
||||||
<span>{column.name}</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="buttons">
|
|
||||||
<ActionButton on:click={showAll}>Show all</ActionButton>
|
|
||||||
<ActionButton on:click={hideAll}>Hide all</ActionButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 12px 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.columns {
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { ActionButton, Popover } from "@budibase/bbui"
|
|
||||||
import {
|
|
||||||
LargeRowHeight,
|
|
||||||
MediumRowHeight,
|
|
||||||
SmallRowHeight,
|
|
||||||
} from "../lib/constants"
|
|
||||||
|
|
||||||
const { rowHeight, columns, table } = getContext("grid")
|
|
||||||
const sizeOptions = [
|
|
||||||
{
|
|
||||||
label: "Small",
|
|
||||||
size: SmallRowHeight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Medium",
|
|
||||||
size: MediumRowHeight,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Large",
|
|
||||||
size: LargeRowHeight,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let open = false
|
|
||||||
let anchor
|
|
||||||
|
|
||||||
const changeRowHeight = height => {
|
|
||||||
columns.actions.saveTable({
|
|
||||||
...$table,
|
|
||||||
rowHeight: height,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<ActionButton
|
|
||||||
icon="LineHeight"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => (open = !open)}
|
|
||||||
selected={open}
|
|
||||||
>
|
|
||||||
Row height
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
|
||||||
<div class="content">
|
|
||||||
{#each sizeOptions as option}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
selected={$rowHeight === option.size}
|
|
||||||
on:click={() => changeRowHeight(option.size)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</ActionButton>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,107 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
|
||||||
|
|
||||||
const { sort, visibleColumns, stickyColumn } = getContext("grid")
|
|
||||||
const orderOptions = [
|
|
||||||
{ label: "A-Z", value: "ascending" },
|
|
||||||
{ label: "Z-A", value: "descending" },
|
|
||||||
]
|
|
||||||
|
|
||||||
let open = false
|
|
||||||
let anchor
|
|
||||||
|
|
||||||
$: columnOptions = getColumnOptions($stickyColumn, $visibleColumns)
|
|
||||||
$: checkValidSortColumn($sort.column, $stickyColumn, $visibleColumns)
|
|
||||||
|
|
||||||
const getColumnOptions = (stickyColumn, columns) => {
|
|
||||||
let options = []
|
|
||||||
if (stickyColumn) {
|
|
||||||
options.push(stickyColumn.name)
|
|
||||||
}
|
|
||||||
return [...options, ...columns.map(col => col.name)]
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSortColumn = e => {
|
|
||||||
sort.update(state => ({
|
|
||||||
...state,
|
|
||||||
column: e.detail,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSortOrder = e => {
|
|
||||||
sort.update(state => ({
|
|
||||||
...state,
|
|
||||||
order: e.detail,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we never have a sort column selected that is not visible
|
|
||||||
const checkValidSortColumn = (sortColumn, stickyColumn, visibleColumns) => {
|
|
||||||
if (!sortColumn) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
sortColumn !== stickyColumn?.name &&
|
|
||||||
!visibleColumns.some(col => col.name === sortColumn)
|
|
||||||
) {
|
|
||||||
if (stickyColumn) {
|
|
||||||
sort.update(state => ({
|
|
||||||
...state,
|
|
||||||
column: stickyColumn.name,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
sort.update(state => ({
|
|
||||||
...state,
|
|
||||||
column: visibleColumns[0]?.name,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
|
||||||
<ActionButton
|
|
||||||
icon="SortOrderDown"
|
|
||||||
quiet
|
|
||||||
size="M"
|
|
||||||
on:click={() => (open = !open)}
|
|
||||||
selected={open || $sort.column}
|
|
||||||
disabled={!columnOptions.length}
|
|
||||||
>
|
|
||||||
Sort
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Popover bind:open {anchor} align="left">
|
|
||||||
<div class="content">
|
|
||||||
<Select
|
|
||||||
placeholder={null}
|
|
||||||
value={$sort.column}
|
|
||||||
options={columnOptions}
|
|
||||||
autoWidth
|
|
||||||
on:change={updateSortColumn}
|
|
||||||
label="Column"
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
placeholder={null}
|
|
||||||
value={$sort.order}
|
|
||||||
options={orderOptions}
|
|
||||||
autoWidth
|
|
||||||
on:change={updateSortOrder}
|
|
||||||
label="Order"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
padding: 6px 12px 12px 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
.content :global(.spectrum-Picker) {
|
|
||||||
width: 140px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1 +0,0 @@
|
||||||
export { default as Grid } from "./layout/Grid.svelte"
|
|
|
@ -1,24 +0,0 @@
|
||||||
<script>
|
|
||||||
export let user
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="user" style="background:{user.color};" title={user.email}>
|
|
||||||
{user.email[0]}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
div:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,250 +0,0 @@
|
||||||
<script>
|
|
||||||
import { setContext } from "svelte"
|
|
||||||
import { writable } from "svelte/store"
|
|
||||||
import { fade } from "svelte/transition"
|
|
||||||
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
|
||||||
import { createEventManagers } from "../lib/events"
|
|
||||||
import { createAPIClient } from "../../../api"
|
|
||||||
import { attachStores } from "../stores"
|
|
||||||
import DeleteButton from "../controls/DeleteButton.svelte"
|
|
||||||
import BetaButton from "../controls/BetaButton.svelte"
|
|
||||||
import GridBody from "./GridBody.svelte"
|
|
||||||
import ResizeOverlay from "../overlays/ResizeOverlay.svelte"
|
|
||||||
import ReorderOverlay from "../overlays/ReorderOverlay.svelte"
|
|
||||||
import HeaderRow from "./HeaderRow.svelte"
|
|
||||||
import ScrollOverlay from "../overlays/ScrollOverlay.svelte"
|
|
||||||
import MenuOverlay from "../overlays/MenuOverlay.svelte"
|
|
||||||
import StickyColumn from "./StickyColumn.svelte"
|
|
||||||
import UserAvatars from "./UserAvatars.svelte"
|
|
||||||
import KeyboardManager from "../overlays/KeyboardManager.svelte"
|
|
||||||
import SortButton from "../controls/SortButton.svelte"
|
|
||||||
import AddColumnButton from "../controls/AddColumnButton.svelte"
|
|
||||||
import HideColumnsButton from "../controls/HideColumnsButton.svelte"
|
|
||||||
import AddRowButton from "../controls/AddRowButton.svelte"
|
|
||||||
import RowHeightButton from "../controls/RowHeightButton.svelte"
|
|
||||||
import {
|
|
||||||
MaxCellRenderHeight,
|
|
||||||
MaxCellRenderWidthOverflow,
|
|
||||||
GutterWidth,
|
|
||||||
DefaultRowHeight,
|
|
||||||
} from "../lib/constants"
|
|
||||||
|
|
||||||
export let API = null
|
|
||||||
export let tableId = null
|
|
||||||
export let schemaOverrides = null
|
|
||||||
export let allowAddRows = true
|
|
||||||
export let allowAddColumns = true
|
|
||||||
export let allowEditColumns = true
|
|
||||||
export let allowExpandRows = true
|
|
||||||
export let allowEditRows = true
|
|
||||||
export let allowDeleteRows = true
|
|
||||||
|
|
||||||
// Unique identifier for DOM nodes inside this instance
|
|
||||||
const rand = Math.random()
|
|
||||||
|
|
||||||
// State stores
|
|
||||||
const tableIdStore = writable(tableId)
|
|
||||||
const schemaOverridesStore = writable(schemaOverrides)
|
|
||||||
const config = writable({
|
|
||||||
allowAddRows,
|
|
||||||
allowAddColumns,
|
|
||||||
allowEditColumns,
|
|
||||||
allowExpandRows,
|
|
||||||
allowEditRows,
|
|
||||||
allowDeleteRows,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Build up context
|
|
||||||
let context = {
|
|
||||||
API: API || createAPIClient(),
|
|
||||||
rand,
|
|
||||||
config,
|
|
||||||
tableId: tableIdStore,
|
|
||||||
schemaOverrides: schemaOverridesStore,
|
|
||||||
}
|
|
||||||
context = { ...context, ...createEventManagers() }
|
|
||||||
context = attachStores(context)
|
|
||||||
|
|
||||||
// Reference some stores for local use
|
|
||||||
const {
|
|
||||||
isResizing,
|
|
||||||
isReordering,
|
|
||||||
ui,
|
|
||||||
loaded,
|
|
||||||
loading,
|
|
||||||
rowHeight,
|
|
||||||
contentLines,
|
|
||||||
} = context
|
|
||||||
|
|
||||||
// Keep stores up to date
|
|
||||||
$: tableIdStore.set(tableId)
|
|
||||||
$: schemaOverridesStore.set(schemaOverrides)
|
|
||||||
$: config.set({
|
|
||||||
allowAddRows,
|
|
||||||
allowAddColumns,
|
|
||||||
allowEditColumns,
|
|
||||||
allowExpandRows,
|
|
||||||
allowEditRows,
|
|
||||||
allowDeleteRows,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set context for children to consume
|
|
||||||
setContext("grid", context)
|
|
||||||
|
|
||||||
// Expose ability to retrieve context externally for external control
|
|
||||||
export const getContext = () => context
|
|
||||||
|
|
||||||
// Initialise websocket for multi-user
|
|
||||||
// onMount(() => createWebsocket(context))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="grid"
|
|
||||||
id="grid-{rand}"
|
|
||||||
class:is-resizing={$isResizing}
|
|
||||||
class:is-reordering={$isReordering}
|
|
||||||
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
|
|
||||||
>
|
|
||||||
<div class="controls">
|
|
||||||
<div class="controls-left">
|
|
||||||
<AddRowButton />
|
|
||||||
<AddColumnButton />
|
|
||||||
<slot name="controls" />
|
|
||||||
<RowHeightButton />
|
|
||||||
<HideColumnsButton />
|
|
||||||
<SortButton />
|
|
||||||
</div>
|
|
||||||
<div class="controls-right">
|
|
||||||
<DeleteButton />
|
|
||||||
<UserAvatars />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if $loaded}
|
|
||||||
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
|
|
||||||
<div class="grid-data-inner">
|
|
||||||
<StickyColumn />
|
|
||||||
<div class="grid-data-content">
|
|
||||||
<HeaderRow />
|
|
||||||
<GridBody />
|
|
||||||
</div>
|
|
||||||
<div class="overlays">
|
|
||||||
<ResizeOverlay />
|
|
||||||
<ReorderOverlay />
|
|
||||||
<BetaButton />
|
|
||||||
<ScrollOverlay />
|
|
||||||
<MenuOverlay />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if $loading}
|
|
||||||
<div in:fade|local={{ duration: 130 }} class="grid-loading">
|
|
||||||
<ProgressCircle />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<KeyboardManager />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.grid {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--cell-background);
|
|
||||||
|
|
||||||
/* Variables */
|
|
||||||
--cell-background: var(--spectrum-global-color-gray-50);
|
|
||||||
--cell-background-hover: var(--spectrum-global-color-gray-100);
|
|
||||||
--cell-padding: 8px;
|
|
||||||
--cell-spacing: 4px;
|
|
||||||
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
|
||||||
--cell-font-size: 14px;
|
|
||||||
--controls-height: 50px;
|
|
||||||
}
|
|
||||||
.grid,
|
|
||||||
.grid :global(*) {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.grid.is-resizing :global(*) {
|
|
||||||
cursor: col-resize !important;
|
|
||||||
}
|
|
||||||
.grid.is-reordering :global(*) {
|
|
||||||
cursor: grabbing !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-data-outer,
|
|
||||||
.grid-data-inner {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
justify-items: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.grid-data-outer {
|
|
||||||
height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.grid-data-inner {
|
|
||||||
flex-direction: row;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.grid-data-content {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Controls */
|
|
||||||
.controls {
|
|
||||||
height: var(--controls-height);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
background: var(--background);
|
|
||||||
}
|
|
||||||
.controls-left,
|
|
||||||
.controls-right {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
}
|
|
||||||
.controls-right {
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overlays */
|
|
||||||
.overlays {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading */
|
|
||||||
.grid-loading {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.grid-loading:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--background);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,38 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
||||||
import GridRow from "./GridRow.svelte"
|
|
||||||
|
|
||||||
const { bounds, renderedRows, rowVerticalInversionIndex } = getContext("grid")
|
|
||||||
|
|
||||||
let body
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
// Observe and record the height of the body
|
|
||||||
const observer = new ResizeObserver(() => {
|
|
||||||
bounds.set(body.getBoundingClientRect())
|
|
||||||
})
|
|
||||||
observer.observe(body)
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={body} class="grid-body">
|
|
||||||
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
|
|
||||||
{#each $renderedRows as row, idx}
|
|
||||||
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} />
|
|
||||||
{/each}
|
|
||||||
</GridScrollWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.grid-body {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
cursor: default;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,57 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
|
||||||
|
|
||||||
export let row
|
|
||||||
export let idx
|
|
||||||
export let invertY = false
|
|
||||||
|
|
||||||
const {
|
|
||||||
focusedCellId,
|
|
||||||
reorder,
|
|
||||||
selectedRows,
|
|
||||||
renderedColumns,
|
|
||||||
hoveredRowId,
|
|
||||||
selectedCellMap,
|
|
||||||
focusedRow,
|
|
||||||
columnHorizontalInversionIndex,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
$: rowSelected = !!$selectedRows[row._id]
|
|
||||||
$: rowHovered = $hoveredRowId === row._id
|
|
||||||
$: rowFocused = $focusedRow?._id === row._id
|
|
||||||
$: reorderSource = $reorder.sourceColumn
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
on:focus
|
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
|
||||||
>
|
|
||||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
|
||||||
{@const cellId = `${row._id}-${column.name}`}
|
|
||||||
<DataCell
|
|
||||||
{cellId}
|
|
||||||
{column}
|
|
||||||
{row}
|
|
||||||
{invertY}
|
|
||||||
{rowFocused}
|
|
||||||
invertX={columnIdx >= $columnHorizontalInversionIndex}
|
|
||||||
highlighted={rowHovered || rowFocused || reorderSource === column.name}
|
|
||||||
selected={rowSelected}
|
|
||||||
rowIdx={idx}
|
|
||||||
focused={$focusedCellId === cellId}
|
|
||||||
selectedUser={$selectedCellMap[cellId]}
|
|
||||||
width={column.width}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.row {
|
|
||||||
width: 0;
|
|
||||||
display: flex;
|
|
||||||
height: var(--row-height);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,84 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { domDebounce } from "../../../utils/utils"
|
|
||||||
|
|
||||||
const {
|
|
||||||
rowHeight,
|
|
||||||
scroll,
|
|
||||||
focusedCellId,
|
|
||||||
renderedRows,
|
|
||||||
maxScrollTop,
|
|
||||||
maxScrollLeft,
|
|
||||||
bounds,
|
|
||||||
hoveredRowId,
|
|
||||||
hiddenColumnsWidth,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
export let scrollVertically = false
|
|
||||||
export let scrollHorizontally = false
|
|
||||||
export let wheelInteractive = false
|
|
||||||
|
|
||||||
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
|
||||||
|
|
||||||
const generateStyle = (scroll, rowHeight, hiddenWidths) => {
|
|
||||||
const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0
|
|
||||||
const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
|
|
||||||
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handles a wheel even and updates the scroll offsets
|
|
||||||
const handleWheel = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
const modifier = e.ctrlKey || e.metaKey
|
|
||||||
let x = modifier ? e.deltaY : e.deltaX
|
|
||||||
let y = modifier ? e.deltaX : e.deltaY
|
|
||||||
debouncedHandleWheel(x, y, e.clientY)
|
|
||||||
}
|
|
||||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
|
||||||
const { top, left } = $scroll
|
|
||||||
|
|
||||||
// Calculate new scroll top
|
|
||||||
let newScrollTop = top + deltaY
|
|
||||||
newScrollTop = Math.max(0, Math.min(newScrollTop, $maxScrollTop))
|
|
||||||
|
|
||||||
// Calculate new scroll left
|
|
||||||
let newScrollLeft = left + deltaX
|
|
||||||
newScrollLeft = Math.max(0, Math.min(newScrollLeft, $maxScrollLeft))
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
scroll.set({
|
|
||||||
left: scrollHorizontally ? newScrollLeft : left,
|
|
||||||
top: scrollVertically ? newScrollTop : top,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Hover row under cursor
|
|
||||||
const y = clientY - $bounds.top + (newScrollTop % $rowHeight)
|
|
||||||
const hoveredRow = $renderedRows[Math.floor(y / $rowHeight)]
|
|
||||||
hoveredRowId.set(hoveredRow?._id)
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="outer"
|
|
||||||
on:wheel={wheelInteractive ? handleWheel : null}
|
|
||||||
on:click|self={() => ($focusedCellId = null)}
|
|
||||||
>
|
|
||||||
<div {style} class="inner">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.outer {
|
|
||||||
min-width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.inner {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
||||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
|
||||||
|
|
||||||
const { renderedColumns } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="header">
|
|
||||||
<GridScrollWrapper scrollHorizontally>
|
|
||||||
<div class="row">
|
|
||||||
{#each $renderedColumns as column, idx}
|
|
||||||
<HeaderCell {column} {idx} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</GridScrollWrapper>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.header {
|
|
||||||
background: var(--background);
|
|
||||||
border-bottom: var(--cell-border);
|
|
||||||
position: relative;
|
|
||||||
height: var(--default-row-height);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,247 +0,0 @@
|
||||||
<script>
|
|
||||||
import GridCell from "../cells/GridCell.svelte"
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { Icon, Button } from "@budibase/bbui"
|
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
|
||||||
import { fly } from "svelte/transition"
|
|
||||||
import { GutterWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
const {
|
|
||||||
hoveredRowId,
|
|
||||||
focusedCellId,
|
|
||||||
stickyColumn,
|
|
||||||
scroll,
|
|
||||||
config,
|
|
||||||
dispatch,
|
|
||||||
visibleColumns,
|
|
||||||
rows,
|
|
||||||
showHScrollbar,
|
|
||||||
tableId,
|
|
||||||
subscribe,
|
|
||||||
scrollLeft,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
let isAdding = false
|
|
||||||
let newRow = {}
|
|
||||||
let touched = false
|
|
||||||
|
|
||||||
$: firstColumn = $stickyColumn || $visibleColumns[0]
|
|
||||||
$: rowHovered = $hoveredRowId === "new"
|
|
||||||
$: rowFocused = $focusedCellId?.startsWith("new-")
|
|
||||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
|
||||||
$: $tableId, (isAdding = false)
|
|
||||||
|
|
||||||
const addRow = async () => {
|
|
||||||
// Create row
|
|
||||||
const savedRow = await rows.actions.addRow(newRow, 0)
|
|
||||||
if (savedRow) {
|
|
||||||
// Select the first cell if possible
|
|
||||||
if (firstColumn) {
|
|
||||||
$focusedCellId = `${savedRow._id}-${firstColumn.name}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
isAdding = false
|
|
||||||
scroll.set({
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
isAdding = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const startAdding = () => {
|
|
||||||
newRow = {}
|
|
||||||
isAdding = true
|
|
||||||
if (firstColumn) {
|
|
||||||
$focusedCellId = `new-${firstColumn.name}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateValue = (rowId, columnName, val) => {
|
|
||||||
touched = true
|
|
||||||
newRow[columnName] = val
|
|
||||||
}
|
|
||||||
|
|
||||||
const addViaModal = () => {
|
|
||||||
isAdding = false
|
|
||||||
dispatch("add-row")
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => subscribe("add-row-inline", startAdding))
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Only show new row functionality if we have any columns -->
|
|
||||||
{#if isAdding}
|
|
||||||
<div class="container" transition:fly={{ y: 20, duration: 130 }}>
|
|
||||||
<div class="content" class:above-scrollbar={$showHScrollbar}>
|
|
||||||
<div
|
|
||||||
class="new-row"
|
|
||||||
on:mouseenter={() => ($hoveredRowId = "new")}
|
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="sticky-column"
|
|
||||||
style="flex: 0 0 {width}px"
|
|
||||||
class:scrolled={$scrollLeft > 0}
|
|
||||||
>
|
|
||||||
<GridCell width={GutterWidth} {rowHovered} {rowFocused}>
|
|
||||||
<div class="gutter">
|
|
||||||
<div class="number">1</div>
|
|
||||||
{#if $config.allowExpandRows}
|
|
||||||
<Icon
|
|
||||||
name="Maximize"
|
|
||||||
size="S"
|
|
||||||
hoverable
|
|
||||||
on:click={addViaModal}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</GridCell>
|
|
||||||
{#if $stickyColumn}
|
|
||||||
{@const cellId = `new-${$stickyColumn.name}`}
|
|
||||||
<DataCell
|
|
||||||
{cellId}
|
|
||||||
column={$stickyColumn}
|
|
||||||
row={newRow}
|
|
||||||
{rowHovered}
|
|
||||||
focused={$focusedCellId === cellId}
|
|
||||||
{rowFocused}
|
|
||||||
width={$stickyColumn.width}
|
|
||||||
{updateValue}
|
|
||||||
rowIdx={0}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GridScrollWrapper scrollHorizontally wheelInteractive>
|
|
||||||
<div class="row">
|
|
||||||
{#each $visibleColumns as column}
|
|
||||||
{@const cellId = `new-${column.name}`}
|
|
||||||
{#key cellId}
|
|
||||||
<DataCell
|
|
||||||
{cellId}
|
|
||||||
{column}
|
|
||||||
row={newRow}
|
|
||||||
{rowHovered}
|
|
||||||
focused={$focusedCellId === cellId}
|
|
||||||
{rowFocused}
|
|
||||||
width={column.width}
|
|
||||||
{updateValue}
|
|
||||||
rowIdx={0}
|
|
||||||
/>
|
|
||||||
{/key}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</GridScrollWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="buttons">
|
|
||||||
<Button size="M" cta on:click={addRow}>Save</Button>
|
|
||||||
<Button size="M" secondary newStyles on:click={cancel}>Cancel</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.container {
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: var(--row-height);
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 800px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.container:before {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--cell-background);
|
|
||||||
opacity: 0.8;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
pointer-events: all;
|
|
||||||
background: var(--background);
|
|
||||||
border-bottom: var(--cell-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-row {
|
|
||||||
display: flex;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
transition: margin-bottom 130ms ease-out;
|
|
||||||
}
|
|
||||||
.new-row :global(.cell) {
|
|
||||||
--cell-background: var(--background) !important;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sticky-column {
|
|
||||||
display: flex;
|
|
||||||
z-index: 1;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
/* Don't show borders between cells in the sticky column */
|
|
||||||
.sticky-column :global(.cell:not(:last-child)) {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
width: 0;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add shadow when scrolled */
|
|
||||||
.sticky-column.scrolled {
|
|
||||||
/*box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);*/
|
|
||||||
}
|
|
||||||
.sticky-column.scrolled:after {
|
|
||||||
content: "";
|
|
||||||
width: 10px;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(to right, rgba(0, 0, 0, 0.05), transparent);
|
|
||||||
left: 100%;
|
|
||||||
top: 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for gutter */
|
|
||||||
.gutter {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating buttons */
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
margin: 24px 0 0 var(--gutter-width);
|
|
||||||
pointer-events: all;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.number {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--spectrum-global-color-gray-500);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const { rows } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{$rows.length} row{$rows.length === 1 ? "" : "s"}
|
|
||||||
</div>
|
|
|
@ -1,245 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { Checkbox, Icon } from "@budibase/bbui"
|
|
||||||
import GridCell from "../cells/GridCell.svelte"
|
|
||||||
import DataCell from "../cells/DataCell.svelte"
|
|
||||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
|
||||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
|
||||||
import { GutterWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
const {
|
|
||||||
rows,
|
|
||||||
selectedRows,
|
|
||||||
stickyColumn,
|
|
||||||
renderedRows,
|
|
||||||
focusedCellId,
|
|
||||||
hoveredRowId,
|
|
||||||
config,
|
|
||||||
selectedCellMap,
|
|
||||||
focusedRow,
|
|
||||||
dispatch,
|
|
||||||
scrollLeft,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
$: rowCount = $rows.length
|
|
||||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
|
||||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
|
||||||
|
|
||||||
const selectAll = () => {
|
|
||||||
const allSelected = selectedRowCount === rowCount
|
|
||||||
if (allSelected) {
|
|
||||||
$selectedRows = {}
|
|
||||||
} else {
|
|
||||||
let allRows = {}
|
|
||||||
$rows.forEach(row => {
|
|
||||||
allRows[row._id] = true
|
|
||||||
})
|
|
||||||
$selectedRows = allRows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectRow = id => {
|
|
||||||
selectedRows.update(state => {
|
|
||||||
let newState = {
|
|
||||||
...state,
|
|
||||||
[id]: !state[id],
|
|
||||||
}
|
|
||||||
if (!newState[id]) {
|
|
||||||
delete newState[id]
|
|
||||||
}
|
|
||||||
return newState
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="sticky-column"
|
|
||||||
style="flex: 0 0 {width}px"
|
|
||||||
class:scrolled={$scrollLeft > 0}
|
|
||||||
>
|
|
||||||
<div class="header row">
|
|
||||||
<GridCell width={GutterWidth} defaultHeight center>
|
|
||||||
<div class="gutter">
|
|
||||||
<div class="checkbox visible">
|
|
||||||
{#if $config.allowDeleteRows}
|
|
||||||
<div on:click={selectAll}>
|
|
||||||
<Checkbox
|
|
||||||
value={rowCount && selectedRowCount === rowCount}
|
|
||||||
disabled={!$renderedRows.length}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if $config.allowExpandRows}
|
|
||||||
<div class="expand">
|
|
||||||
<Icon name="Maximize" size="S" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</GridCell>
|
|
||||||
|
|
||||||
{#if $stickyColumn}
|
|
||||||
<HeaderCell column={$stickyColumn} orderable={false} idx="sticky" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content" on:mouseleave={() => ($hoveredRowId = null)}>
|
|
||||||
<GridScrollWrapper scrollVertically wheelInteractive>
|
|
||||||
{#each $renderedRows as row, idx}
|
|
||||||
{@const rowSelected = !!$selectedRows[row._id]}
|
|
||||||
{@const rowHovered = $hoveredRowId === row._id}
|
|
||||||
{@const rowFocused = $focusedRow?._id === row._id}
|
|
||||||
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
|
||||||
<div
|
|
||||||
class="row"
|
|
||||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
|
||||||
on:mouseleave={() => ($hoveredRowId = null)}
|
|
||||||
>
|
|
||||||
<GridCell
|
|
||||||
width={GutterWidth}
|
|
||||||
highlighted={rowFocused || rowHovered}
|
|
||||||
selected={rowSelected}
|
|
||||||
>
|
|
||||||
<div class="gutter">
|
|
||||||
<div
|
|
||||||
on:click={() => selectRow(row._id)}
|
|
||||||
class="checkbox"
|
|
||||||
class:visible={$config.allowDeleteRows &&
|
|
||||||
(rowSelected || rowHovered || rowFocused)}
|
|
||||||
>
|
|
||||||
<Checkbox value={rowSelected} />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="number"
|
|
||||||
class:visible={!$config.allowDeleteRows ||
|
|
||||||
!(rowSelected || rowHovered || rowFocused)}
|
|
||||||
>
|
|
||||||
{row.__idx + 1}
|
|
||||||
</div>
|
|
||||||
{#if $config.allowExpandRows}
|
|
||||||
<div class="expand" class:visible={rowFocused || rowHovered}>
|
|
||||||
<Icon
|
|
||||||
name="Maximize"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => {
|
|
||||||
dispatch("edit-row", row)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</GridCell>
|
|
||||||
{#if $stickyColumn}
|
|
||||||
<DataCell
|
|
||||||
{row}
|
|
||||||
{cellId}
|
|
||||||
{rowFocused}
|
|
||||||
selected={rowSelected}
|
|
||||||
highlighted={rowHovered || rowFocused}
|
|
||||||
rowIdx={idx}
|
|
||||||
focused={$focusedCellId === cellId}
|
|
||||||
selectedUser={$selectedCellMap[cellId]}
|
|
||||||
width={$stickyColumn.width}
|
|
||||||
column={$stickyColumn}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</GridScrollWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.sticky-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add right border */
|
|
||||||
.sticky-column:before {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
width: 0;
|
|
||||||
height: 100%;
|
|
||||||
left: calc(100% - 1px);
|
|
||||||
top: 0;
|
|
||||||
border-left: var(--cell-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add shadow when scrolled */
|
|
||||||
.sticky-column.scrolled:after {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
width: 8px;
|
|
||||||
height: 100%;
|
|
||||||
left: 100%;
|
|
||||||
top: 0;
|
|
||||||
opacity: 1;
|
|
||||||
background: linear-gradient(to right, var(--drop-shadow), transparent);
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Don't show borders between cells in the sticky column */
|
|
||||||
.sticky-column :global(.cell:not(:last-child)) {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.header :global(.cell) {
|
|
||||||
background: var(--spectrum-global-color-gray-100);
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for gutter */
|
|
||||||
.gutter {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
|
||||||
padding: var(--cell-padding);
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
gap: var(--cell-spacing);
|
|
||||||
}
|
|
||||||
.checkbox,
|
|
||||||
.number {
|
|
||||||
display: none;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.checkbox :global(.spectrum-Checkbox) {
|
|
||||||
min-height: 0;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
.checkbox :global(.spectrum-Checkbox-box) {
|
|
||||||
margin: 3px 0 0 0;
|
|
||||||
}
|
|
||||||
.number {
|
|
||||||
color: var(--spectrum-global-color-gray-500);
|
|
||||||
}
|
|
||||||
.checkbox.visible,
|
|
||||||
.number.visible {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.expand {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.expand.visible {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: all;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import Avatar from "./Avatar.svelte"
|
|
||||||
|
|
||||||
const { users } = getContext("grid")
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="users">
|
|
||||||
{#each $users as user}
|
|
||||||
<Avatar {user} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.users {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,11 +0,0 @@
|
||||||
export const Padding = 276
|
|
||||||
export const MaxCellRenderHeight = 252
|
|
||||||
export const MaxCellRenderWidthOverflow = 200
|
|
||||||
export const ScrollBarSize = 8
|
|
||||||
export const GutterWidth = 72
|
|
||||||
export const DefaultColumnWidth = 200
|
|
||||||
export const MinColumnWidth = 100
|
|
||||||
export const SmallRowHeight = 36
|
|
||||||
export const MediumRowHeight = 64
|
|
||||||
export const LargeRowHeight = 92
|
|
||||||
export const DefaultRowHeight = SmallRowHeight
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
|
||||||
export const createEventManagers = () => {
|
|
||||||
const svelteDispatch = createEventDispatcher()
|
|
||||||
let subscribers = {}
|
|
||||||
|
|
||||||
// Dispatches an event, notifying subscribers and also emitting a normal
|
|
||||||
// svelte event
|
|
||||||
const dispatch = (event, payload) => {
|
|
||||||
svelteDispatch(event, payload)
|
|
||||||
const subs = subscribers[event] || []
|
|
||||||
for (let i = 0; i < subs.length; i++) {
|
|
||||||
subs[i](payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribes to events
|
|
||||||
const subscribe = (event, callback) => {
|
|
||||||
const subs = subscribers[event] || []
|
|
||||||
subscribers[event] = [...subs, callback]
|
|
||||||
|
|
||||||
// Return unsubscribe function
|
|
||||||
return () => {
|
|
||||||
subscribers[event] = subscribers[event].filter(cb => cb !== callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { dispatch, subscribe }
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import OptionsCell from "../cells/OptionsCell.svelte"
|
|
||||||
import DateCell from "../cells/DateCell.svelte"
|
|
||||||
import MultiSelectCell from "../cells/MultiSelectCell.svelte"
|
|
||||||
import NumberCell from "../cells/NumberCell.svelte"
|
|
||||||
import RelationshipCell from "../cells/RelationshipCell.svelte"
|
|
||||||
import TextCell from "../cells/TextCell.svelte"
|
|
||||||
import LongFormCell from "../cells/LongFormCell.svelte"
|
|
||||||
import BooleanCell from "../cells/BooleanCell.svelte"
|
|
||||||
import FormulaCell from "../cells/FormulaCell.svelte"
|
|
||||||
import JSONCell from "../cells/JSONCell.svelte"
|
|
||||||
import AttachmentCell from "../cells/AttachmentCell.svelte"
|
|
||||||
|
|
||||||
const TypeComponentMap = {
|
|
||||||
text: TextCell,
|
|
||||||
options: OptionsCell,
|
|
||||||
datetime: DateCell,
|
|
||||||
barcodeqr: TextCell,
|
|
||||||
longform: LongFormCell,
|
|
||||||
array: MultiSelectCell,
|
|
||||||
number: NumberCell,
|
|
||||||
boolean: BooleanCell,
|
|
||||||
attachment: AttachmentCell,
|
|
||||||
link: RelationshipCell,
|
|
||||||
formula: FormulaCell,
|
|
||||||
json: JSONCell,
|
|
||||||
}
|
|
||||||
export const getCellRenderer = column => {
|
|
||||||
return TypeComponentMap[column?.schema?.type] || TextCell
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
export const getColor = (idx, opacity = 0.3) => {
|
|
||||||
if (idx == null || idx === -1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
|
|
||||||
}
|
|
||||||
|
|
||||||
const TypeIconMap = {
|
|
||||||
text: "Text",
|
|
||||||
options: "Dropdown",
|
|
||||||
datetime: "Date",
|
|
||||||
barcodeqr: "Camera",
|
|
||||||
longform: "TextAlignLeft",
|
|
||||||
array: "Dropdown",
|
|
||||||
number: "123",
|
|
||||||
boolean: "Boolean",
|
|
||||||
attachment: "AppleFiles",
|
|
||||||
link: "Link",
|
|
||||||
formula: "Calculator",
|
|
||||||
json: "Brackets",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getColumnIcon = column => {
|
|
||||||
const type = column.schema.type
|
|
||||||
return TypeIconMap[type] || "Text"
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { io } from "socket.io-client"
|
|
||||||
|
|
||||||
export const createWebsocket = context => {
|
|
||||||
const { rows, tableId, users, userId, focusedCellId } = context
|
|
||||||
|
|
||||||
// Determine connection info
|
|
||||||
const tls = location.protocol === "https:"
|
|
||||||
const proto = tls ? "wss:" : "ws:"
|
|
||||||
const host = location.hostname
|
|
||||||
const port = location.port || (tls ? 443 : 80)
|
|
||||||
const socket = io(`${proto}//${host}:${port}`, {
|
|
||||||
path: "/socket/grid",
|
|
||||||
// Cap reconnection attempts to 3 (total of 15 seconds before giving up)
|
|
||||||
reconnectionAttempts: 3,
|
|
||||||
// Delay reconnection attempt by 5 seconds
|
|
||||||
reconnectionDelay: 5000,
|
|
||||||
reconnectionDelayMax: 5000,
|
|
||||||
// Timeout after 4 seconds so we never stack requests
|
|
||||||
timeout: 4000,
|
|
||||||
})
|
|
||||||
|
|
||||||
const connectToTable = tableId => {
|
|
||||||
if (!socket.connected) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Identify which table we are editing
|
|
||||||
socket.emit("select-table", tableId, response => {
|
|
||||||
// handle initial connection info
|
|
||||||
users.set(response.users)
|
|
||||||
userId.set(response.id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
socket.on("connect", () => {
|
|
||||||
connectToTable(get(tableId))
|
|
||||||
})
|
|
||||||
socket.on("row-update", data => {
|
|
||||||
if (data.id) {
|
|
||||||
rows.actions.refreshRow(data.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
socket.on("user-update", user => {
|
|
||||||
users.actions.updateUser(user)
|
|
||||||
})
|
|
||||||
socket.on("user-disconnect", user => {
|
|
||||||
users.actions.removeUser(user)
|
|
||||||
})
|
|
||||||
socket.on("connect_error", err => {
|
|
||||||
console.log("Failed to connect to grid websocket:", err.message)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Change websocket connection when table changes
|
|
||||||
tableId.subscribe(connectToTable)
|
|
||||||
|
|
||||||
// Notify selected cell changes
|
|
||||||
focusedCellId.subscribe($focusedCellId => {
|
|
||||||
socket.emit("select-cell", $focusedCellId)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => socket?.disconnect()
|
|
||||||
}
|
|
|
@ -1,180 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext, onMount } from "svelte"
|
|
||||||
import { debounce } from "../../../utils/utils"
|
|
||||||
|
|
||||||
const {
|
|
||||||
enrichedRows,
|
|
||||||
focusedCellId,
|
|
||||||
visibleColumns,
|
|
||||||
focusedRow,
|
|
||||||
stickyColumn,
|
|
||||||
focusedCellAPI,
|
|
||||||
clipboard,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
// Global key listener which intercepts all key events
|
|
||||||
const handleKeyDown = e => {
|
|
||||||
// If nothing selected avoid processing further key presses
|
|
||||||
if (!$focusedCellId) {
|
|
||||||
if (e.key === "Tab") {
|
|
||||||
e.preventDefault()
|
|
||||||
focusFirstCell()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always intercept certain key presses
|
|
||||||
const api = $focusedCellAPI
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
api?.blur?.()
|
|
||||||
} else if (e.key === "Tab") {
|
|
||||||
api?.blur?.()
|
|
||||||
changeFocusedColumn(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass the key event to the selected cell and let it decide whether to
|
|
||||||
// capture it or not
|
|
||||||
if (!api?.isReadonly()) {
|
|
||||||
const handled = api?.onKeyDown?.(e)
|
|
||||||
if (handled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid processing events sourced from modals
|
|
||||||
if (e.target?.closest?.(".spectrum-Modal")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
// Handle the key ourselves
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
switch (e.key) {
|
|
||||||
case "c":
|
|
||||||
clipboard.actions.copy()
|
|
||||||
break
|
|
||||||
case "v":
|
|
||||||
clipboard.actions.paste()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (e.key) {
|
|
||||||
case "ArrowLeft":
|
|
||||||
changeFocusedColumn(-1)
|
|
||||||
break
|
|
||||||
case "ArrowRight":
|
|
||||||
changeFocusedColumn(1)
|
|
||||||
break
|
|
||||||
case "ArrowUp":
|
|
||||||
changeFocusedRow(-1)
|
|
||||||
break
|
|
||||||
case "ArrowDown":
|
|
||||||
changeFocusedRow(1)
|
|
||||||
break
|
|
||||||
case "Delete":
|
|
||||||
case "Backspace":
|
|
||||||
deleteSelectedCell()
|
|
||||||
break
|
|
||||||
case "Enter":
|
|
||||||
focusCell()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
startEnteringValue(e.key, e.which)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focuses the first cell in the grid
|
|
||||||
const focusFirstCell = () => {
|
|
||||||
const firstRow = $enrichedRows[0]
|
|
||||||
if (!firstRow) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const firstColumn = $stickyColumn || $visibleColumns[0]
|
|
||||||
if (!firstColumn) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
focusedCellId.set(`${firstRow._id}-${firstColumn.name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changes the focused cell by moving it left or right to a different column
|
|
||||||
const changeFocusedColumn = delta => {
|
|
||||||
if (!$focusedCellId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const cols = $visibleColumns
|
|
||||||
const split = $focusedCellId.split("-")
|
|
||||||
const columnName = split[1]
|
|
||||||
let newColumnName
|
|
||||||
if (columnName === $stickyColumn?.name) {
|
|
||||||
const index = delta - 1
|
|
||||||
newColumnName = cols[index]?.name
|
|
||||||
} else {
|
|
||||||
const index = cols.findIndex(col => col.name === columnName) + delta
|
|
||||||
if (index === -1) {
|
|
||||||
newColumnName = $stickyColumn?.name
|
|
||||||
} else {
|
|
||||||
newColumnName = cols[index]?.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newColumnName) {
|
|
||||||
$focusedCellId = `${split[0]}-${newColumnName}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changes the focused cell by moving it up or down to a new row
|
|
||||||
const changeFocusedRow = delta => {
|
|
||||||
if (!$focusedRow) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const newRow = $enrichedRows[$focusedRow.__idx + delta]
|
|
||||||
if (newRow) {
|
|
||||||
const split = $focusedCellId.split("-")
|
|
||||||
$focusedCellId = `${newRow._id}-${split[1]}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce to avoid holding down delete and spamming requests
|
|
||||||
const deleteSelectedCell = debounce(() => {
|
|
||||||
if ($focusedCellAPI?.isReadonly()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$focusedCellAPI.setValue(null)
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
// Focuses the current cell for editing
|
|
||||||
const focusCell = () => {
|
|
||||||
if ($focusedCellAPI?.isReadonly()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$focusedCellAPI?.focus?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utils to identify a key code
|
|
||||||
const keyCodeIsNumber = keyCode => keyCode >= 48 && keyCode <= 57
|
|
||||||
const keyCodeIsLetter = keyCode => keyCode >= 65 && keyCode <= 90
|
|
||||||
|
|
||||||
// Focuses the cell and starts entering a new value
|
|
||||||
const startEnteringValue = (key, keyCode) => {
|
|
||||||
if ($focusedCellAPI) {
|
|
||||||
const type = $focusedCellAPI.getType()
|
|
||||||
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
|
||||||
$focusedCellAPI.setValue(parseInt(key))
|
|
||||||
$focusedCellAPI.focus()
|
|
||||||
} else if (
|
|
||||||
["string", "barcodeqr", "longform"].includes(type) &&
|
|
||||||
(keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
|
|
||||||
) {
|
|
||||||
$focusedCellAPI.setValue(key)
|
|
||||||
$focusedCellAPI.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
document.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
|
@ -1,95 +0,0 @@
|
||||||
<script>
|
|
||||||
import { clickOutside, Menu, MenuItem, notifications } from "@budibase/bbui"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const {
|
|
||||||
focusedRow,
|
|
||||||
menu,
|
|
||||||
rows,
|
|
||||||
columns,
|
|
||||||
focusedCellId,
|
|
||||||
stickyColumn,
|
|
||||||
config,
|
|
||||||
copiedCell,
|
|
||||||
clipboard,
|
|
||||||
dispatch,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
$: style = makeStyle($menu)
|
|
||||||
|
|
||||||
const makeStyle = menu => {
|
|
||||||
return `left:${menu.left}px; top:${menu.top}px;`
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteRow = () => {
|
|
||||||
rows.actions.deleteRows([$focusedRow])
|
|
||||||
menu.actions.close()
|
|
||||||
notifications.success("Deleted 1 row")
|
|
||||||
}
|
|
||||||
|
|
||||||
const duplicate = async () => {
|
|
||||||
menu.actions.close()
|
|
||||||
const newRow = await rows.actions.duplicateRow($focusedRow)
|
|
||||||
if (newRow) {
|
|
||||||
const column = $stickyColumn?.name || $columns[0].name
|
|
||||||
$focusedCellId = `${newRow._id}-${column}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $menu.visible}
|
|
||||||
<div class="menu" {style} use:clickOutside={() => menu.actions.close()}>
|
|
||||||
<Menu>
|
|
||||||
<MenuItem
|
|
||||||
icon="Copy"
|
|
||||||
on:click={clipboard.actions.copy}
|
|
||||||
on:click={menu.actions.close}
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="Paste"
|
|
||||||
disabled={$copiedCell == null}
|
|
||||||
on:click={clipboard.actions.paste}
|
|
||||||
on:click={menu.actions.close}
|
|
||||||
>
|
|
||||||
Paste
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="Maximize"
|
|
||||||
disabled={!$config.allowEditRows}
|
|
||||||
on:click={() => dispatch("edit-row", $focusedRow)}
|
|
||||||
on:click={menu.actions.close}
|
|
||||||
>
|
|
||||||
Edit row in modal
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="Duplicate"
|
|
||||||
disabled={!$config.allowAddRows}
|
|
||||||
on:click={duplicate}
|
|
||||||
>
|
|
||||||
Duplicate row
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon="Delete"
|
|
||||||
disabled={!$config.allowDeleteRows}
|
|
||||||
on:click={deleteRow}
|
|
||||||
>
|
|
||||||
Delete row
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.menu {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--cell-background);
|
|
||||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
|
||||||
width: 180px;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,63 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import GridScrollWrapper from "../layout/GridScrollWrapper.svelte"
|
|
||||||
import { DefaultRowHeight, GutterWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
const {
|
|
||||||
isReordering,
|
|
||||||
reorder,
|
|
||||||
visibleColumns,
|
|
||||||
stickyColumn,
|
|
||||||
rowHeight,
|
|
||||||
renderedRows,
|
|
||||||
scrollLeft,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
$: targetColumn = $reorder.targetColumn
|
|
||||||
$: minLeft = GutterWidth + ($stickyColumn?.width || 0)
|
|
||||||
$: left = getLeft(targetColumn, $stickyColumn, $visibleColumns, $scrollLeft)
|
|
||||||
$: height = $rowHeight * $renderedRows.length + DefaultRowHeight
|
|
||||||
$: style = `left:${left}px; height:${height}px;`
|
|
||||||
$: visible = $isReordering && left >= minLeft
|
|
||||||
|
|
||||||
const getLeft = (targetColumn, stickyColumn, visibleColumns, scrollLeft) => {
|
|
||||||
let left = GutterWidth + (stickyColumn?.width || 0) - scrollLeft
|
|
||||||
|
|
||||||
// If this is not the sticky column, add additional left space
|
|
||||||
if (targetColumn !== stickyColumn?.name) {
|
|
||||||
const column = visibleColumns.find(x => x.name === targetColumn)
|
|
||||||
if (!column) {
|
|
||||||
return left
|
|
||||||
}
|
|
||||||
left += column.left + column.width
|
|
||||||
}
|
|
||||||
|
|
||||||
return left
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if visible}
|
|
||||||
<div class="reorder-wrapper">
|
|
||||||
<GridScrollWrapper scrollVertically>
|
|
||||||
<div class="reorder-overlay" {style} />
|
|
||||||
</GridScrollWrapper>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.reorder-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.reorder-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 2px;
|
|
||||||
background: var(--spectrum-global-color-blue-400);
|
|
||||||
margin-left: -2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { GutterWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
const {
|
|
||||||
columns,
|
|
||||||
resize,
|
|
||||||
renderedColumns,
|
|
||||||
stickyColumn,
|
|
||||||
isReordering,
|
|
||||||
scrollLeft,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
$: cutoff = $scrollLeft + GutterWidth + ($columns[0]?.width || 0)
|
|
||||||
$: offset = GutterWidth + ($stickyColumn?.width || 0)
|
|
||||||
$: activeColumn = $resize.column
|
|
||||||
|
|
||||||
const getStyle = (column, offset, scrollLeft) => {
|
|
||||||
const left = offset + column.left + column.width - scrollLeft
|
|
||||||
return `left:${left}px;`
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if !$isReordering}
|
|
||||||
{#if $stickyColumn}
|
|
||||||
<div
|
|
||||||
class="resize-slider"
|
|
||||||
class:visible={activeColumn === $stickyColumn.name}
|
|
||||||
on:mousedown={e => resize.actions.startResizing($stickyColumn, e)}
|
|
||||||
on:dblclick={() => resize.actions.resetSize($stickyColumn)}
|
|
||||||
style="left:{GutterWidth + $stickyColumn.width}px;"
|
|
||||||
>
|
|
||||||
<div class="resize-indicator" />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#each $renderedColumns as column}
|
|
||||||
<div
|
|
||||||
class="resize-slider"
|
|
||||||
class:visible={activeColumn === column.name}
|
|
||||||
on:mousedown={e => resize.actions.startResizing(column, e)}
|
|
||||||
on:dblclick={() => resize.actions.resetSize(column)}
|
|
||||||
style={getStyle(column, offset, $scrollLeft)}
|
|
||||||
>
|
|
||||||
<div class="resize-indicator" />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.resize-slider {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
height: var(--default-row-height);
|
|
||||||
opacity: 0;
|
|
||||||
padding: 0 8px;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
.resize-slider:hover,
|
|
||||||
.resize-slider.visible {
|
|
||||||
cursor: col-resize;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.resize-indicator {
|
|
||||||
margin-left: -1px;
|
|
||||||
width: 2px;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--spectrum-global-color-blue-400);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,121 +0,0 @@
|
||||||
<script>
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
import { domDebounce } from "../../../utils/utils"
|
|
||||||
import { DefaultRowHeight, ScrollBarSize } from "../lib/constants"
|
|
||||||
|
|
||||||
const {
|
|
||||||
scroll,
|
|
||||||
contentHeight,
|
|
||||||
maxScrollTop,
|
|
||||||
contentWidth,
|
|
||||||
maxScrollLeft,
|
|
||||||
screenWidth,
|
|
||||||
showHScrollbar,
|
|
||||||
showVScrollbar,
|
|
||||||
scrollLeft,
|
|
||||||
scrollTop,
|
|
||||||
height,
|
|
||||||
} = getContext("grid")
|
|
||||||
|
|
||||||
// State for dragging bars
|
|
||||||
let initialMouse
|
|
||||||
let initialScroll
|
|
||||||
|
|
||||||
// Calculate V scrollbar size and offset
|
|
||||||
// Terminology is the same for both axes:
|
|
||||||
// renderX - the space available to render the bar in, edge to edge
|
|
||||||
// availX - the space available to render the bar in, until the edge
|
|
||||||
$: renderHeight = $height - 2 * ScrollBarSize
|
|
||||||
$: barHeight = Math.max(50, ($height / $contentHeight) * renderHeight)
|
|
||||||
$: availHeight = renderHeight - barHeight
|
|
||||||
$: barTop =
|
|
||||||
ScrollBarSize +
|
|
||||||
DefaultRowHeight +
|
|
||||||
availHeight * ($scrollTop / $maxScrollTop)
|
|
||||||
|
|
||||||
// Calculate H scrollbar size and offset
|
|
||||||
$: renderWidth = $screenWidth - 2 * ScrollBarSize
|
|
||||||
$: barWidth = Math.max(50, ($screenWidth / $contentWidth) * renderWidth)
|
|
||||||
$: availWidth = renderWidth - barWidth
|
|
||||||
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
|
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
|
||||||
const startVDragging = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
initialMouse = e.clientY
|
|
||||||
initialScroll = $scrollTop
|
|
||||||
document.addEventListener("mousemove", moveVDragging)
|
|
||||||
document.addEventListener("mouseup", stopVDragging)
|
|
||||||
}
|
|
||||||
const moveVDragging = domDebounce(e => {
|
|
||||||
const delta = e.clientY - initialMouse
|
|
||||||
const weight = delta / availHeight
|
|
||||||
const newScrollTop = initialScroll + weight * $maxScrollTop
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
top: Math.max(0, Math.min(newScrollTop, $maxScrollTop)),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
const stopVDragging = () => {
|
|
||||||
document.removeEventListener("mousemove", moveVDragging)
|
|
||||||
document.removeEventListener("mouseup", stopVDragging)
|
|
||||||
}
|
|
||||||
|
|
||||||
// H scrollbar drag handlers
|
|
||||||
const startHDragging = e => {
|
|
||||||
e.preventDefault()
|
|
||||||
initialMouse = e.clientX
|
|
||||||
initialScroll = $scrollLeft
|
|
||||||
document.addEventListener("mousemove", moveHDragging)
|
|
||||||
document.addEventListener("mouseup", stopHDragging)
|
|
||||||
}
|
|
||||||
const moveHDragging = domDebounce(e => {
|
|
||||||
const delta = e.clientX - initialMouse
|
|
||||||
const weight = delta / availWidth
|
|
||||||
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
|
||||||
scroll.update(state => ({
|
|
||||||
...state,
|
|
||||||
left: Math.max(0, Math.min(newScrollLeft, $maxScrollLeft)),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
const stopHDragging = () => {
|
|
||||||
document.removeEventListener("mousemove", moveHDragging)
|
|
||||||
document.removeEventListener("mouseup", stopHDragging)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if $showVScrollbar}
|
|
||||||
<div
|
|
||||||
class="v-scrollbar"
|
|
||||||
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
|
||||||
on:mousedown={startVDragging}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if $showHScrollbar}
|
|
||||||
<div
|
|
||||||
class="h-scrollbar"
|
|
||||||
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
|
||||||
on:mousedown={startHDragging}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--spectrum-global-color-gray-500);
|
|
||||||
opacity: 0.7;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: opacity 130ms ease-out;
|
|
||||||
}
|
|
||||||
div:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.v-scrollbar {
|
|
||||||
width: var(--size);
|
|
||||||
right: var(--size);
|
|
||||||
}
|
|
||||||
.h-scrollbar {
|
|
||||||
height: var(--size);
|
|
||||||
bottom: var(--size);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,16 +0,0 @@
|
||||||
import { derived, writable } from "svelte/store"
|
|
||||||
|
|
||||||
export const createStores = () => {
|
|
||||||
const bounds = writable({
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Derive height and width as primitives to avoid wasted computation
|
|
||||||
const width = derived(bounds, $bounds => $bounds.width, 0)
|
|
||||||
const height = derived(bounds, $bounds => $bounds.height, 0)
|
|
||||||
|
|
||||||
return { bounds, height, width }
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
|
|
||||||
export const createStores = () => {
|
|
||||||
const copiedCell = writable(null)
|
|
||||||
|
|
||||||
return {
|
|
||||||
copiedCell,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveStores = context => {
|
|
||||||
const { copiedCell, focusedCellAPI } = context
|
|
||||||
|
|
||||||
const copy = () => {
|
|
||||||
copiedCell.set(get(focusedCellAPI)?.getValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
const paste = () => {
|
|
||||||
const $copiedCell = get(copiedCell)
|
|
||||||
const $focusedCellAPI = get(focusedCellAPI)
|
|
||||||
if ($copiedCell != null && $focusedCellAPI) {
|
|
||||||
$focusedCellAPI.setValue($copiedCell)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
clipboard: {
|
|
||||||
actions: {
|
|
||||||
copy,
|
|
||||||
paste,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
import { derived, get, writable } from "svelte/store"
|
|
||||||
import { cloneDeep } from "lodash/fp"
|
|
||||||
import { GutterWidth, DefaultColumnWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
export const createStores = () => {
|
|
||||||
const columns = writable([])
|
|
||||||
const stickyColumn = writable(null)
|
|
||||||
|
|
||||||
// Derive an enriched version of columns with left offsets and indexes
|
|
||||||
// automatically calculated
|
|
||||||
const enrichedColumns = derived(
|
|
||||||
columns,
|
|
||||||
$columns => {
|
|
||||||
let offset = 0
|
|
||||||
return $columns.map(column => {
|
|
||||||
const enriched = {
|
|
||||||
...column,
|
|
||||||
left: offset,
|
|
||||||
}
|
|
||||||
if (column.visible) {
|
|
||||||
offset += column.width
|
|
||||||
}
|
|
||||||
return enriched
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Derived list of columns which have not been explicitly hidden
|
|
||||||
const visibleColumns = derived(
|
|
||||||
enrichedColumns,
|
|
||||||
$columns => {
|
|
||||||
return $columns.filter(col => col.visible)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns: {
|
|
||||||
...columns,
|
|
||||||
subscribe: enrichedColumns.subscribe,
|
|
||||||
},
|
|
||||||
stickyColumn,
|
|
||||||
visibleColumns,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveStores = context => {
|
|
||||||
const { table, columns, stickyColumn, API, dispatch } = context
|
|
||||||
|
|
||||||
// Updates the tables primary display column
|
|
||||||
const changePrimaryDisplay = async column => {
|
|
||||||
return await saveTable({
|
|
||||||
...get(table),
|
|
||||||
primaryDisplay: column,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persists column changes by saving metadata against table schema
|
|
||||||
const saveChanges = async () => {
|
|
||||||
const $columns = get(columns)
|
|
||||||
const $table = get(table)
|
|
||||||
const $stickyColumn = get(stickyColumn)
|
|
||||||
const newSchema = cloneDeep($table.schema)
|
|
||||||
|
|
||||||
// Build new updated table schema
|
|
||||||
Object.keys(newSchema).forEach(column => {
|
|
||||||
// Respect order specified by columns
|
|
||||||
const index = $columns.findIndex(x => x.name === column)
|
|
||||||
if (index !== -1) {
|
|
||||||
newSchema[column].order = index
|
|
||||||
} else {
|
|
||||||
delete newSchema[column].order
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy over metadata
|
|
||||||
if (column === $stickyColumn?.name) {
|
|
||||||
newSchema[column].visible = true
|
|
||||||
newSchema[column].width = $stickyColumn.width || DefaultColumnWidth
|
|
||||||
} else {
|
|
||||||
newSchema[column].visible = $columns[index]?.visible ?? true
|
|
||||||
newSchema[column].width = $columns[index]?.width || DefaultColumnWidth
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await saveTable({ ...$table, schema: newSchema })
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveTable = async newTable => {
|
|
||||||
// Update local state
|
|
||||||
table.set(newTable)
|
|
||||||
|
|
||||||
// Broadcast event so that we can keep sync with external state
|
|
||||||
// (e.g. data section which maintains a list of table definitions)
|
|
||||||
dispatch("updatetable", newTable)
|
|
||||||
|
|
||||||
// Update server
|
|
||||||
await API.saveTable(newTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
columns: {
|
|
||||||
...columns,
|
|
||||||
actions: {
|
|
||||||
saveChanges,
|
|
||||||
saveTable,
|
|
||||||
changePrimaryDisplay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialise = context => {
|
|
||||||
const { table, columns, stickyColumn, schemaOverrides } = context
|
|
||||||
|
|
||||||
const schema = derived(
|
|
||||||
[table, schemaOverrides],
|
|
||||||
([$table, $schemaOverrides]) => {
|
|
||||||
let newSchema = $table?.schema
|
|
||||||
if (!newSchema) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
Object.keys($schemaOverrides || {}).forEach(field => {
|
|
||||||
if (newSchema[field]) {
|
|
||||||
newSchema[field] = {
|
|
||||||
...newSchema[field],
|
|
||||||
...$schemaOverrides[field],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return newSchema
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Merge new schema fields with existing schema in order to preserve widths
|
|
||||||
schema.subscribe($schema => {
|
|
||||||
if (!$schema) {
|
|
||||||
columns.set([])
|
|
||||||
stickyColumn.set(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const $table = get(table)
|
|
||||||
|
|
||||||
// Find primary display
|
|
||||||
let primaryDisplay
|
|
||||||
if ($table.primaryDisplay && $schema[$table.primaryDisplay]) {
|
|
||||||
primaryDisplay = $table.primaryDisplay
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get field list
|
|
||||||
let fields = []
|
|
||||||
Object.keys($schema).forEach(field => {
|
|
||||||
if (field !== primaryDisplay) {
|
|
||||||
fields.push(field)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update columns, removing extraneous columns and adding missing ones
|
|
||||||
columns.set(
|
|
||||||
fields
|
|
||||||
.map(field => ({
|
|
||||||
name: field,
|
|
||||||
label: $schema[field].name || field,
|
|
||||||
schema: $schema[field],
|
|
||||||
width: $schema[field].width || DefaultColumnWidth,
|
|
||||||
visible: $schema[field].visible ?? true,
|
|
||||||
order: $schema[field].order,
|
|
||||||
}))
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Sort by order first
|
|
||||||
const orderA = a.order
|
|
||||||
const orderB = b.order
|
|
||||||
if (orderA != null && orderB != null) {
|
|
||||||
return orderA < orderB ? -1 : 1
|
|
||||||
} else if (orderA != null) {
|
|
||||||
return -1
|
|
||||||
} else if (orderB != null) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then sort by auto columns
|
|
||||||
const autoColA = a.schema?.autocolumn
|
|
||||||
const autoColB = b.schema?.autocolumn
|
|
||||||
if (autoColA === autoColB) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return autoColA ? 1 : -1
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update sticky column
|
|
||||||
if (!primaryDisplay) {
|
|
||||||
stickyColumn.set(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
stickyColumn.set({
|
|
||||||
name: primaryDisplay,
|
|
||||||
label: $schema[primaryDisplay].name || primaryDisplay,
|
|
||||||
schema: $schema[primaryDisplay],
|
|
||||||
width: $schema[primaryDisplay].width || DefaultColumnWidth,
|
|
||||||
visible: true,
|
|
||||||
order: 0,
|
|
||||||
left: GutterWidth,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import * as Bounds from "./bounds"
|
|
||||||
import * as Columns from "./columns"
|
|
||||||
import * as Menu from "./menu"
|
|
||||||
import * as Pagination from "./pagination"
|
|
||||||
import * as Reorder from "./reorder"
|
|
||||||
import * as Resize from "./resize"
|
|
||||||
import * as Rows from "./rows"
|
|
||||||
import * as Scroll from "./scroll"
|
|
||||||
import * as UI from "./ui"
|
|
||||||
import * as Users from "./users"
|
|
||||||
import * as Validation from "./validation"
|
|
||||||
import * as Viewport from "./viewport"
|
|
||||||
import * as Clipboard from "./clipboard"
|
|
||||||
|
|
||||||
const DependencyOrderedStores = [
|
|
||||||
Bounds,
|
|
||||||
Scroll,
|
|
||||||
Rows,
|
|
||||||
Columns,
|
|
||||||
UI,
|
|
||||||
Validation,
|
|
||||||
Resize,
|
|
||||||
Viewport,
|
|
||||||
Reorder,
|
|
||||||
Users,
|
|
||||||
Menu,
|
|
||||||
Pagination,
|
|
||||||
Clipboard,
|
|
||||||
]
|
|
||||||
|
|
||||||
export const attachStores = context => {
|
|
||||||
// Atomic store creation
|
|
||||||
for (let store of DependencyOrderedStores) {
|
|
||||||
context = { ...context, ...store.createStores?.(context) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derived store creation
|
|
||||||
for (let store of DependencyOrderedStores) {
|
|
||||||
context = { ...context, ...store.deriveStores?.(context) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise any store logic
|
|
||||||
for (let store of DependencyOrderedStores) {
|
|
||||||
store.initialise?.(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
import { GutterWidth } from "../lib/constants"
|
|
||||||
|
|
||||||
export const createStores = () => {
|
|
||||||
const menu = writable({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
visible: false,
|
|
||||||
selectedRow: null,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
menu,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveStores = context => {
|
|
||||||
const { menu, bounds, focusedCellId, stickyColumn, rowHeight } = context
|
|
||||||
|
|
||||||
const open = (cellId, e) => {
|
|
||||||
const $bounds = get(bounds)
|
|
||||||
const $stickyColumn = get(stickyColumn)
|
|
||||||
const $rowHeight = get(rowHeight)
|
|
||||||
e.preventDefault()
|
|
||||||
focusedCellId.set(cellId)
|
|
||||||
menu.set({
|
|
||||||
left:
|
|
||||||
e.clientX - $bounds.left + GutterWidth + ($stickyColumn?.width || 0),
|
|
||||||
top: e.clientY - $bounds.top + $rowHeight,
|
|
||||||
visible: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
menu.update(state => ({
|
|
||||||
...state,
|
|
||||||
visible: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
menu: {
|
|
||||||
...menu,
|
|
||||||
actions: {
|
|
||||||
open,
|
|
||||||
close,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { derived } from "svelte/store"
|
|
||||||
|
|
||||||
export const initialise = context => {
|
|
||||||
const { scrolledRowCount, rows, visualRowCapacity } = context
|
|
||||||
|
|
||||||
// Derive how many rows we have in total
|
|
||||||
const rowCount = derived(rows, $rows => $rows.length, 0)
|
|
||||||
|
|
||||||
// Derive how many rows we have available to scroll
|
|
||||||
const remainingRows = derived(
|
|
||||||
[scrolledRowCount, rowCount, visualRowCapacity],
|
|
||||||
([$scrolledRowCount, $rowCount, $visualRowCapacity]) => {
|
|
||||||
return Math.max(0, $rowCount - $scrolledRowCount - $visualRowCapacity)
|
|
||||||
},
|
|
||||||
100
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fetch next page when fewer than 25 remaining rows to scroll
|
|
||||||
remainingRows.subscribe(remaining => {
|
|
||||||
if (remaining < 25) {
|
|
||||||
rows.actions.loadNextPage()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
import { get, writable, derived } from "svelte/store"
|
|
||||||
|
|
||||||
const reorderInitialState = {
|
|
||||||
sourceColumn: null,
|
|
||||||
targetColumn: null,
|
|
||||||
breakpoints: [],
|
|
||||||
initialMouseX: null,
|
|
||||||
scrollLeft: 0,
|
|
||||||
gridLeft: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createStores = () => {
|
|
||||||
const reorder = writable(reorderInitialState)
|
|
||||||
const isReordering = derived(
|
|
||||||
reorder,
|
|
||||||
$reorder => !!$reorder.sourceColumn,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
reorder,
|
|
||||||
isReordering,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deriveStores = context => {
|
|
||||||
const { reorder, columns, visibleColumns, scroll, bounds, stickyColumn, ui } =
|
|
||||||
context
|
|
||||||
|
|
||||||
// Callback when dragging on a colum header and starting reordering
|
|
||||||
const startReordering = (column, e) => {
|
|
||||||
const $visibleColumns = get(visibleColumns)
|
|
||||||
const $bounds = get(bounds)
|
|
||||||
const $scroll = get(scroll)
|
|
||||||
const $stickyColumn = get(stickyColumn)
|
|
||||||
ui.actions.blur()
|
|
||||||
|
|
||||||
// Generate new breakpoints for the current columns
|
|
||||||
let breakpoints = $visibleColumns.map(col => ({
|
|
||||||
x: col.left + col.width,
|
|
||||||
column: col.name,
|
|
||||||
}))
|
|
||||||
if ($stickyColumn) {
|
|
||||||
breakpoints.unshift({
|
|
||||||
x: 0,
|
|
||||||
column: $stickyColumn.name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
reorder.set({
|
|
||||||
sourceColumn: column,
|
|
||||||
targetColumn: null,
|
|
||||||
breakpoints,
|
|
||||||
initialMouseX: e.clientX,
|
|
||||||
scrollLeft: $scroll.left,
|
|
||||||
gridLeft: $bounds.left,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add listeners to handle mouse movement
|
|
||||||
document.addEventListener("mousemove", onReorderMouseMove)
|
|
||||||
document.addEventListener("mouseup", stopReordering)
|
|
||||||
|
|
||||||
// Trigger a move event immediately so ensure a candidate column is chosen
|
|
||||||
onReorderMouseMove(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback when moving the mouse when reordering columns
|
|
||||||
const onReorderMouseMove = e => {
|
|
||||||
const $reorder = get(reorder)
|
|
||||||
|
|
||||||
// Compute the closest breakpoint to the current position
|
|
||||||
let targetColumn
|
|
||||||
let minDistance = Number.MAX_SAFE_INTEGER
|
|
||||||
const mouseX = e.clientX - $reorder.gridLeft + $reorder.scrollLeft
|
|
||||||
$reorder.breakpoints.forEach(point => {
|
|
||||||
const distance = Math.abs(point.x - mouseX)
|
|
||||||
if (distance < minDistance) {
|
|
||||||
minDistance = distance
|
|
||||||
targetColumn = point.column
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (targetColumn !== $reorder.targetColumn) {
|
|
||||||
reorder.update(state => ({
|
|
||||||
...state,
|
|
||||||
targetColumn,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback when stopping reordering columns
|
|
||||||
const stopReordering = async () => {
|
|
||||||
// Swap position of columns
|
|
||||||
let { sourceColumn, targetColumn } = get(reorder)
|
|
||||||
moveColumn(sourceColumn, targetColumn)
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
reorder.set(reorderInitialState)
|
|
||||||
|
|
||||||
// Remove event handlers
|
|
||||||
document.removeEventListener("mousemove", onReorderMouseMove)
|
|
||||||
document.removeEventListener("mouseup", stopReordering)
|
|
||||||
|
|
||||||
// Save column changes
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moves a column after another columns.
|
|
||||||
// An undefined target column will move the source to index 0.
|
|
||||||
const moveColumn = (sourceColumn, targetColumn) => {
|
|
||||||
let $columns = get(columns)
|
|
||||||
let sourceIdx = $columns.findIndex(x => x.name === sourceColumn)
|
|
||||||
let targetIdx = $columns.findIndex(x => x.name === targetColumn)
|
|
||||||
targetIdx++
|
|
||||||
columns.update(state => {
|
|
||||||
const removed = state.splice(sourceIdx, 1)
|
|
||||||
if (--targetIdx < sourceIdx) {
|
|
||||||
targetIdx++
|
|
||||||
}
|
|
||||||
state.splice(targetIdx, 0, removed[0])
|
|
||||||
return state.slice()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moves a column one place left (as appears visually)
|
|
||||||
const moveColumnLeft = async column => {
|
|
||||||
const $visibleColumns = get(visibleColumns)
|
|
||||||
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
|
||||||
moveColumn(column, $visibleColumns[sourceIdx - 2]?.name)
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moves a column one place right (as appears visually)
|
|
||||||
const moveColumnRight = async column => {
|
|
||||||
const $visibleColumns = get(visibleColumns)
|
|
||||||
const sourceIdx = $visibleColumns.findIndex(x => x.name === column)
|
|
||||||
if (sourceIdx === $visibleColumns.length - 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
moveColumn(column, $visibleColumns[sourceIdx + 1]?.name)
|
|
||||||
await columns.actions.saveChanges()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
reorder: {
|
|
||||||
...reorder,
|
|
||||||
actions: {
|
|
||||||
startReordering,
|
|
||||||
stopReordering,
|
|
||||||
moveColumnLeft,
|
|
||||||
moveColumnRight,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue