Merge branch 'master' into fix/couchdb-image-build

This commit is contained in:
Michael Drury 2023-10-20 12:23:22 +01:00 committed by GitHub
commit 86b9556b0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 866 additions and 258 deletions

View File

@ -231,7 +231,7 @@ jobs:
cache: "yarn" cache: "yarn"
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Build packages - name: Build packages
run: yarn build --scope @budibase/server --scope @budibase/worker --scope @budibase/client --scope @budibase/backend-core run: yarn build --scope @budibase/server --scope @budibase/worker
- name: Run tests - name: Run tests
run: | run: |
cd qa-core cd qa-core

View File

@ -4,7 +4,9 @@ version: "3"
services: services:
app-service: app-service:
build: ../packages/server build:
context: ..
dockerfile: packages/server/Dockerfile.v2
container_name: build-bbapps container_name: build-bbapps
environment: environment:
SELF_HOSTED: 1 SELF_HOSTED: 1
@ -32,7 +34,9 @@ services:
# - /some/path/to/plugins:/plugins # - /some/path/to/plugins:/plugins
worker-service: worker-service:
build: ../packages/worker build:
context: ..
dockerfile: packages/worker/Dockerfile.v2
container_name: build-bbworker container_name: build-bbworker
environment: environment:
SELF_HOSTED: 1 SELF_HOSTED: 1

View File

@ -19,7 +19,8 @@ COPY packages/string-templates/package.json packages/string-templates/package.js
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
# We will never want to sync pro, but the script is still required # We will never want to sync pro, but the script is still required

View File

@ -46,7 +46,7 @@
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker", "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server", "dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", "dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
"test": "lerna run --stream test --stream", "test": "lerna run --stream test --stream",
"lint:eslint": "eslint packages qa-core --max-warnings=0", "lint:eslint": "eslint packages qa-core --max-warnings=0",
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"", "lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",

View File

@ -35,7 +35,7 @@
"dotenv": "16.0.1", "dotenv": "16.0.1",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"joi": "17.6.0", "joi": "17.6.0",
"jsonwebtoken": "9.0.0", "jsonwebtoken": "9.0.2",
"koa-passport": "4.1.4", "koa-passport": "4.1.4",
"koa-pino-logger": "4.0.0", "koa-pino-logger": "4.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
@ -63,7 +63,7 @@
"@types/chance": "1.1.3", "@types/chance": "1.1.3",
"@types/cookies": "0.7.8", "@types/cookies": "0.7.8",
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/lodash": "4.14.180", "@types/lodash": "4.14.200",
"@types/node": "18.17.0", "@types/node": "18.17.0",
"@types/node-fetch": "2.6.4", "@types/node-fetch": "2.6.4",
"@types/pouchdb": "6.4.0", "@types/pouchdb": "6.4.0",

View File

@ -3,13 +3,10 @@
import { goto, params } from "@roxi/routify" import { goto, params } from "@roxi/routify"
import { Table, Heading, Layout } from "@budibase/bbui" import { Table, Heading, Layout } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import { import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
TableNames,
UNEDITABLE_USER_FIELDS,
UNSORTABLE_TYPES,
} from "constants"
import RoleCell from "./cells/RoleCell.svelte" import RoleCell from "./cells/RoleCell.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { canBeSortColumn } from "@budibase/shared-core"
export let schema = {} export let schema = {}
export let data = [] export let data = []
@ -32,13 +29,11 @@
$: isUsersTable = tableId === TableNames.USERS $: isUsersTable = tableId === TableNames.USERS
$: data && resetSelectedRows() $: data && resetSelectedRows()
$: { $: {
UNSORTABLE_TYPES.forEach(type => {
Object.values(schema || {}).forEach(col => { Object.values(schema || {}).forEach(col => {
if (col.type === type) { if (!canBeSortColumn(col.type)) {
col.sortable = false col.sortable = false
} }
}) })
})
} }
$: { $: {
if (isUsersTable) { if (isUsersTable) {

View File

@ -1,5 +1,9 @@
<script> <script>
import { getContextProviderComponents } from "builderStore/dataBinding" import {
getContextProviderComponents,
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import { import {
Button, Button,
Popover, Popover,
@ -9,6 +13,11 @@
Heading, Heading,
Drawer, Drawer,
DrawerContent, DrawerContent,
Icon,
Modal,
ModalContent,
CoreDropzone,
notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
@ -22,6 +31,8 @@
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte" import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
import IntegrationQueryEditor from "components/integration/index.svelte" import IntegrationQueryEditor from "components/integration/index.svelte"
import { makePropSafe as safe } from "@budibase/string-templates" import { makePropSafe as safe } from "@budibase/string-templates"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { API } from "api"
export let value = {} export let value = {}
export let otherSources export let otherSources
@ -31,9 +42,13 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const arrayTypes = ["attachment", "array"] const arrayTypes = ["attachment", "array"]
let anchorRight, dropdownRight let anchorRight, dropdownRight
let drawer let drawer
let tmpQueryParams let tmpQueryParams
let tmpCustomData
let customDataValid = true
let modal
$: text = value?.label ?? "Choose an option" $: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list.map(m => ({ $: tables = $tablesStore.list.map(m => ({
@ -125,6 +140,10 @@
value: `{{ literal ${runtimeBinding} }}`, value: `{{ literal ${runtimeBinding} }}`,
} }
}) })
$: custom = {
type: "custom",
label: "JSON / CSV",
}
const handleSelected = selected => { const handleSelected = selected => {
dispatch("change", selected) dispatch("change", selected)
@ -151,6 +170,11 @@
drawer.show() drawer.show()
} }
const openCustomDrawer = () => {
tmpCustomData = runtimeToReadableBinding(bindings, value.data || "")
drawer.show()
}
const getQueryValue = queries => { const getQueryValue = queries => {
return queries.find(q => q._id === value._id) || value return queries.find(q => q._id === value._id) || value
} }
@ -162,6 +186,35 @@
}) })
drawer.hide() drawer.hide()
} }
const saveCustomData = () => {
handleSelected({
...value,
data: readableToRuntimeBinding(bindings, tmpCustomData),
})
drawer.hide()
}
const promptForCSV = () => {
drawer.hide()
modal.show()
}
const handleCSV = async e => {
try {
const csv = await e.detail[0]?.text()
if (csv?.length) {
const js = await API.csvToJson(csv)
tmpCustomData = JSON.stringify(js)
}
modal.hide()
saveCustomData()
} catch (error) {
notifications.error("Failed to parse CSV")
modal.hide()
drawer.show()
}
}
</script> </script>
<div class="container" bind:this={anchorRight}> <div class="container" bind:this={anchorRight}>
@ -172,7 +225,9 @@
on:click={dropdownRight.show} on:click={dropdownRight.show}
/> />
{#if value?.type === "query"} {#if value?.type === "query"}
<i class="ri-settings-5-line" on:click={openQueryParamsDrawer} /> <div class="icon">
<Icon hoverable name="Settings" on:click={openQueryParamsDrawer} />
</div>
<Drawer title={"Query Bindings"} bind:this={drawer}> <Drawer title={"Query Bindings"} bind:this={drawer}>
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button> <Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
<DrawerContent slot="body"> <DrawerContent slot="body">
@ -198,6 +253,29 @@
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
{/if} {/if}
{#if value?.type === "custom"}
<div class="icon">
<Icon hoverable name="Settings" on:click={openCustomDrawer} />
</div>
<Drawer title="Custom data" bind:this={drawer}>
<div slot="buttons" style="display:contents">
<Button primary on:click={promptForCSV}>Load CSV</Button>
<Button cta on:click={saveCustomData} disabled={!customDataValid}>
Save
</Button>
</div>
<div slot="description">Provide a JSON array to use as data</div>
<ClientBindingPanel
slot="body"
bind:valid={customDataValid}
value={tmpCustomData}
on:change={event => (tmpCustomData = event.detail)}
{bindings}
allowJS
allowHelpers
/>
</Drawer>
{/if}
</div> </div>
<Popover bind:this={dropdownRight} anchor={anchorRight}> <Popover bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown"> <div class="dropdown">
@ -285,20 +363,27 @@
{/each} {/each}
</ul> </ul>
{/if} {/if}
{#if otherSources?.length}
<Divider /> <Divider />
<div class="title"> <div class="title">
<Heading size="XS">Other</Heading> <Heading size="XS">Other</Heading>
</div> </div>
<ul> <ul>
<li on:click={() => handleSelected(custom)}>{custom.label}</li>
{#if otherSources?.length}
{#each otherSources as source} {#each otherSources as source}
<li on:click={() => handleSelected(source)}>{source.label}</li> <li on:click={() => handleSelected(source)}>{source.label}</li>
{/each} {/each}
</ul>
{/if} {/if}
</ul>
</div> </div>
</Popover> </Popover>
<Modal bind:this={modal}>
<ModalContent title="Load CSV" showConfirmButton={false}>
<CoreDropzone compact extensions=".csv" on:change={handleCSV} />
</ModalContent>
</Modal>
<style> <style>
.container { .container {
display: flex; display: flex;
@ -340,16 +425,7 @@
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-200);
} }
i { .icon {
margin-left: 5px; margin-left: 8px;
display: flex;
align-items: center;
transition: all 0.2s;
}
i:hover {
transform: scale(1.1);
font-weight: 600;
cursor: pointer;
} }
</style> </style>

View File

@ -6,7 +6,7 @@
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { currentAsset } from "builderStore" import { currentAsset } from "builderStore"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { UNSORTABLE_TYPES } from "constants" import { canBeSortColumn } from "@budibase/shared-core"
export let componentInstance = {} export let componentInstance = {}
export let value = "" export let value = ""
@ -20,7 +20,7 @@
const getSortableFields = schema => { const getSortableFields = schema => {
return Object.entries(schema || {}) return Object.entries(schema || {})
.filter(entry => !UNSORTABLE_TYPES.includes(entry[1].type)) .filter(entry => canBeSortColumn(entry[1].type))
.map(entry => entry[0]) .map(entry => entry[0])
} }

View File

@ -34,8 +34,6 @@ export const UNEDITABLE_USER_FIELDS = [
"lastName", "lastName",
] ]
export const UNSORTABLE_TYPES = ["formula", "attachment", "array", "link"]
export const LAYOUT_NAMES = { export const LAYOUT_NAMES = {
MASTER: { MASTER: {
PRIVATE: "layout_private_master", PRIVATE: "layout_private_master",

View File

@ -1,5 +1,5 @@
<script> <script>
import { isEmpty } from "lodash/fp" import { helpers } from "@budibase/shared-core"
import { Input, DetailSummary, notifications } from "@budibase/bbui" import { Input, DetailSummary, notifications } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte" import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
@ -70,41 +70,43 @@
} }
const shouldDisplay = (instance, setting) => { const shouldDisplay = (instance, setting) => {
// Parse dependant settings let dependsOn = setting.dependsOn
if (setting.dependsOn) { if (dependsOn && !Array.isArray(dependsOn)) {
let dependantSetting = setting.dependsOn dependsOn = [dependsOn]
let dependantValue = null }
let invert = !!setting.dependsOn.invert if (!dependsOn?.length) {
if (typeof setting.dependsOn === "object") { return true
dependantSetting = setting.dependsOn.setting }
dependantValue = setting.dependsOn.value
// Ensure all conditions are met
return dependsOn.every(condition => {
let dependantSetting = condition
let dependantValues = null
let invert = !!condition.invert
if (typeof condition === "object") {
dependantSetting = condition.setting
dependantValues = condition.value
} }
if (!dependantSetting) { if (!dependantSetting) {
return false return false
} }
// If no specific value is depended upon, check if a value exists at all // Ensure values is an array
// for the dependent setting if (!Array.isArray(dependantValues)) {
if (dependantValue == null) { dependantValues = [dependantValues]
const currentValue = instance[dependantSetting]
if (currentValue === false) {
return false
}
if (currentValue === true) {
return true
}
return !isEmpty(currentValue)
} }
// Otherwise check the value matches // If inverting, we want to ensure that we don't have any matches.
if (invert) { // If not inverting, we want to ensure that we do have any matches.
return instance[dependantSetting] !== dependantValue const currentVal = helpers.deepGet(instance, dependantSetting)
} else { const anyMatches = dependantValues.some(dependantVal => {
return instance[dependantSetting] === dependantValue if (dependantVal == null) {
return currentVal != null && currentVal !== false && currentVal !== ""
} }
} return dependantVal === currentVal
})
return typeof setting.visible == "boolean" ? setting.visible : true return anyMatches !== invert
})
} }
const canRenderControl = (instance, setting, isScreen) => { const canRenderControl = (instance, setting, isScreen) => {

View File

@ -5556,10 +5556,9 @@
"width": 600, "width": 600,
"height": 400 "height": 400
}, },
"info": "Grid Blocks are only compatible with internal or SQL tables",
"settings": [ "settings": [
{ {
"type": "table", "type": "dataSource",
"label": "Data", "label": "Data",
"key": "table", "key": "table",
"required": true "required": true
@ -5568,18 +5567,35 @@
"type": "columns/grid", "type": "columns/grid",
"label": "Columns", "label": "Columns",
"key": "columns", "key": "columns",
"dependsOn": "table" "dependsOn": [
"table",
{
"setting": "table.type",
"value": "custom",
"invert": true
}
]
}, },
{ {
"type": "filter", "type": "filter",
"label": "Filtering", "label": "Filtering",
"key": "initialFilter" "key": "initialFilter",
"dependsOn": {
"setting": "table.type",
"value": "custom",
"invert": true
}
}, },
{ {
"type": "field/sortable", "type": "field/sortable",
"label": "Sort column", "label": "Sort column",
"key": "initialSortColumn", "key": "initialSortColumn",
"placeholder": "Default" "placeholder": "Default",
"dependsOn": {
"setting": "table.type",
"value": "custom",
"invert": true
}
}, },
{ {
"type": "select", "type": "select",
@ -5618,29 +5634,37 @@
"label": "Clicked row", "label": "Clicked row",
"key": "row" "key": "row"
} }
], ]
"dependsOn": {
"setting": "allowEditRows",
"value": false
}
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Add rows", "label": "Add rows",
"key": "allowAddRows", "key": "allowAddRows",
"defaultValue": true "defaultValue": true,
"dependsOn": {
"setting": "table.type",
"value": ["table", "viewV2"]
}
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Edit rows", "label": "Edit rows",
"key": "allowEditRows", "key": "allowEditRows",
"defaultValue": true "defaultValue": true,
"dependsOn": {
"setting": "table.type",
"value": ["table", "viewV2"]
}
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Delete rows", "label": "Delete rows",
"key": "allowDeleteRows", "key": "allowDeleteRows",
"defaultValue": true "defaultValue": true,
"dependsOn": {
"setting": "table.type",
"value": ["table", "viewV2"]
}
}, },
{ {
"type": "boolean", "type": "boolean",

View File

@ -4,6 +4,7 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
// table is actually any datasource, but called table for legacy compatibility
export let table export let table
export let allowAddRows = true export let allowAddRows = true
export let allowEditRows = true export let allowEditRows = true
@ -21,7 +22,6 @@
$: columnWhitelist = columns?.map(col => col.name) $: columnWhitelist = columns?.map(col => col.name)
$: schemaOverrides = getSchemaOverrides(columns) $: schemaOverrides = getSchemaOverrides(columns)
$: handleRowClick = allowEditRows ? undefined : onRowClick
const getSchemaOverrides = columns => { const getSchemaOverrides = columns => {
let overrides = {} let overrides = {}
@ -58,7 +58,7 @@
showControls={false} showControls={false}
notifySuccess={notificationStore.actions.success} notifySuccess={notificationStore.actions.success}
notifyError={notificationStore.actions.error} notifyError={notificationStore.actions.error}
on:rowclick={e => handleRowClick?.({ row: e.detail })} on:rowclick={e => onRowClick?.({ row: e.detail })}
/> />
</div> </div>

View File

@ -2,8 +2,8 @@
import { getContext } from "svelte" import { getContext } from "svelte"
import { Table } from "@budibase/bbui" import { Table } from "@budibase/bbui"
import SlotRenderer from "./SlotRenderer.svelte" import SlotRenderer from "./SlotRenderer.svelte"
import { UnsortableTypes } from "../../../constants"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import { canBeSortColumn } from "@budibase/shared-core"
export let dataProvider export let dataProvider
export let columns export let columns
@ -102,7 +102,7 @@
return return
} }
newSchema[columnName] = schema[columnName] newSchema[columnName] = schema[columnName]
if (UnsortableTypes.includes(schema[columnName].type)) { if (!canBeSortColumn(schema[columnName].type)) {
newSchema[columnName].sortable = false newSchema[columnName].sortable = false
} }

View File

@ -1,13 +1,5 @@
import { FieldType as FieldTypes } from "@budibase/types"
export { FieldType as FieldTypes } from "@budibase/types" export { FieldType as FieldTypes } from "@budibase/types"
export const UnsortableTypes = [
FieldTypes.FORMULA,
FieldTypes.ATTACHMENT,
FieldTypes.ARRAY,
FieldTypes.LINK,
]
export const ActionTypes = { export const ActionTypes = {
ValidateForm: "ValidateForm", ValidateForm: "ValidateForm",
UpdateFieldValue: "UpdateFieldValue", UpdateFieldValue: "UpdateFieldValue",

View File

@ -34,7 +34,7 @@
column.schema.autocolumn || column.schema.autocolumn ||
column.schema.disabled || column.schema.disabled ||
column.schema.type === "formula" || column.schema.type === "formula" ||
(!$config.canEditRows && row._id) (!$config.canEditRows && !row._isNewRow)
// Register this cell API if the row is focused // Register this cell API if the row is focused
$: { $: {

View File

@ -1,6 +1,6 @@
<script> <script>
import { getContext, onMount, tick } from "svelte" import { getContext, onMount, tick } from "svelte"
import { canBeDisplayColumn } from "@budibase/shared-core" import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui" import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui"
import GridCell from "./GridCell.svelte" import GridCell from "./GridCell.svelte"
import { getColumnIcon } from "../lib/utils" import { getColumnIcon } from "../lib/utils"
@ -23,6 +23,7 @@
columns, columns,
definition, definition,
datasource, datasource,
schema,
} = getContext("grid") } = getContext("grid")
let anchor let anchor
@ -119,16 +120,16 @@
// Generate new name // Generate new name
let newName = `${column.name} copy` let newName = `${column.name} copy`
let attempts = 2 let attempts = 2
while ($definition.schema[newName]) { while ($schema[newName]) {
newName = `${column.name} copy ${attempts++}` newName = `${column.name} copy ${attempts++}`
} }
// Save schema with new column // Save schema with new column
const existingColumnDefinition = $definition.schema[column.name] const existingColumnDefinition = $schema[column.name]
await datasource.actions.saveDefinition({ await datasource.actions.saveDefinition({
...$definition, ...$definition,
schema: { schema: {
...$definition.schema, ...$schema,
[newName]: { [newName]: {
...existingColumnDefinition, ...existingColumnDefinition,
name: newName, name: newName,
@ -231,14 +232,16 @@
<MenuItem <MenuItem
icon="SortOrderUp" icon="SortOrderUp"
on:click={sortAscending} on:click={sortAscending}
disabled={column.name === $sort.column && $sort.order === "ascending"} disabled={!canBeSortColumn(column.schema.type) ||
(column.name === $sort.column && $sort.order === "ascending")}
> >
Sort {ascendingLabel} Sort {ascendingLabel}
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="SortOrderDown" icon="SortOrderDown"
on:click={sortDescending} on:click={sortDescending}
disabled={column.name === $sort.column && $sort.order === "descending"} disabled={!canBeSortColumn(column.schema.type) ||
(column.name === $sort.column && $sort.order === "descending")}
> >
Sort {descendingLabel} Sort {descendingLabel}
</MenuItem> </MenuItem>

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { ActionButton, Popover, Select } from "@budibase/bbui" import { ActionButton, Popover, Select } from "@budibase/bbui"
import { canBeSortColumn } from "@budibase/shared-core"
const { sort, columns, stickyColumn } = getContext("grid") const { sort, columns, stickyColumn } = getContext("grid")
@ -19,7 +20,7 @@
type: stickyColumn.schema?.type, type: stickyColumn.schema?.type,
}) })
} }
return [ options = [
...options, ...options,
...columns.map(col => ({ ...columns.map(col => ({
label: col.label || col.name, label: col.label || col.name,
@ -27,6 +28,7 @@
type: col.schema?.type, type: col.schema?.type,
})), })),
] ]
return options.filter(col => canBeSortColumn(col.type))
} }
const getOrderOptions = (column, columnOptions) => { const getOrderOptions = (column, columnOptions) => {

View File

@ -141,7 +141,14 @@
</div> </div>
</div> </div>
{/if} {/if}
{#if $loaded} {#if $error}
<div class="grid-error">
<div class="grid-error-title">There was a problem loading your grid</div>
<div class="grid-error-subtitle">
{$error}
</div>
</div>
{:else if $loaded}
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}> <div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
<div class="grid-data-inner"> <div class="grid-data-inner">
<StickyColumn> <StickyColumn>
@ -171,13 +178,6 @@
</div> </div>
</div> </div>
</div> </div>
{:else if $error}
<div class="grid-error">
<div class="grid-error-title">There was a problem loading your grid</div>
<div class="grid-error-subtitle">
{$error}
</div>
</div>
{/if} {/if}
{#if $loading && !$error} {#if $loading && !$error}
<div in:fade|local={{ duration: 130 }} class="grid-loading"> <div in:fade|local={{ duration: 130 }} class="grid-loading">

View File

@ -18,6 +18,7 @@
contentLines, contentLines,
isDragging, isDragging,
dispatch, dispatch,
rows,
} = getContext("grid") } = getContext("grid")
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
@ -31,7 +32,7 @@
on:focus on:focus
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)} on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)} on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
on:click={() => dispatch("rowclick", row)} on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
> >
{#each $renderedColumns as column, columnIdx (column.name)} {#each $renderedColumns as column, columnIdx (column.name)}
{@const cellId = `${row._id}-${column.name}`} {@const cellId = `${row._id}-${column.name}`}

View File

@ -33,7 +33,7 @@
let visible = false let visible = false
let isAdding = false let isAdding = false
let newRow = {} let newRow
let offset = 0 let offset = 0
$: firstColumn = $stickyColumn || $renderedColumns[0] $: firstColumn = $stickyColumn || $renderedColumns[0]
@ -58,7 +58,9 @@
// Create row // Create row
const newRowIndex = offset ? undefined : 0 const newRowIndex = offset ? undefined : 0
const savedRow = await rows.actions.addRow(newRow, newRowIndex) let rowToCreate = { ...newRow }
delete rowToCreate._isNewRow
const savedRow = await rows.actions.addRow(rowToCreate, newRowIndex)
if (savedRow) { if (savedRow) {
// Reset state // Reset state
clear() clear()
@ -109,7 +111,7 @@
} }
// Update state and select initial cell // Update state and select initial cell
newRow = {} newRow = { _isNewRow: true }
visible = true visible = true
$hoveredRowId = NewRowID $hoveredRowId = NewRowID
if (firstColumn) { if (firstColumn) {

View File

@ -74,7 +74,7 @@
class="row" class="row"
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)} on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)} on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
on:click={() => dispatch("rowclick", row)} on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
> >
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} /> <GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
{#if $stickyColumn} {#if $stickyColumn}

View File

@ -1,6 +1,6 @@
export const getColor = (idx, opacity = 0.3) => { export const getColor = (idx, opacity = 0.3) => {
if (idx == null || idx === -1) { if (idx == null || idx === -1) {
return null idx = 0
} }
return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})` return `hsla(${((idx + 1) * 222) % 360}, 90%, 75%, ${opacity})`
} }

View File

@ -17,6 +17,7 @@
focusedCellAPI, focusedCellAPI,
focusedRowId, focusedRowId,
notifications, notifications,
isDatasourcePlus,
} = getContext("grid") } = getContext("grid")
$: style = makeStyle($menu) $: style = makeStyle($menu)
@ -75,7 +76,7 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="Copy" icon="Copy"
disabled={isNewRow || !$focusedRow?._id} disabled={isNewRow || !$focusedRow?._id || !$isDatasourcePlus}
on:click={() => copyToClipboard($focusedRow?._id)} on:click={() => copyToClipboard($focusedRow?._id)}
on:click={menu.actions.close} on:click={menu.actions.close}
> >

View File

@ -69,7 +69,7 @@ export const deriveStores = context => {
} }
export const createActions = context => { export const createActions = context => {
const { columns, stickyColumn, datasource, definition } = context const { columns, stickyColumn, datasource, definition, schema } = context
// Updates the datasources primary display column // Updates the datasources primary display column
const changePrimaryDisplay = async column => { const changePrimaryDisplay = async column => {
@ -101,7 +101,7 @@ export const createActions = context => {
const $columns = get(columns) const $columns = get(columns)
const $definition = get(definition) const $definition = get(definition)
const $stickyColumn = get(stickyColumn) const $stickyColumn = get(stickyColumn)
const newSchema = cloneDeep($definition.schema) let newSchema = cloneDeep(get(schema)) || {}
// Build new updated datasource schema // Build new updated datasource schema
Object.keys(newSchema).forEach(column => { Object.keys(newSchema).forEach(column => {
@ -142,26 +142,35 @@ export const createActions = context => {
} }
export const initialise = context => { export const initialise = context => {
const { definition, columns, stickyColumn, schema } = context const { definition, columns, stickyColumn, enrichedSchema } = context
// Merge new schema fields with existing schema in order to preserve widths // Merge new schema fields with existing schema in order to preserve widths
schema.subscribe($schema => { enrichedSchema.subscribe($enrichedSchema => {
if (!$schema) { if (!$enrichedSchema) {
columns.set([]) columns.set([])
stickyColumn.set(null) stickyColumn.set(null)
return return
} }
const $definition = get(definition) const $definition = get(definition)
const $columns = get(columns)
const $stickyColumn = get(stickyColumn)
// Generate array of all columns to easily find pre-existing columns
let allColumns = $columns || []
if ($stickyColumn) {
allColumns.push($stickyColumn)
}
// Find primary display // Find primary display
let primaryDisplay let primaryDisplay
if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) { const candidatePD = $definition.primaryDisplay || $stickyColumn?.name
primaryDisplay = $definition.primaryDisplay if (candidatePD && $enrichedSchema[candidatePD]) {
primaryDisplay = candidatePD
} }
// Get field list // Get field list
let fields = [] let fields = []
Object.keys($schema).forEach(field => { Object.keys($enrichedSchema).forEach(field => {
if (field !== primaryDisplay) { if (field !== primaryDisplay) {
fields.push(field) fields.push(field)
} }
@ -170,14 +179,18 @@ export const initialise = context => {
// Update columns, removing extraneous columns and adding missing ones // Update columns, removing extraneous columns and adding missing ones
columns.set( columns.set(
fields fields
.map(field => ({ .map(field => {
const fieldSchema = $enrichedSchema[field]
const oldColumn = allColumns?.find(x => x.name === field)
return {
name: field, name: field,
label: $schema[field].displayName || field, label: fieldSchema.displayName || field,
schema: $schema[field], schema: fieldSchema,
width: $schema[field].width || DefaultColumnWidth, width: fieldSchema.width || oldColumn?.width || DefaultColumnWidth,
visible: $schema[field].visible ?? true, visible: fieldSchema.visible ?? true,
order: $schema[field].order, order: fieldSchema.order ?? oldColumn?.order,
})) }
})
.sort((a, b) => { .sort((a, b) => {
// Sort by order first // Sort by order first
const orderA = a.order const orderA = a.order
@ -205,11 +218,13 @@ export const initialise = context => {
stickyColumn.set(null) stickyColumn.set(null)
return return
} }
const stickySchema = $enrichedSchema[primaryDisplay]
const oldStickyColumn = allColumns?.find(x => x.name === primaryDisplay)
stickyColumn.set({ stickyColumn.set({
name: primaryDisplay, name: primaryDisplay,
label: $schema[primaryDisplay].displayName || primaryDisplay, label: stickySchema.displayName || primaryDisplay,
schema: $schema[primaryDisplay], schema: stickySchema,
width: $schema[primaryDisplay].width || DefaultColumnWidth, width: stickySchema.width || oldStickyColumn?.width || DefaultColumnWidth,
visible: true, visible: true,
order: 0, order: 0,
left: GutterWidth, left: GutterWidth,

View File

@ -37,9 +37,10 @@ export const deriveStores = context => {
[props, hasNonAutoColumn], [props, hasNonAutoColumn],
([$props, $hasNonAutoColumn]) => { ([$props, $hasNonAutoColumn]) => {
let config = { ...$props } let config = { ...$props }
const type = $props.datasource?.type
// Disable some features if we're editing a view // Disable some features if we're editing a view
if ($props.datasource?.type === "viewV2") { if (type === "viewV2") {
config.canEditColumns = false config.canEditColumns = false
} }
@ -48,6 +49,16 @@ export const deriveStores = context => {
config.canAddRows = false config.canAddRows = false
} }
// Disable features for non DS+
if (!["table", "viewV2"].includes(type)) {
config.canAddRows = false
config.canEditRows = false
config.canDeleteRows = false
config.canExpandRows = false
config.canSaveSchema = false
config.canEditColumns = false
}
return config return config
} }
) )

View File

@ -1,4 +1,5 @@
import { derived, get, writable } from "svelte/store" import { derived, get, writable } from "svelte/store"
import { getDatasourceDefinition } from "../../../fetch"
export const createStores = () => { export const createStores = () => {
const definition = writable(null) const definition = writable(null)
@ -9,21 +10,38 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { definition, schemaOverrides, columnWhitelist } = context const { definition, schemaOverrides, columnWhitelist, datasource } = context
const schema = derived( const schema = derived(definition, $definition => {
[definition, schemaOverrides, columnWhitelist], let schema = $definition?.schema
([$definition, $schemaOverrides, $columnWhitelist]) => { if (!schema) {
if (!$definition?.schema) {
return null return null
} }
let newSchema = { ...$definition?.schema }
// Ensure schema is configured as objects.
// Certain datasources like queries use primitives.
Object.keys(schema || {}).forEach(key => {
if (typeof schema[key] !== "object") {
schema[key] = { type: schema[key] }
}
})
return schema
})
const enrichedSchema = derived(
[schema, schemaOverrides, columnWhitelist],
([$schema, $schemaOverrides, $columnWhitelist]) => {
if (!$schema) {
return null
}
let enrichedSchema = { ...$schema }
// Apply schema overrides // Apply schema overrides
Object.keys($schemaOverrides || {}).forEach(field => { Object.keys($schemaOverrides || {}).forEach(field => {
if (newSchema[field]) { if (enrichedSchema[field]) {
newSchema[field] = { enrichedSchema[field] = {
...newSchema[field], ...enrichedSchema[field],
...$schemaOverrides[field], ...$schemaOverrides[field],
} }
} }
@ -31,41 +49,64 @@ export const deriveStores = context => {
// Apply whitelist if specified // Apply whitelist if specified
if ($columnWhitelist?.length) { if ($columnWhitelist?.length) {
Object.keys(newSchema).forEach(key => { Object.keys(enrichedSchema).forEach(key => {
if (!$columnWhitelist.includes(key)) { if (!$columnWhitelist.includes(key)) {
delete newSchema[key] delete enrichedSchema[key]
} }
}) })
} }
return newSchema return enrichedSchema
} }
) )
const isDatasourcePlus = derived(datasource, $datasource => {
return ["table", "viewV2"].includes($datasource?.type)
})
return { return {
schema, schema,
enrichedSchema,
isDatasourcePlus,
} }
} }
export const createActions = context => { export const createActions = context => {
const { datasource, definition, config, dispatch, table, viewV2 } = context const {
API,
datasource,
definition,
config,
dispatch,
table,
viewV2,
nonPlus,
} = context
// Gets the appropriate API for the configured datasource type // Gets the appropriate API for the configured datasource type
const getAPI = () => { const getAPI = () => {
const $datasource = get(datasource) const $datasource = get(datasource)
switch ($datasource?.type) { const type = $datasource?.type
if (!type) {
return null
}
switch (type) {
case "table": case "table":
return table return table
case "viewV2": case "viewV2":
return viewV2 return viewV2
default: default:
return null return nonPlus
} }
} }
// Refreshes the datasource definition // Refreshes the datasource definition
const refreshDefinition = async () => { const refreshDefinition = async () => {
return await getAPI()?.actions.refreshDefinition() const def = await getDatasourceDefinition({
API,
datasource: get(datasource),
})
definition.set(def)
} }
// Saves the datasource definition // Saves the datasource definition
@ -113,6 +154,11 @@ export const createActions = context => {
return getAPI()?.actions.canUseColumn(name) return getAPI()?.actions.canUseColumn(name)
} }
// Gets the default number of rows for a single page
const getFeatures = () => {
return getAPI()?.actions.getFeatures()
}
return { return {
datasource: { datasource: {
...datasource, ...datasource,
@ -125,6 +171,7 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View File

@ -0,0 +1,124 @@
import { get } from "svelte/store"
export const createActions = context => {
const { columns, stickyColumn, table, viewV2 } = context
const saveDefinition = async () => {
throw "This datasource does not support updating the definition"
}
const saveRow = async () => {
throw "This datasource does not support saving rows"
}
const deleteRows = async () => {
throw "This datasource does not support deleting rows"
}
const getRow = () => {
throw "This datasource does not support fetching individual rows"
}
const isDatasourceValid = datasource => {
// There are many different types and shapes of datasource, so we only
// check that we aren't null
return (
!table.actions.isDatasourceValid(datasource) &&
!viewV2.actions.isDatasourceValid(datasource) &&
datasource?.type != null
)
}
const canUseColumn = name => {
const $columns = get(columns)
const $sticky = get(stickyColumn)
return $columns.some(col => col.name === name) || $sticky?.name === name
}
const getFeatures = () => {
// We don't support any features
return {}
}
return {
nonPlus: {
actions: {
saveDefinition,
addRow: saveRow,
updateRow: saveRow,
deleteRows,
getRow,
isDatasourceValid,
canUseColumn,
getFeatures,
},
},
}
}
// Small util to compare datasource definitions
const isSameDatasource = (a, b) => {
return JSON.stringify(a) === JSON.stringify(b)
}
export const initialise = context => {
const {
datasource,
sort,
filter,
nonPlus,
initialFilter,
initialSortColumn,
initialSortOrder,
fetch,
} = context
// Keep a list of subscriptions so that we can clear them when the datasource
// config changes
let unsubscribers = []
// Observe datasource changes and apply logic for view V2 datasources
datasource.subscribe($datasource => {
// Clear previous subscriptions
unsubscribers?.forEach(unsubscribe => unsubscribe())
unsubscribers = []
if (!nonPlus.actions.isDatasourceValid($datasource)) {
return
}
// Wipe state
filter.set(get(initialFilter))
sort.set({
column: get(initialSortColumn),
order: get(initialSortOrder) || "ascending",
})
// Update fetch when filter changes
unsubscribers.push(
filter.subscribe($filter => {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
return
}
$fetch.update({
filter: $filter,
})
})
)
// Update fetch when sorting changes
unsubscribers.push(
sort.subscribe($sort => {
// Ensure we're updating the correct fetch
const $fetch = get(fetch)
if (!isSameDatasource($fetch?.options?.datasource, $datasource)) {
return
}
$fetch.update({
sortOrder: $sort.order || "ascending",
sortColumn: $sort.column,
})
})
)
})
}

View File

@ -1,13 +1,10 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import TableFetch from "../../../../fetch/TableFetch"
const SuppressErrors = true const SuppressErrors = true
export const createActions = context => { export const createActions = context => {
const { definition, API, datasource, columns, stickyColumn } = context const { API, datasource, columns, stickyColumn } = context
const refreshDefinition = async () => {
definition.set(await API.fetchTableDefinition(get(datasource).tableId))
}
const saveDefinition = async newDefinition => { const saveDefinition = async newDefinition => {
await API.saveTable(newDefinition) await API.saveTable(newDefinition)
@ -49,10 +46,13 @@ export const createActions = context => {
return $columns.some(col => col.name === name) || $sticky?.name === name return $columns.some(col => col.name === name) || $sticky?.name === name
} }
const getFeatures = () => {
return new TableFetch({ API }).determineFeatureFlags()
}
return { return {
table: { table: {
actions: { actions: {
refreshDefinition,
saveDefinition, saveDefinition,
addRow: saveRow, addRow: saveRow,
updateRow: saveRow, updateRow: saveRow,
@ -60,6 +60,7 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View File

@ -1,22 +1,10 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import ViewV2Fetch from "../../../../fetch/ViewV2Fetch"
const SuppressErrors = true const SuppressErrors = true
export const createActions = context => { export const createActions = context => {
const { definition, API, datasource, columns, stickyColumn } = context const { API, datasource, columns, stickyColumn } = context
const refreshDefinition = async () => {
const $datasource = get(datasource)
if (!$datasource) {
definition.set(null)
return
}
const table = await API.fetchTableDefinition($datasource.tableId)
const view = Object.values(table?.views || {}).find(
view => view.id === $datasource.id
)
definition.set(view)
}
const saveDefinition = async newDefinition => { const saveDefinition = async newDefinition => {
await API.viewV2.update(newDefinition) await API.viewV2.update(newDefinition)
@ -58,10 +46,13 @@ export const createActions = context => {
) )
} }
const getFeatures = () => {
return new ViewV2Fetch({ API }).determineFeatureFlags()
}
return { return {
viewV2: { viewV2: {
actions: { actions: {
refreshDefinition,
saveDefinition, saveDefinition,
addRow: saveRow, addRow: saveRow,
updateRow: saveRow, updateRow: saveRow,
@ -69,6 +60,7 @@ export const createActions = context => {
getRow, getRow,
isDatasourceValid, isDatasourceValid,
canUseColumn, canUseColumn,
getFeatures,
}, },
}, },
} }

View File

@ -15,9 +15,10 @@ import * as Config from "./config"
import * as Sort from "./sort" import * as Sort from "./sort"
import * as Filter from "./filter" import * as Filter from "./filter"
import * as Notifications from "./notifications" import * as Notifications from "./notifications"
import * as Table from "./table"
import * as ViewV2 from "./viewV2"
import * as Datasource from "./datasource" import * as Datasource from "./datasource"
import * as Table from "./datasources/table"
import * as ViewV2 from "./datasources/viewV2"
import * as NonPlus from "./datasources/nonPlus"
const DependencyOrderedStores = [ const DependencyOrderedStores = [
Sort, Sort,
@ -26,6 +27,7 @@ const DependencyOrderedStores = [
Scroll, Scroll,
Table, Table,
ViewV2, ViewV2,
NonPlus,
Datasource, Datasource,
Columns, Columns,
Rows, Rows,

View File

@ -1,7 +1,8 @@
import { writable, derived, get } from "svelte/store" import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData" import { fetchData } from "../../../fetch"
import { NewRowID, RowPageSize } from "../lib/constants" import { NewRowID, RowPageSize } from "../lib/constants"
import { tick } from "svelte" import { tick } from "svelte"
import { Helpers } from "@budibase/bbui"
export const createStores = () => { export const createStores = () => {
const rows = writable([]) const rows = writable([])
@ -76,11 +77,11 @@ export const createActions = context => {
columns, columns,
rowChangeCache, rowChangeCache,
inProgressChanges, inProgressChanges,
previousFocusedRowId,
hasNextPage, hasNextPage,
error, error,
notifications, notifications,
fetch, fetch,
isDatasourcePlus,
} = context } = context
const instanceLoaded = writable(false) const instanceLoaded = writable(false)
@ -93,12 +94,14 @@ export const createActions = context => {
datasource.subscribe(async $datasource => { datasource.subscribe(async $datasource => {
// Unsub from previous fetch if one exists // Unsub from previous fetch if one exists
unsubscribe?.() unsubscribe?.()
unsubscribe = null
fetch.set(null) fetch.set(null)
instanceLoaded.set(false) instanceLoaded.set(false)
loading.set(true) loading.set(true)
// Abandon if we don't have a valid datasource // Abandon if we don't have a valid datasource
if (!datasource.actions.isDatasourceValid($datasource)) { if (!datasource.actions.isDatasourceValid($datasource)) {
error.set("Datasource is invalid")
return return
} }
@ -108,6 +111,10 @@ export const createActions = context => {
const $filter = get(filter) const $filter = get(filter)
const $sort = get(sort) const $sort = get(sort)
// Determine how many rows to fetch per page
const features = datasource.actions.getFeatures()
const limit = features?.supportsPagination ? RowPageSize : null
// Create new fetch model // Create new fetch model
const newFetch = fetchData({ const newFetch = fetchData({
API, API,
@ -116,7 +123,7 @@ export const createActions = context => {
filter: $filter, filter: $filter,
sortColumn: $sort.column, sortColumn: $sort.column,
sortOrder: $sort.order, sortOrder: $sort.order,
limit: RowPageSize, limit,
paginate: true, paginate: true,
}, },
}) })
@ -355,7 +362,7 @@ export const createActions = context => {
// Update row // Update row
const saved = await datasource.actions.updateRow({ const saved = await datasource.actions.updateRow({
...row, ...cleanRow(row),
...get(rowChangeCache)[rowId], ...get(rowChangeCache)[rowId],
}) })
@ -411,8 +418,17 @@ export const createActions = context => {
} }
let rowsToAppend = [] let rowsToAppend = []
let newRow let newRow
const $isDatasourcePlus = get(isDatasourcePlus)
for (let i = 0; i < newRows.length; i++) { for (let i = 0; i < newRows.length; i++) {
newRow = newRows[i] newRow = newRows[i]
// Ensure we have a unique _id.
// This means generating one for non DS+, overriting any that may already
// exist as we cannot allow duplicates.
if (!$isDatasourcePlus) {
newRow._id = Helpers.uuid()
}
if (!rowCacheMap[newRow._id]) { if (!rowCacheMap[newRow._id]) {
rowCacheMap[newRow._id] = true rowCacheMap[newRow._id] = true
rowsToAppend.push(newRow) rowsToAppend.push(newRow)
@ -449,15 +465,16 @@ export const createActions = context => {
return get(rowLookupMap)[id] != null return get(rowLookupMap)[id] != null
} }
// Wipe the row change cache when changing row // Cleans a row by removing any internal grid metadata from it.
previousFocusedRowId.subscribe(id => { // Call this before passing a row to any sort of external flow.
if (id && !get(inProgressChanges)[id]) { const cleanRow = row => {
rowChangeCache.update(state => { let clone = { ...row }
delete state[id] delete clone.__idx
return state if (!get(isDatasourcePlus)) {
}) delete clone._id
}
return clone
} }
})
return { return {
rows: { rows: {
@ -474,7 +491,22 @@ export const createActions = context => {
refreshRow, refreshRow,
replaceRow, replaceRow,
refreshData, refreshData,
cleanRow,
}, },
}, },
} }
} }
export const initialise = context => {
const { rowChangeCache, inProgressChanges, previousFocusedRowId } = context
// Wipe the row change cache when changing row
previousFocusedRowId.subscribe(id => {
if (id && !get(inProgressChanges)[id]) {
rowChangeCache.update(state => {
delete state[id]
return state
})
}
})
}

View File

@ -17,7 +17,7 @@ export const createStores = context => {
} }
export const initialise = context => { export const initialise = context => {
const { sort, initialSortColumn, initialSortOrder, definition } = context const { sort, initialSortColumn, initialSortOrder, schema } = context
// Reset sort when initial sort props change // Reset sort when initial sort props change
initialSortColumn.subscribe(newSortColumn => { initialSortColumn.subscribe(newSortColumn => {
@ -28,15 +28,12 @@ export const initialise = context => {
}) })
// Derive if the current sort column exists in the schema // Derive if the current sort column exists in the schema
const sortColumnExists = derived( const sortColumnExists = derived([sort, schema], ([$sort, $schema]) => {
[sort, definition], if (!$sort?.column || !$schema) {
([$sort, $definition]) => {
if (!$sort?.column || !$definition) {
return true return true
} }
return $definition.schema?.[$sort.column] != null return $schema[$sort.column] != null
} })
)
// Clear sort state if our sort column does not exist // Clear sort state if our sort column does not exist
sortColumnExists.subscribe(exists => { sortColumnExists.subscribe(exists => {

View File

@ -0,0 +1,145 @@
import DataFetch from "./DataFetch.js"
export default class CustomFetch extends DataFetch {
// Gets the correct Budibase type for a JS value
getType(value) {
if (value == null) {
return "string"
}
const type = typeof value
if (type === "object") {
if (Array.isArray(value)) {
// Use our custom array type to render badges
return "array"
}
// Use JSON for objects to ensure they are stringified
return "json"
} else if (!isNaN(value)) {
return "number"
} else {
return "string"
}
}
// Parses the custom data into an array format
parseCustomData(data) {
if (!data) {
return []
}
// Happy path - already an array
if (Array.isArray(data)) {
return data
}
// For strings, try JSON then fall back to attempting a CSV
if (typeof data === "string") {
try {
const js = JSON.parse(data)
return Array.isArray(js) ? js : [js]
} catch (error) {
// Ignore
}
// Try splitting by newlines first
if (data.includes("\n")) {
return data.split("\n").map(x => x.trim())
}
// Split by commas next
return data.split(",").map(x => x.trim())
}
// Other cases we just assume it's a single object and wrap it
return [data]
}
// Enriches the custom data to ensure the structure and format is usable
enrichCustomData(data) {
if (!data?.length) {
return []
}
// Filter out any invalid values
data = data.filter(x => x != null && x !== "" && !Array.isArray(x))
// Ensure all values are packed into objects
return data.map(value => {
if (typeof value === "object") {
return value
}
// Try parsing strings
if (typeof value === "string") {
const split = value.split(",").map(x => x.trim())
let obj = {}
for (let i = 0; i < split.length; i++) {
const suffix = i === 0 ? "" : ` ${i + 1}`
const key = `Value${suffix}`
obj[key] = split[i]
}
return obj
}
// For anything else, wrap in an object
return { Value: value }
})
}
// Extracts and parses the custom data from the datasource definition
getCustomData(datasource) {
return this.enrichCustomData(this.parseCustomData(datasource?.data))
}
async getDefinition(datasource) {
// Try and work out the schema from the array provided
let schema = {}
const data = this.getCustomData(datasource)
if (!data?.length) {
return { schema }
}
// Go through every object and extract all valid keys
for (let datum of data) {
for (let key of Object.keys(datum)) {
if (key === "_id") {
continue
}
if (!schema[key]) {
let type = this.getType(datum[key])
let constraints = {}
// Determine whether we should render text columns as options instead
if (type === "string") {
const uniqueValues = [...new Set(data.map(x => x[key]))]
const uniqueness = uniqueValues.length / data.length
if (uniqueness <= 0.8 && uniqueValues.length > 1) {
type = "options"
constraints.inclusion = uniqueValues
}
}
// Generate options for array columns
else if (type === "array") {
constraints.inclusion = [...new Set(data.map(x => x[key]).flat())]
}
schema[key] = {
type,
constraints,
}
}
}
}
return { schema }
}
async getData() {
const { datasource } = this.options
return {
rows: this.getCustomData(datasource),
hasNextPage: false,
cursor: null,
}
}
}

View File

@ -8,6 +8,7 @@ import FieldFetch from "./FieldFetch.js"
import JSONArrayFetch from "./JSONArrayFetch.js" import JSONArrayFetch from "./JSONArrayFetch.js"
import UserFetch from "./UserFetch.js" import UserFetch from "./UserFetch.js"
import GroupUserFetch from "./GroupUserFetch.js" import GroupUserFetch from "./GroupUserFetch.js"
import CustomFetch from "./CustomFetch.js"
const DataFetchMap = { const DataFetchMap = {
table: TableFetch, table: TableFetch,
@ -17,6 +18,7 @@ const DataFetchMap = {
link: RelationshipFetch, link: RelationshipFetch,
user: UserFetch, user: UserFetch,
groupUser: GroupUserFetch, groupUser: GroupUserFetch,
custom: CustomFetch,
// Client specific datasource types // Client specific datasource types
provider: NestedProviderFetch, provider: NestedProviderFetch,
@ -24,7 +26,18 @@ const DataFetchMap = {
jsonarray: JSONArrayFetch, jsonarray: JSONArrayFetch,
} }
// Constructs a new fetch model for a certain datasource
export const fetchData = ({ API, datasource, options }) => { export const fetchData = ({ API, datasource, options }) => {
const Fetch = DataFetchMap[datasource?.type] || TableFetch const Fetch = DataFetchMap[datasource?.type] || TableFetch
return new Fetch({ API, datasource, ...options }) return new Fetch({ API, datasource, ...options })
} }
// Fetches the definition of any type of datasource
export const getDatasourceDefinition = async ({ API, datasource }) => {
const handler = DataFetchMap[datasource?.type]
if (!handler) {
return null
}
const instance = new handler({ API })
return await instance.getDefinition(datasource)
}

View File

@ -1,5 +1,5 @@
export { createAPIClient } from "./api" export { createAPIClient } from "./api"
export { fetchData } from "./fetch/fetchData" export { fetchData } from "./fetch"
export { Utils } from "./utils" export { Utils } from "./utils"
export * as Constants from "./constants" export * as Constants from "./constants"
export * from "./stores" export * from "./stores"

View File

@ -0,0 +1,79 @@
FROM node:18-slim
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="scripts/watchtower-hooks/post-update.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-hooks/post-check.sh"
WORKDIR /app
ENV PORT=4001
ENV COUCH_DB_URL=https://couchdb.budi.live:5984
ENV BUDIBASE_ENVIRONMENT=PRODUCTION
ENV SERVICE=app-service
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
ENV TOP_LEVEL_PATH=/
# handle node-gyp
RUN apt-get update \
&& apt-get install -y --no-install-recommends g++ make python3 jq
RUN yarn global add pm2
# Install client for oracle datasource
RUN apt-get install unzip libaio1
COPY packages/server/scripts/integrations/oracle/ scripts/integrations/oracle/
RUN /bin/bash -e scripts/integrations/oracle/instantclient/linux/x86-64/install.sh
# Install postgres client for pg_dump utils
RUN apt update && apt upgrade -y \
&& apt install software-properties-common apt-transport-https curl gpg -y \
&& curl -fsSl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null \
&& echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | tee /etc/apt/sources.list.d/postgresql.list \
&& apt update -y \
&& apt install postgresql-client-15 -y \
&& apt remove software-properties-common apt-transport-https curl gpg -y
WORKDIR /
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
COPY packages/string-templates .
WORKDIR /app
COPY packages/server/package.json .
COPY packages/server/dist/yarn.lock .
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
RUN ./scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true \
# Remove unneeded data from file system to reduce image size
&& yarn cache clean && apt-get remove -y --purge --auto-remove g++ make python jq \
&& rm -rf /tmp/* /root/.node-gyp /usr/local/lib/node_modules/npm/node_modules/node-gyp
COPY packages/server/dist/ dist/
COPY packages/server/docker_run.sh .
COPY packages/server/builder/ builder/
COPY packages/server/client/ client/
EXPOSE 4001
# have to add node environment production after install
# due to this causing yarn to stop installing dev dependencies
# which are actually needed to get this environment up and running
ENV NODE_ENV=production
ENV CLUSTER_MODE=${CLUSTER_MODE}
ENV TOP_LEVEL_PATH=/app
CMD ["./docker_run.sh"]

View File

@ -122,7 +122,7 @@
"@types/jest": "29.5.5", "@types/jest": "29.5.5",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa__router": "8.0.8", "@types/koa__router": "8.0.8",
"@types/lodash": "4.14.180", "@types/lodash": "4.14.200",
"@types/mssql": "8.1.2", "@types/mssql": "8.1.2",
"@types/node": "18.17.0", "@types/node": "18.17.0",
"@types/node-fetch": "2.6.4", "@types/node-fetch": "2.6.4",

View File

@ -20,6 +20,30 @@ const allowDisplayColumnByType: Record<FieldType, boolean> = {
[FieldType.BB_REFERENCE]: false, [FieldType.BB_REFERENCE]: false,
} }
const allowSortColumnByType: Record<FieldType, boolean> = {
[FieldType.STRING]: true,
[FieldType.LONGFORM]: true,
[FieldType.OPTIONS]: true,
[FieldType.NUMBER]: true,
[FieldType.DATETIME]: true,
[FieldType.AUTO]: true,
[FieldType.INTERNAL]: true,
[FieldType.BARCODEQR]: true,
[FieldType.BIGINT]: true,
[FieldType.BOOLEAN]: true,
[FieldType.JSON]: true,
[FieldType.FORMULA]: false,
[FieldType.ATTACHMENT]: false,
[FieldType.ARRAY]: false,
[FieldType.LINK]: false,
[FieldType.BB_REFERENCE]: false,
}
export function canBeDisplayColumn(type: FieldType): boolean { export function canBeDisplayColumn(type: FieldType): boolean {
return !!allowDisplayColumnByType[type] return !!allowDisplayColumnByType[type]
} }
export function canBeSortColumn(type: FieldType): boolean {
return !!allowSortColumnByType[type]
}

View File

@ -28,7 +28,7 @@
"@budibase/handlebars-helpers": "^0.11.9", "@budibase/handlebars-helpers": "^0.11.9",
"dayjs": "^1.10.8", "dayjs": "^1.10.8",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"lodash": "^4.17.20", "lodash": "4.17.21",
"vm2": "^3.9.19" "vm2": "^3.9.19"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,53 @@
FROM node:18-alpine
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="scripts/watchtower-hooks/post-update.sh"
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="scripts/watchtower-hooks/post-check.sh"
# handle node-gyp
RUN apk add --no-cache --virtual .gyp python3 make g++ jq
RUN yarn global add pm2
WORKDIR /
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
WORKDIR /string-templates
COPY packages/string-templates/package.json package.json
RUN ../scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
COPY packages/string-templates .
WORKDIR /app
COPY packages/worker/package.json .
COPY packages/worker/dist/yarn.lock .
RUN cd ../string-templates && yarn link && cd - && yarn link @budibase/string-templates
RUN ../scripts/removeWorkspaceDependencies.sh package.json
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production=true
# Remove unneeded data from file system to reduce image size
RUN apk del .gyp \
&& yarn cache clean
COPY packages/worker/dist/ dist/
COPY packages/worker/docker_run.sh .
EXPOSE 4001
# have to add node environment production after install
# due to this causing yarn to stop installing dev dependencies
# which are actually needed to get this environment up and running
ENV NODE_ENV=production
ENV CLUSTER_MODE=${CLUSTER_MODE}
ENV SERVICE=worker-service
ENV POSTHOG_TOKEN=phc_bIjZL7oh2GEUd2vqvTBH8WvrX0fWTFQMs6H5KQxiUxU
ENV TENANT_FEATURE_FLAGS=*:LICENSING,*:USER_GROUPS,*:ONBOARDING_TOUR
ENV ACCOUNT_PORTAL_URL=https://account.budibase.app
CMD ["./docker_run.sh"]

View File

@ -48,6 +48,7 @@
"aws-sdk": "2.1030.0", "aws-sdk": "2.1030.0",
"bcrypt": "5.1.0", "bcrypt": "5.1.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bull": "4.10.1",
"dd-trace": "3.13.2", "dd-trace": "3.13.2",
"dotenv": "8.6.0", "dotenv": "8.6.0",
"global-agent": "3.0.0", "global-agent": "3.0.0",
@ -61,6 +62,7 @@
"koa-session": "5.13.1", "koa-session": "5.13.1",
"koa-static": "5.0.0", "koa-static": "5.0.0",
"koa-useragent": "^4.1.0", "koa-useragent": "^4.1.0",
"lodash": "4.17.21",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"nodemailer": "6.7.2", "nodemailer": "6.7.2",
"passport-google-oauth": "2.0.0", "passport-google-oauth": "2.0.0",
@ -77,14 +79,13 @@
"@types/jsonwebtoken": "8.5.1", "@types/jsonwebtoken": "8.5.1",
"@types/koa": "2.13.4", "@types/koa": "2.13.4",
"@types/koa__router": "8.0.8", "@types/koa__router": "8.0.8",
"@types/lodash": "^4.14.191", "@types/lodash": "4.14.200",
"@types/node": "18.17.0", "@types/node": "18.17.0",
"@types/node-fetch": "2.6.4", "@types/node-fetch": "2.6.4",
"@types/server-destroy": "1.0.1", "@types/server-destroy": "1.0.1",
"@types/supertest": "2.0.12", "@types/supertest": "2.0.12",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"jest": "29.6.2", "jest": "29.6.2",
"lodash": "4.17.21",
"nodemon": "2.0.15", "nodemon": "2.0.15",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"supertest": "6.2.2", "supertest": "6.2.2",

View File

@ -1,31 +1,16 @@
#!/bin/bash #!/bin/sh
packages_to_remove=( packages_to_remove="@budibase/backend-core @budibase/bbui @budibase/builder @budibase/cli @budibase/client @budibase/frontend-core @budibase/pro @budibase/sdk @budibase/server @budibase/shared-core @budibase/string-templates @budibase/types @budibase/worker"
@budibase/backend-core
@budibase/bbui
@budibase/builder
@budibase/cli
@budibase/client
@budibase/frontend-core
@budibase/pro
@budibase/sdk
@budibase/server
@budibase/shared-core
# We cannot remove string-templates yet because it cannot be bundled by esbuild as a dependency
@budibase/string-templates
@budibase/types
@budibase/worker
)
package_json_path="$1"
root_package_json=$(cat "package.json") package_json=$(cat "$package_json_path")
process_package() { process_package() {
local pkg="$1" pkg_path="$1"
local package_json=$(cat "$pkg/package.json") package_json=$(cat "$pkg_path")
local has_changes=false has_changes=false
for package_name in "${packages_to_remove[@]}"; do for package_name in $packages_to_remove; do
if echo "$package_json" | jq -e --arg package_name "$package_name" '.dependencies | has($package_name)' > /dev/null; then if echo "$package_json" | jq -e --arg package_name "$package_name" '.dependencies | has($package_name)' > /dev/null; then
package_json=$(echo "$package_json" | jq "del(.dependencies[\"$package_name\"])") package_json=$(echo "$package_json" | jq "del(.dependencies[\"$package_name\"])")
has_changes=true has_changes=true
@ -33,20 +18,11 @@ process_package() {
done done
if [ "$has_changes" = true ]; then if [ "$has_changes" = true ]; then
echo "$package_json" > "$1/package.json" echo "$package_json" > "$pkg_path"
fi fi
} }
process_package "$package_json_path"
for pkg in $(echo "$root_package_json" | jq -r '.workspaces.packages[]' ); do package_json=$(cat "$package_json_path")
if [[ "$pkg" == *"*"* ]]; then echo "$package_json" | jq "del(.resolutions)" > "$1"
# Use find to iterate through immediate subdirectories
find "$pkg" -maxdepth 1 -type d -print | while read -r workspace_package; do
process_package "$workspace_package"
done
else
process_package "$pkg"
fi
done
echo "$root_package_json" | jq "del(.resolutions)" > "package.json"

View File

@ -5595,15 +5595,10 @@
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b" resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.3.tgz#15a0712296c5041733c79efe233ba17ae5a7587b"
integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g== integrity sha512-pTjcqY9E4nOI55Wgpz7eiI8+LzdYnw3qxXCfHyBDdPbYvbyLgWLJGh8EdPvqawwMK1Uo1794AUkkR38Fr0g+2g==
"@types/lodash@4.14.180": "@types/lodash@4.14.200":
version "4.14.180" version "4.14.200"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149"
integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g== integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==
"@types/lodash@^4.14.191":
version "4.14.192"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285"
integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==
"@types/long@^4.0.0", "@types/long@^4.0.1": "@types/long@^4.0.0", "@types/long@^4.0.1":
version "4.0.2" version "4.0.2"
@ -13835,10 +13830,10 @@ jsonschema@1.4.0:
resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2" resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.0.tgz#1afa34c4bc22190d8e42271ec17ac8b3404f87b2"
integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw== integrity sha512-/YgW6pRMr6M7C+4o8kS+B/2myEpHCrxO4PEWnqJNBFMjn7EWXqlQ4tGwL6xTHeRplwuZmcAncdvfOad1nT2yMw==
jsonwebtoken@8.5.1: jsonwebtoken@9.0.2:
version "8.5.1" version "9.0.2"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
dependencies: dependencies:
jws "^3.2.2" jws "^3.2.2"
lodash.includes "^4.3.0" lodash.includes "^4.3.0"
@ -13849,9 +13844,9 @@ jsonwebtoken@8.5.1:
lodash.isstring "^4.0.1" lodash.isstring "^4.0.1"
lodash.once "^4.0.0" lodash.once "^4.0.0"
ms "^2.1.1" ms "^2.1.1"
semver "^5.6.0" semver "^7.5.4"
jsonwebtoken@9.0.0, jsonwebtoken@^9.0.0: jsonwebtoken@^9.0.0:
version "9.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d"
integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==
@ -14799,7 +14794,7 @@ lodash.xor@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.xor/-/lodash.xor-4.5.0.tgz#4d48ed7e98095b0632582ba714d3ff8ae8fb1db6" resolved "https://registry.yarnpkg.com/lodash.xor/-/lodash.xor-4.5.0.tgz#4d48ed7e98095b0632582ba714d3ff8ae8fb1db6"
integrity sha512-sVN2zimthq7aZ5sPGXnSz32rZPuqcparVW50chJQe+mzTYV+IsxSsl/2gnkWWE2Of7K3myBQBqtLKOUEHJKRsQ== integrity sha512-sVN2zimthq7aZ5sPGXnSz32rZPuqcparVW50chJQe+mzTYV+IsxSsl/2gnkWWE2Of7K3myBQBqtLKOUEHJKRsQ==
lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0: lodash@4.17.21, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.7.0:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==