Merge branch 'develop' of github.com:Budibase/budibase into feature/custom-role-readable-ids
This commit is contained in:
commit
b94801d10a
|
@ -1,5 +1,9 @@
|
|||
name: Budibase CI
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
# Trigger the workflow on push or pull request,
|
||||
# but only for the master branch
|
||||
|
@ -23,6 +27,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
- name: Use Node.js 14.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
|
@ -135,15 +142,39 @@ jobs:
|
|||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
- name: Check submodule
|
||||
|
||||
- name: Check pro commit
|
||||
id: get_pro_commits
|
||||
run: |
|
||||
cd packages/pro
|
||||
git fetch
|
||||
if ! git merge-base --is-ancestor $(git log -n 1 --pretty=format:%H) origin/develop; then
|
||||
echo "Current commit has not been merged to develop"
|
||||
echo "Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md"
|
||||
exit 1
|
||||
pro_commit=$(git rev-parse HEAD)
|
||||
|
||||
branch=${{ github.base_ref || github.ref_name }}
|
||||
echo "Running on branch `$branch` (base_ref=${{ github.base_ref }}, ref_name=${{ github.head_ref }})"
|
||||
|
||||
if [[ "$branch" == "master" ]]; then
|
||||
base_commit=$(git rev-parse origin/master)
|
||||
else
|
||||
echo "All good, the submodule had been merged!"
|
||||
base_commit=$(git rev-parse origin/develop)
|
||||
fi
|
||||
|
||||
echo "pro_commit=$pro_commit"
|
||||
echo "pro_commit=$pro_commit" >> "$GITHUB_OUTPUT"
|
||||
echo "base_commit=$base_commit"
|
||||
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check submodule merged to develop
|
||||
uses: actions/github-script@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const submoduleCommit = '${{ steps.get_pro_commits.outputs.pro_commit }}';
|
||||
const baseCommit = '${{ steps.get_pro_commits.outputs.base_commit }}';
|
||||
|
||||
if (submoduleCommit !== baseCommit) {
|
||||
console.error('Submodule commit does not match the latest commit on the develop branch.');
|
||||
console.error('Refer to the pro repo to merge your changes: https://github.com/Budibase/budibase-pro/blob/develop/docs/getting_started.md')
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All good, the submodule had been merged and setup correctly!')
|
||||
}
|
||||
|
|
18
lerna.json
18
lerna.json
|
@ -1,22 +1,10 @@
|
|||
{
|
||||
"version": "2.7.26-alpha.2",
|
||||
"version": "2.7.34-alpha.6",
|
||||
"npmClient": "yarn",
|
||||
"useNx": true,
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
"packages/bbui",
|
||||
"packages/builder",
|
||||
"packages/cli",
|
||||
"packages/client",
|
||||
"packages/frontend-core",
|
||||
"packages/sdk",
|
||||
"packages/server",
|
||||
"packages/shared-core",
|
||||
"packages/string-templates",
|
||||
"packages/types",
|
||||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
"packages/*"
|
||||
],
|
||||
"useNx": true,
|
||||
"command": {
|
||||
"publish": {
|
||||
"ignoreChanges": [
|
||||
|
|
14
package.json
14
package.json
|
@ -95,19 +95,7 @@
|
|||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/backend-core",
|
||||
"packages/bbui",
|
||||
"packages/builder",
|
||||
"packages/cli",
|
||||
"packages/client",
|
||||
"packages/frontend-core",
|
||||
"packages/sdk",
|
||||
"packages/server",
|
||||
"packages/shared-core",
|
||||
"packages/string-templates",
|
||||
"packages/types",
|
||||
"packages/worker",
|
||||
"packages/pro/packages/pro"
|
||||
"packages/*"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: updateSelected(selectedBooleans)
|
||||
$: dispatch("change", selected)
|
||||
$: allSelected = selected?.length === options.length
|
||||
$: noneSelected = !selected?.length
|
||||
|
||||
|
@ -28,6 +27,7 @@
|
|||
}
|
||||
}
|
||||
selected = array
|
||||
dispatch("change", selected)
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
|
@ -36,6 +36,7 @@
|
|||
} else {
|
||||
selectedBooleans = reset()
|
||||
}
|
||||
dispatch("change", selected)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
border-color: var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
/* Toolbar button color */
|
||||
:global(.EasyMDEContainer .editor-toolbar button i) {
|
||||
:global(.EasyMDEContainer .editor-toolbar button) {
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
}
|
||||
/* Separator between toolbar buttons*/
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"dev:builder": "routify -c dev:vite",
|
||||
"dev:vite": "vite --host 0.0.0.0",
|
||||
"rollup": "rollup -c -w",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { datasources, tables } from "stores/backend"
|
||||
import EditRolesButton from "./buttons/EditRolesButton.svelte"
|
||||
import { TableNames } from "constants"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
@ -26,19 +26,33 @@
|
|||
$: id = $tables.selected?._id
|
||||
$: isUsersTable = id === TableNames.USERS
|
||||
$: isInternal = $tables.selected?.type !== "external"
|
||||
|
||||
const handleGridTableUpdate = async e => {
|
||||
tables.replaceTable(id, e.detail)
|
||||
|
||||
// We need to refresh datasources when an external table changes.
|
||||
// Type "external" may exist - sometimes type is "table" and sometimes it
|
||||
// is "external" - it has different meanings in different endpoints.
|
||||
// If we check both these then we hopefully catch all external tables.
|
||||
if (e.detail?.type === "external" || e.detail?.sql) {
|
||||
await datasources.fetch()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
tableType={$tables.selected?.type}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
on:updatetable={e => tables.replaceTable(id, e.detail)}
|
||||
on:updatetable={handleGridTableUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
<GridCreateViewButton />
|
||||
|
@ -53,7 +67,6 @@
|
|||
<GridImportButton />
|
||||
{/if}
|
||||
<GridExportButton />
|
||||
<GridFilterButton />
|
||||
<GridAddColumnModal />
|
||||
<GridEditColumnModal />
|
||||
{#if isUsersTable}
|
||||
|
|
|
@ -14,6 +14,12 @@
|
|||
|
||||
$: tempValue = filters || []
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(filters)
|
||||
|
||||
const getText = filters => {
|
||||
const count = filters?.length
|
||||
return count ? `Filter (${count})` : "Filter"
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
|
@ -23,7 +29,7 @@
|
|||
on:click={modal.show}
|
||||
selected={tempValue?.length > 0}
|
||||
>
|
||||
Filter
|
||||
{text}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
const { columns, tableId, filter, table } = getContext("grid")
|
||||
|
||||
// Wipe filter whenever table ID changes to avoid using stale filters
|
||||
$: $tableId, filter.set([])
|
||||
|
||||
const onFilter = e => {
|
||||
filter.set(e.detail || [])
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId, tableType } = getContext("grid")
|
||||
const { rows, tableId, table } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
{tableType}
|
||||
tableType={$table?.type}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
export let tableId
|
||||
export let tableType
|
||||
|
||||
let rows = []
|
||||
let allValid = false
|
||||
let displayColumn = null
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Heading, Detail } from "@budibase/bbui"
|
||||
import IntegrationIcon from "../IntegrationIcon.svelte"
|
||||
|
||||
export let integration
|
||||
export let integrationType
|
||||
export let schema
|
||||
|
||||
let dispatcher = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:selected={integration.type === integrationType}
|
||||
on:click={() => dispatcher("selected", integrationType)}
|
||||
class="item hoverable"
|
||||
>
|
||||
<div class="item-body" class:with-type={!!schema.type}>
|
||||
<IntegrationIcon {integrationType} {schema} size="25" />
|
||||
<div class="text">
|
||||
<Heading size="XXS">{schema.friendlyName}</Heading>
|
||||
{#if schema.type}
|
||||
<Detail size="S">{schema.type || ""}</Detail>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.item {
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s)
|
||||
var(--spectrum-alias-item-padding-m);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: background 0.13s ease-out;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
border-width: 2px;
|
||||
}
|
||||
.item:hover,
|
||||
.item.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.item-body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.item-body.with-type {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.item-body.with-type :global(svg) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
|
@ -1,145 +0,0 @@
|
|||
<script>
|
||||
export let width = 100
|
||||
export let height = 100
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
viewBox="0 0 46 46"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
|
||||
>
|
||||
<!-- Generator: Sketch 3.3.3 (12081) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>btn_google_dark_normal_ios</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<filter
|
||||
x="-50%"
|
||||
y="-50%"
|
||||
width="200%"
|
||||
height="200%"
|
||||
filterUnits="objectBoundingBox"
|
||||
id="filter-1"
|
||||
>
|
||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
in="shadowOffsetOuter1"
|
||||
result="shadowBlurOuter1"
|
||||
/>
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.168 0"
|
||||
in="shadowBlurOuter1"
|
||||
type="matrix"
|
||||
result="shadowMatrixOuter1"
|
||||
/>
|
||||
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter2" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
in="shadowOffsetOuter2"
|
||||
result="shadowBlurOuter2"
|
||||
/>
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.084 0"
|
||||
in="shadowBlurOuter2"
|
||||
type="matrix"
|
||||
result="shadowMatrixOuter2"
|
||||
/>
|
||||
<feMerge>
|
||||
<feMergeNode in="shadowMatrixOuter1" />
|
||||
<feMergeNode in="shadowMatrixOuter2" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<rect id="path-2" x="0" y="0" width="40" height="40" rx="2" />
|
||||
<rect id="path-3" x="5" y="5" width="38" height="38" rx="1" />
|
||||
</defs>
|
||||
<g
|
||||
id="Google-Button"
|
||||
stroke="none"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSPage"
|
||||
>
|
||||
<g
|
||||
id="9-PATCH"
|
||||
sketch:type="MSArtboardGroup"
|
||||
transform="translate(-608.000000, -219.000000)"
|
||||
/>
|
||||
<g
|
||||
id="btn_google_dark_normal"
|
||||
sketch:type="MSArtboardGroup"
|
||||
transform="translate(-1.000000, -1.000000)"
|
||||
>
|
||||
<g
|
||||
id="button"
|
||||
sketch:type="MSLayerGroup"
|
||||
transform="translate(4.000000, 4.000000)"
|
||||
filter="url(#filter-1)"
|
||||
>
|
||||
<g id="button-bg">
|
||||
<use
|
||||
fill="#4285F4"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSShapeGroup"
|
||||
xlink:href="#path-2"
|
||||
/>
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
<use fill="none" xlink:href="#path-2" />
|
||||
</g>
|
||||
</g>
|
||||
<g id="button-bg-copy">
|
||||
<use
|
||||
fill="#FFFFFF"
|
||||
fill-rule="evenodd"
|
||||
sketch:type="MSShapeGroup"
|
||||
xlink:href="#path-3"
|
||||
/>
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
<use fill="none" xlink:href="#path-3" />
|
||||
</g>
|
||||
<g
|
||||
id="logo_googleg_48dp"
|
||||
sketch:type="MSLayerGroup"
|
||||
transform="translate(15.000000, 15.000000)"
|
||||
>
|
||||
<path
|
||||
d="M17.64,9.20454545 C17.64,8.56636364 17.5827273,7.95272727 17.4763636,7.36363636 L9,7.36363636 L9,10.845 L13.8436364,10.845 C13.635,11.97 13.0009091,12.9231818 12.0477273,13.5613636 L12.0477273,15.8195455 L14.9563636,15.8195455 C16.6581818,14.2527273 17.64,11.9454545 17.64,9.20454545 L17.64,9.20454545 Z"
|
||||
id="Shape"
|
||||
fill="#4285F4"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M9,18 C11.43,18 13.4672727,17.1940909 14.9563636,15.8195455 L12.0477273,13.5613636 C11.2418182,14.1013636 10.2109091,14.4204545 9,14.4204545 C6.65590909,14.4204545 4.67181818,12.8372727 3.96409091,10.71 L0.957272727,10.71 L0.957272727,13.0418182 C2.43818182,15.9831818 5.48181818,18 9,18 L9,18 Z"
|
||||
id="Shape"
|
||||
fill="#34A853"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M3.96409091,10.71 C3.78409091,10.17 3.68181818,9.59318182 3.68181818,9 C3.68181818,8.40681818 3.78409091,7.83 3.96409091,7.29 L3.96409091,4.95818182 L0.957272727,4.95818182 C0.347727273,6.17318182 0,7.54772727 0,9 C0,10.4522727 0.347727273,11.8268182 0.957272727,13.0418182 L3.96409091,10.71 L3.96409091,10.71 Z"
|
||||
id="Shape"
|
||||
fill="#FBBC05"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M9,3.57954545 C10.3213636,3.57954545 11.5077273,4.03363636 12.4404545,4.92545455 L15.0218182,2.34409091 C13.4631818,0.891818182 11.4259091,0 9,0 C5.48181818,0 2.43818182,2.01681818 0.957272727,4.95818182 L3.96409091,7.29 C4.67181818,5.16272727 6.65590909,3.57954545 9,3.57954545 L9,3.57954545 Z"
|
||||
id="Shape"
|
||||
fill="#EA4335"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
<path
|
||||
d="M0,0 L18,0 L18,18 L0,18 L0,0 Z"
|
||||
id="Shape"
|
||||
sketch:type="MSShapeGroup"
|
||||
/>
|
||||
</g>
|
||||
<g id="handles_square" sketch:type="MSLayerGroup" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
|
@ -1,207 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Body,
|
||||
FancyCheckboxGroup,
|
||||
InlineAlert,
|
||||
Layout,
|
||||
Link,
|
||||
ModalContent,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { IntegrationNames, IntegrationTypes } from "constants/backend"
|
||||
import GoogleButton from "../_components/GoogleButton.svelte"
|
||||
import { organisation } from "stores/portal"
|
||||
import { onDestroy, onMount } from "svelte"
|
||||
import {
|
||||
getDatasourceInfo,
|
||||
saveDatasource,
|
||||
validateDatasourceConfig,
|
||||
} from "builderStore/datasource"
|
||||
import cloneDeep from "lodash/cloneDeepWith"
|
||||
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import { API } from "api"
|
||||
|
||||
export let integration
|
||||
export let continueSetupId = false
|
||||
|
||||
let datasource = cloneDeep(integration)
|
||||
datasource.config.continueSetupId = continueSetupId
|
||||
|
||||
let { schema } = datasource
|
||||
|
||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||
|
||||
onMount(async () => {
|
||||
await organisation.init()
|
||||
})
|
||||
|
||||
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
|
||||
|
||||
export const GoogleDatasouceConfigStep = {
|
||||
AUTH: "auth",
|
||||
SET_URL: "set_url",
|
||||
SET_SHEETS: "set_sheets",
|
||||
}
|
||||
|
||||
let step = continueSetupId
|
||||
? GoogleDatasouceConfigStep.SET_URL
|
||||
: GoogleDatasouceConfigStep.AUTH
|
||||
|
||||
let isValid = false
|
||||
|
||||
let allSheets
|
||||
let selectedSheets
|
||||
let setSheetsErrorTitle, setSheetsErrorMessage
|
||||
|
||||
$: modalConfig = {
|
||||
[GoogleDatasouceConfigStep.AUTH]: {
|
||||
title: `Connect to ${integrationName}`,
|
||||
},
|
||||
[GoogleDatasouceConfigStep.SET_URL]: {
|
||||
title: `Connect your spreadsheet`,
|
||||
confirmButtonText: "Connect",
|
||||
onConfirm: async () => {
|
||||
const checkConnection =
|
||||
integration.features[DatasourceFeature.CONNECTION_CHECKING]
|
||||
if (checkConnection) {
|
||||
const resp = await validateDatasourceConfig(datasource)
|
||||
if (!resp.connected) {
|
||||
notifications.error(`Unable to connect - ${resp.error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
datasource = await saveDatasource(datasource, {
|
||||
tablesFilter: selectedSheets,
|
||||
skipFetch: true,
|
||||
})
|
||||
} catch (err) {
|
||||
notifications.error(err?.message ?? "Error saving datasource")
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
|
||||
if (!integration.features[DatasourceFeature.FETCH_TABLE_NAMES]) {
|
||||
notifications.success(`Datasource created successfully.`)
|
||||
return
|
||||
}
|
||||
|
||||
const info = await getDatasourceInfo(datasource)
|
||||
allSheets = info.tableNames
|
||||
|
||||
step = GoogleDatasouceConfigStep.SET_SHEETS
|
||||
notifications.success(
|
||||
checkConnection
|
||||
? "Connection Successful"
|
||||
: `Datasource created successfully.`
|
||||
)
|
||||
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
},
|
||||
},
|
||||
[GoogleDatasouceConfigStep.SET_SHEETS]: {
|
||||
title: `Choose your sheets`,
|
||||
confirmButtonText: selectedSheets?.length
|
||||
? "Fetch sheets"
|
||||
: "Continue without fetching",
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
if (selectedSheets.length) {
|
||||
await API.buildDatasourceSchema({
|
||||
datasourceId: datasource._id,
|
||||
tablesFilter: selectedSheets,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
} catch (err) {
|
||||
const message = err?.message ?? "Error fetching the sheets"
|
||||
// Handling message with format: Error title - error description
|
||||
const indexSeparator = message.indexOf(" - ")
|
||||
if (indexSeparator >= 0) {
|
||||
setSheetsErrorTitle = message.substr(0, indexSeparator)
|
||||
setSheetsErrorMessage =
|
||||
message[indexSeparator + 3].toUpperCase() +
|
||||
message.substr(indexSeparator + 4)
|
||||
} else {
|
||||
setSheetsErrorTitle = null
|
||||
setSheetsErrorMessage = message
|
||||
}
|
||||
|
||||
// prevent the modal from closing
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// This will handle the user closing the modal pressing outside the modal
|
||||
onDestroy(() => {
|
||||
if (step === GoogleDatasouceConfigStep.SET_SHEETS) {
|
||||
$goto(`./datasource/${datasource._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={modalConfig[step].title}
|
||||
cancelText="Cancel"
|
||||
size="L"
|
||||
confirmText={modalConfig[step].confirmButtonText}
|
||||
showConfirmButton={!!modalConfig[step].onConfirm}
|
||||
onConfirm={modalConfig[step].onConfirm}
|
||||
disabled={!isValid}
|
||||
>
|
||||
{#if step === GoogleDatasouceConfigStep.AUTH}
|
||||
<!-- check true and false directly, don't render until flag is set -->
|
||||
{#if isGoogleConfigured === true}
|
||||
<Layout noPadding>
|
||||
<Body size="S"
|
||||
>Authenticate with your google account to use the {integrationName} integration.</Body
|
||||
>
|
||||
</Layout>
|
||||
<GoogleButton samePage />
|
||||
{:else if isGoogleConfigured === false}
|
||||
<Body size="S"
|
||||
>Google authentication is not enabled, please complete Google SSO
|
||||
configuration.</Body
|
||||
>
|
||||
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if step === GoogleDatasouceConfigStep.SET_URL}
|
||||
<Layout noPadding no>
|
||||
<Body size="S">Add the URL of the sheet you want to connect.</Body>
|
||||
|
||||
<IntegrationConfigForm
|
||||
{schema}
|
||||
bind:datasource
|
||||
creating={true}
|
||||
on:valid={e => (isValid = e.detail)}
|
||||
/>
|
||||
</Layout>
|
||||
{/if}
|
||||
{#if step === GoogleDatasouceConfigStep.SET_SHEETS}
|
||||
<Layout noPadding no>
|
||||
<Body size="S">Select which spreadsheets you want to connect.</Body>
|
||||
|
||||
<FancyCheckboxGroup
|
||||
options={allSheets}
|
||||
bind:selected={selectedSheets}
|
||||
selectAllText="Select all sheets"
|
||||
/>
|
||||
|
||||
{#if setSheetsErrorTitle || setSheetsErrorMessage}
|
||||
<InlineAlert
|
||||
type="error"
|
||||
header={setSheetsErrorTitle}
|
||||
message={setSheetsErrorMessage}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
</ModalContent>
|
|
@ -59,7 +59,6 @@
|
|||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
|
||||
$: toRelationship.relationshipType = fromRelationship?.relationshipType
|
||||
|
||||
function getTable(id) {
|
||||
return plusTables.find(table => table._id === id)
|
||||
|
@ -180,6 +179,16 @@
|
|||
return getErrorCount(errors) === 0
|
||||
}
|
||||
|
||||
function otherRelationshipType(type) {
|
||||
if (type === RelationshipTypes.MANY_TO_ONE) {
|
||||
return RelationshipTypes.ONE_TO_MANY
|
||||
} else if (type === RelationshipTypes.ONE_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_ONE
|
||||
} else if (type === RelationshipTypes.MANY_TO_MANY) {
|
||||
return RelationshipTypes.MANY_TO_MANY
|
||||
}
|
||||
}
|
||||
|
||||
function buildRelationships() {
|
||||
const id = Helpers.uuid()
|
||||
//Map temporary variables
|
||||
|
@ -200,6 +209,7 @@
|
|||
...toRelationship,
|
||||
tableId: fromId,
|
||||
name: fromColumn,
|
||||
relationshipType: otherRelationshipType(relationshipType),
|
||||
through: throughId,
|
||||
type: "link",
|
||||
_id: id,
|
||||
|
|
|
@ -93,6 +93,7 @@
|
|||
try {
|
||||
await beforeSave()
|
||||
table = await tables.save(newTable)
|
||||
await datasources.fetch()
|
||||
await afterSave(table)
|
||||
} catch (e) {
|
||||
notifications.error(e)
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
const updatedTable = cloneDeep(table)
|
||||
updatedTable.name = updatedName
|
||||
await tables.save(updatedTable)
|
||||
await datasources.fetch()
|
||||
notifications.success("Table renamed successfully")
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,18 @@
|
|||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
faCircleInfo,
|
||||
faBold,
|
||||
faItalic,
|
||||
faHeading,
|
||||
faQuoteLeft,
|
||||
faListUl,
|
||||
faListOl,
|
||||
faLink,
|
||||
faImage,
|
||||
faEye,
|
||||
faColumns,
|
||||
faArrowsAlt,
|
||||
faQuestionCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
|
||||
|
||||
|
@ -22,7 +34,22 @@
|
|||
faEnvelope,
|
||||
faFileArrowUp,
|
||||
faChevronLeft,
|
||||
faCircleInfo
|
||||
faCircleInfo,
|
||||
|
||||
// -- Required for easyMDE use in the builder.
|
||||
faBold,
|
||||
faItalic,
|
||||
faHeading,
|
||||
faQuoteLeft,
|
||||
faListUl,
|
||||
faListOl,
|
||||
faLink,
|
||||
faImage,
|
||||
faEye,
|
||||
faColumns,
|
||||
faArrowsAlt,
|
||||
faQuestionCircle
|
||||
// --
|
||||
)
|
||||
dom.watch()
|
||||
</script>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
export let allowHelpers = true
|
||||
export let updateOnChange = true
|
||||
export let drawerLeft
|
||||
export let key
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { currentAsset, store } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { Label, Combobox, Select } from "@budibase/bbui"
|
||||
import {
|
||||
getActionProviderComponents,
|
||||
buildFormSchema,
|
||||
} from "builderStore/dataBinding"
|
||||
import { findComponent } from "builderStore/componentUtils"
|
||||
|
||||
export let parameters
|
||||
|
||||
onMount(() => {
|
||||
if (!parameters.type) {
|
||||
parameters.type = "top"
|
||||
}
|
||||
})
|
||||
|
||||
$: formComponent = findComponent($currentAsset.props, parameters.componentId)
|
||||
$: formSchema = buildFormSchema(formComponent)
|
||||
$: fieldOptions = Object.keys(formSchema || {})
|
||||
$: actionProviders = getActionProviderComponents(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId,
|
||||
"ScrollTo"
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label small>Form</Label>
|
||||
<Select
|
||||
bind:value={parameters.componentId}
|
||||
options={actionProviders}
|
||||
getOptionLabel={x => x._instanceName}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
<Label small>Field</Label>
|
||||
<Combobox bind:value={parameters.field} options={fieldOptions} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
grid-template-columns: auto;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
|
@ -16,6 +16,7 @@ export { default as S3Upload } from "./S3Upload.svelte"
|
|||
export { default as ExportData } from "./ExportData.svelte"
|
||||
export { default as ContinueIf } from "./ContinueIf.svelte"
|
||||
export { default as UpdateFieldValue } from "./UpdateFieldValue.svelte"
|
||||
export { default as ScrollTo } from "./ScrollTo.svelte"
|
||||
export { default as ShowNotification } from "./ShowNotification.svelte"
|
||||
export { default as PromptUser } from "./PromptUser.svelte"
|
||||
export { default as OpenSidePanel } from "./OpenSidePanel.svelte"
|
||||
|
|
|
@ -70,6 +70,11 @@
|
|||
"type": "form",
|
||||
"component": "UpdateFieldValue"
|
||||
},
|
||||
{
|
||||
"name": "Scroll To Field",
|
||||
"type": "form",
|
||||
"component": "ScrollTo"
|
||||
},
|
||||
{
|
||||
"name": "Validate Form",
|
||||
"type": "form",
|
||||
|
|
|
@ -2,9 +2,4 @@
|
|||
import ColumnEditor from "./ColumnEditor.svelte"
|
||||
</script>
|
||||
|
||||
<ColumnEditor
|
||||
{...$$props}
|
||||
on:change
|
||||
allowCellEditing={false}
|
||||
subject="Dynamic Filter"
|
||||
/>
|
||||
<ColumnEditor {...$$props} on:change allowCellEditing={false} />
|
||||
|
|
|
@ -142,10 +142,10 @@
|
|||
<div class="column">
|
||||
<div class="wide">
|
||||
<Body size="S">
|
||||
By default, all table columns will automatically be shown.
|
||||
By default, all columns will automatically be shown.
|
||||
<br />
|
||||
You can manually control which columns are included in your table,
|
||||
and their appearance, by adding them below.
|
||||
You can manually control which columns are included by adding them
|
||||
below.
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
export let componentInstance
|
||||
export let value = []
|
||||
export let allowCellEditing = true
|
||||
export let subject = "Table"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -75,11 +74,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||
<Drawer bind:this={drawer} title="{subject} Columns">
|
||||
<svelte:fragment slot="description">
|
||||
Configure the columns in your {subject.toLowerCase()}.
|
||||
</svelte:fragment>
|
||||
<div class="column-editor">
|
||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||
</div>
|
||||
<Drawer bind:this={drawer} title="Columns">
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<ColumnDrawer
|
||||
slot="body"
|
||||
|
@ -89,3 +87,9 @@
|
|||
{allowCellEditing}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.column-editor :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { Button, ActionButton, Drawer } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ColumnDrawer from "./ColumnDrawer.svelte"
|
||||
import ColumnDrawer from "./ColumnEditor/ColumnDrawer.svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import {
|
||||
getDatasourceForProvider,
|
||||
|
|
|
@ -20,15 +20,26 @@
|
|||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(value)
|
||||
|
||||
async function saveFilter() {
|
||||
dispatch("change", tempValue)
|
||||
notifications.success("Filters saved")
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
const getText = filters => {
|
||||
if (!filters?.length) {
|
||||
return "No filters set"
|
||||
} else {
|
||||
return `${filters.length} filter${filters.length === 1 ? "" : "s"} set`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
||||
<div class="filter-editor">
|
||||
<ActionButton on:click={drawer.show}>{text}</ActionButton>
|
||||
</div>
|
||||
<Drawer bind:this={drawer} title="Filtering">
|
||||
<Button cta slot="buttons" on:click={saveFilter}>Save</Button>
|
||||
<FilterDrawer
|
||||
|
@ -40,3 +51,9 @@
|
|||
on:change={e => (tempValue = e.detail)}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.filter-editor :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
import ObjectField from "./fields/Object.svelte"
|
||||
import BooleanField from "./fields/Boolean.svelte"
|
||||
import LongFormField from "./fields/LongForm.svelte"
|
||||
import FieldGroupField from "./fields/FieldGroup.svelte"
|
||||
import StringField from "./fields/String.svelte"
|
||||
|
||||
export let type
|
||||
export let value
|
||||
export let error
|
||||
export let name
|
||||
export let showModal = () => {}
|
||||
|
||||
const selectComponent = type => {
|
||||
if (type === "object") {
|
||||
return ObjectField
|
||||
} else if (type === "boolean") {
|
||||
return BooleanField
|
||||
} else if (type === "longForm") {
|
||||
return LongFormField
|
||||
} else if (type === "fieldGroup") {
|
||||
return FieldGroupField
|
||||
} else {
|
||||
return StringField
|
||||
}
|
||||
}
|
||||
|
||||
$: component = selectComponent(type)
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
this={component}
|
||||
{type}
|
||||
{value}
|
||||
{error}
|
||||
{name}
|
||||
{showModal}
|
||||
on:blur
|
||||
on:change
|
||||
/>
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { Label, Toggle } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let name
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<Toggle on:blur on:change text="" {value} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Label, Input, Layout, Accordion } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
export let name
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
const handleChange = (updatedFieldKey, updatedFieldValue) => {
|
||||
const updatedValue = value.map(field => {
|
||||
return {
|
||||
key: field.key,
|
||||
value: field.key === updatedFieldKey ? updatedFieldValue : field.value,
|
||||
}
|
||||
})
|
||||
|
||||
dispatch("change", updatedValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion
|
||||
initialOpen={Object.values(value).some(properties => !!properties.value)}
|
||||
header={name}
|
||||
>
|
||||
<Layout gap="S">
|
||||
{#each value as field}
|
||||
<div class="form-row">
|
||||
<Label>{field.name}</Label>
|
||||
<Input
|
||||
type={field.type}
|
||||
on:change={e => handleChange(field.key, e.detail)}
|
||||
value={field.value}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
</Accordion>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { Label, TextArea } from "@budibase/bbui"
|
||||
|
||||
export let type
|
||||
export let name
|
||||
export let value
|
||||
export let error
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<TextArea on:blur on:change {type} {value} {error} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import { Label, Button } from "@budibase/bbui"
|
||||
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
|
||||
|
||||
export let name
|
||||
export let value
|
||||
|
||||
let addButton
|
||||
</script>
|
||||
|
||||
<div class="form-row ssl">
|
||||
<Label>{name}</Label>
|
||||
<Button secondary thin outline on:click={addButton.addEntry()}>Add</Button>
|
||||
</div>
|
||||
<KeyValueBuilder
|
||||
on:change
|
||||
on:blur
|
||||
bind:this={addButton}
|
||||
defaults={value}
|
||||
noAddButton={true}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-row.ssl {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20%;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { Label, EnvDropdown } from "@budibase/bbui"
|
||||
import { environment, licensing } from "stores/portal"
|
||||
|
||||
export let type
|
||||
export let name
|
||||
export let value
|
||||
export let error
|
||||
export let showModal = () => {}
|
||||
|
||||
async function handleUpgradePanel() {
|
||||
await environment.upgradePanelOpened()
|
||||
$licensing.goToUpgradePage()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-row">
|
||||
<Label>{name}</Label>
|
||||
<EnvDropdown
|
||||
on:change
|
||||
on:blur
|
||||
type={type === "port" ? "string" : type}
|
||||
{value}
|
||||
{error}
|
||||
variables={$environment.variables}
|
||||
environmentVariablesEnabled={$licensing.environmentVariablesEnabled}
|
||||
{showModal}
|
||||
{handleUpgradePanel}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 1fr;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,114 @@
|
|||
<script>
|
||||
import {
|
||||
Modal,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import CreateEditVariableModal from "components/portal/environment/CreateEditVariableModal.svelte"
|
||||
import ConfigInput from "./ConfigInput.svelte"
|
||||
import { createConfigStore } from "./stores/config"
|
||||
import { createValidationStore } from "./stores/validation"
|
||||
import { createValidatedConfigStore } from "./stores/validatedConfig"
|
||||
import { datasources } from "stores/backend"
|
||||
import { get } from "svelte/store"
|
||||
import { environment } from "stores/portal"
|
||||
|
||||
export let integration
|
||||
export let config
|
||||
export let onDatasourceCreated = () => {}
|
||||
|
||||
$: configStore = createConfigStore(integration, config)
|
||||
$: validationStore = createValidationStore(integration)
|
||||
$: validatedConfigStore = createValidatedConfigStore(
|
||||
configStore,
|
||||
validationStore,
|
||||
integration
|
||||
)
|
||||
|
||||
const handleConfirm = async () => {
|
||||
validationStore.markAllFieldsActive()
|
||||
const config = get(configStore)
|
||||
|
||||
try {
|
||||
if (await validationStore.validate(config)) {
|
||||
const datasource = await datasources.create({
|
||||
integration,
|
||||
fields: config,
|
||||
})
|
||||
await onDatasourceCreated(datasource)
|
||||
} else {
|
||||
notifications.send("Invalid fields", {
|
||||
type: "error",
|
||||
icon: "Alert",
|
||||
autoDismiss: true,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing on errors, alerts are handled by `datasources.create`
|
||||
}
|
||||
|
||||
// Prevent modal closing
|
||||
return false
|
||||
}
|
||||
|
||||
const handleBlur = key => {
|
||||
validationStore.markFieldActive(key)
|
||||
validationStore.validate(get(configStore))
|
||||
}
|
||||
|
||||
const handleChange = (key, newValue) => {
|
||||
configStore.updateFieldValue(key, newValue)
|
||||
validationStore.validate(get(configStore))
|
||||
}
|
||||
|
||||
let createVariableModal
|
||||
let selectedConfigKey
|
||||
|
||||
const showModal = key => {
|
||||
selectedConfigKey = key
|
||||
createVariableModal.show()
|
||||
}
|
||||
|
||||
async function save(data) {
|
||||
try {
|
||||
await environment.createVariable(data)
|
||||
configStore.updateFieldValue(selectedConfigKey, `{{ env.${data.name} }}`)
|
||||
createVariableModal.hide()
|
||||
} catch (err) {
|
||||
notifications.error(`Failed to create variable: ${err.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={`Connect to ${integration.friendlyName}`}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText={integration.plus ? "Connect" : "Save and continue to query"}
|
||||
cancelText="Back"
|
||||
disabled={$validationStore.allFieldsActive && $validationStore.invalid}
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Body size="XS">
|
||||
Connect your database to Budibase using the config below.
|
||||
</Body>
|
||||
</Layout>
|
||||
|
||||
{#each $validatedConfigStore as { type, key, value, error, name }}
|
||||
<ConfigInput
|
||||
{type}
|
||||
{value}
|
||||
{error}
|
||||
{name}
|
||||
showModal={() => showModal(key)}
|
||||
on:blur={() => handleBlur(key)}
|
||||
on:change={e => handleChange(key, e.detail)}
|
||||
/>
|
||||
{/each}
|
||||
</ModalContent>
|
||||
|
||||
<Modal bind:this={createVariableModal}>
|
||||
<CreateEditVariableModal {save} />
|
||||
</Modal>
|
|
@ -0,0 +1,26 @@
|
|||
import { writable } from "svelte/store"
|
||||
|
||||
export const createConfigStore = (integration, config) => {
|
||||
const configStore = writable(config)
|
||||
|
||||
const updateFieldValue = (key, value) => {
|
||||
configStore.update($configStore => {
|
||||
const newStore = { ...$configStore }
|
||||
|
||||
if (integration.datasource[key].type === "fieldGroup") {
|
||||
value.forEach(field => {
|
||||
newStore[field.key] = field.value
|
||||
})
|
||||
} else {
|
||||
newStore[key] = value
|
||||
}
|
||||
|
||||
return newStore
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: configStore.subscribe,
|
||||
updateFieldValue,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { derived } from "svelte/store"
|
||||
|
||||
export const createValidatedConfigStore = (
|
||||
configStore,
|
||||
validationStore,
|
||||
integration
|
||||
) => {
|
||||
return derived(
|
||||
[configStore, validationStore],
|
||||
([$configStore, $validationStore]) => {
|
||||
return Object.entries(integration.datasource).map(([key, properties]) => {
|
||||
const getValue = () => {
|
||||
if (properties.type === "fieldGroup") {
|
||||
return Object.entries(properties.fields).map(
|
||||
([fieldKey, fieldProperties]) => {
|
||||
return {
|
||||
key: fieldKey,
|
||||
name: capitalise(fieldProperties.display || fieldKey),
|
||||
type: fieldProperties.type,
|
||||
value: $configStore[fieldKey],
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return $configStore[key]
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
value: getValue(),
|
||||
error: $validationStore.errors[key],
|
||||
name: capitalise(properties.display || key),
|
||||
type: properties.type,
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { object, string, number } from "yup"
|
||||
import { derived, writable, get } from "svelte/store"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
const propertyValidator = type => {
|
||||
if (type === "number") {
|
||||
return number().nullable()
|
||||
}
|
||||
|
||||
if (type === "email") {
|
||||
return string().email().nullable()
|
||||
}
|
||||
|
||||
return string().nullable()
|
||||
}
|
||||
|
||||
const getValidatorFields = integration => {
|
||||
const validatorFields = {}
|
||||
|
||||
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||
if (properties.required) {
|
||||
validatorFields[key] = propertyValidator(properties.type).required()
|
||||
} else {
|
||||
validatorFields[key] = propertyValidator(properties.type).notRequired()
|
||||
}
|
||||
})
|
||||
|
||||
return validatorFields
|
||||
}
|
||||
|
||||
export const createValidationStore = integration => {
|
||||
const allValidators = getValidatorFields(integration)
|
||||
const selectedValidatorsStore = writable({})
|
||||
const errorsStore = writable({})
|
||||
|
||||
const markAllFieldsActive = () => {
|
||||
selectedValidatorsStore.set(allValidators)
|
||||
}
|
||||
|
||||
const markFieldActive = key => {
|
||||
selectedValidatorsStore.update($validatorsStore => ({
|
||||
...$validatorsStore,
|
||||
[key]: allValidators[key],
|
||||
}))
|
||||
}
|
||||
|
||||
const validate = async config => {
|
||||
try {
|
||||
await object()
|
||||
.shape(get(selectedValidatorsStore))
|
||||
.validate(config, { abortEarly: false })
|
||||
|
||||
errorsStore.set({})
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
// Yup error
|
||||
if (error.inner) {
|
||||
const errors = {}
|
||||
|
||||
error.inner.forEach(innerError => {
|
||||
errors[innerError.path] = capitalise(innerError.message)
|
||||
})
|
||||
|
||||
errorsStore.set(errors)
|
||||
} else {
|
||||
// Non-yup error
|
||||
notifications.error("Unexpected validation error")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[errorsStore, selectedValidatorsStore],
|
||||
([$errorsStore, $selectedValidatorsStore]) => {
|
||||
return {
|
||||
errors: $errorsStore,
|
||||
invalid: Object.keys($errorsStore).length > 0,
|
||||
allFieldsActive:
|
||||
Object.keys($selectedValidatorsStore).length ===
|
||||
Object.keys(allValidators).length,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
markAllFieldsActive,
|
||||
markFieldActive,
|
||||
validate,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
|
||||
import { organisation } from "stores/portal"
|
||||
import GoogleButton from "./GoogleButton.svelte"
|
||||
|
||||
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
showConfirmButton={false}
|
||||
title={`Connect to Google Sheets`}
|
||||
cancelText="Cancel"
|
||||
size="L"
|
||||
>
|
||||
<!-- check true and false directly, don't render until flag is set -->
|
||||
{#if isGoogleConfigured === true}
|
||||
<Layout noPadding>
|
||||
<Body size="S"
|
||||
>Authenticate with your Google account to use the Google Sheets
|
||||
integration.</Body
|
||||
>
|
||||
</Layout>
|
||||
<GoogleButton samePage />
|
||||
{:else if isGoogleConfigured === false}
|
||||
<Body size="S"
|
||||
>Google authentication is not enabled, please complete Google SSO
|
||||
configuration.</Body
|
||||
>
|
||||
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
|
||||
{/if}
|
||||
</ModalContent>
|
|
@ -0,0 +1,62 @@
|
|||
<script>
|
||||
import {
|
||||
Body,
|
||||
FancyCheckboxGroup,
|
||||
InlineAlert,
|
||||
Layout,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import { createTableSelectionStore } from "./tableSelectionStore"
|
||||
|
||||
export let integration
|
||||
export let datasource
|
||||
export let onComplete = () => {}
|
||||
|
||||
$: store = createTableSelectionStore(integration, datasource)
|
||||
|
||||
$: isSheets = integration.name === IntegrationTypes.GOOGLE_SHEETS
|
||||
$: tableType = isSheets ? "sheets" : "tables"
|
||||
$: title = `Choose your ${tableType}`
|
||||
|
||||
$: confirmText = $store.hasSelected
|
||||
? `Fetch ${tableType}`
|
||||
: "Continue without fetching"
|
||||
|
||||
$: description = isSheets
|
||||
? "Select which spreadsheets you want to connect."
|
||||
: "Choose what tables you want to sync with Budibase"
|
||||
|
||||
$: selectAllText = isSheets ? "Select all sheets" : "Select all"
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
{title}
|
||||
cancelText="Cancel"
|
||||
size="L"
|
||||
{confirmText}
|
||||
onConfirm={() => store.importSelectedTables(onComplete)}
|
||||
disabled={$store.loading}
|
||||
>
|
||||
{#if $store.loading}
|
||||
<p>loading...</p>
|
||||
{:else}
|
||||
<Layout noPadding no>
|
||||
<Body size="S">{description}</Body>
|
||||
|
||||
<FancyCheckboxGroup
|
||||
options={$store.tableNames}
|
||||
selected={$store.selectedTableNames}
|
||||
on:change={e => store.setSelectedTableNames(e.detail)}
|
||||
{selectAllText}
|
||||
/>
|
||||
{#if $store.error}
|
||||
<InlineAlert
|
||||
type="error"
|
||||
header={$store.error.title}
|
||||
message={$store.error.description}
|
||||
/>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
</ModalContent>
|
|
@ -0,0 +1,64 @@
|
|||
import { derived, writable, get } from "svelte/store"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { datasources, ImportTableError } from "stores/backend"
|
||||
|
||||
export const createTableSelectionStore = (integration, datasource) => {
|
||||
const tableNamesStore = writable([])
|
||||
const selectedTableNamesStore = writable([])
|
||||
const errorStore = writable(null)
|
||||
const loadingStore = writable(true)
|
||||
|
||||
datasources.getTableNames(datasource).then(tableNames => {
|
||||
tableNamesStore.set(tableNames)
|
||||
selectedTableNamesStore.set(tableNames)
|
||||
loadingStore.set(false)
|
||||
})
|
||||
|
||||
const setSelectedTableNames = selectedTableNames => {
|
||||
selectedTableNamesStore.set(selectedTableNames)
|
||||
}
|
||||
|
||||
const importSelectedTables = async onComplete => {
|
||||
errorStore.set(null)
|
||||
|
||||
try {
|
||||
await datasources.updateSchema(datasource, get(selectedTableNamesStore))
|
||||
|
||||
notifications.success(`Tables fetched successfully.`)
|
||||
await onComplete()
|
||||
} catch (err) {
|
||||
if (err instanceof ImportTableError) {
|
||||
errorStore.set(err)
|
||||
} else {
|
||||
notifications.error("Error fetching tables.")
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent modal closing
|
||||
return false
|
||||
}
|
||||
|
||||
const combined = derived(
|
||||
[tableNamesStore, selectedTableNamesStore, errorStore, loadingStore],
|
||||
([
|
||||
$tableNamesStore,
|
||||
$selectedTableNamesStore,
|
||||
$errorStore,
|
||||
$loadingStore,
|
||||
]) => {
|
||||
return {
|
||||
tableNames: $tableNamesStore,
|
||||
selectedTableNames: $selectedTableNamesStore,
|
||||
error: $errorStore,
|
||||
loading: $loadingStore,
|
||||
hasSelected: $selectedTableNamesStore.length > 0,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
subscribe: combined.subscribe,
|
||||
setSelectedTableNames,
|
||||
importSelectedTables,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
<script>
|
||||
import { Modal } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
import GoogleAuthPrompt from "./GoogleAuthPrompt.svelte"
|
||||
|
||||
import TableImportSelection from "./TableImportSelection/index.svelte"
|
||||
import DatasourceConfigEditor from "./DatasourceConfigEditor/index.svelte"
|
||||
import { datasources } from "stores/backend"
|
||||
import { createOnGoogleAuthStore } from "./stores/onGoogleAuth.js"
|
||||
import { createDatasourceCreationStore } from "./stores/datasourceCreation.js"
|
||||
import { configFromIntegration } from "stores/selectors"
|
||||
|
||||
export let loading = false
|
||||
const store = createDatasourceCreationStore()
|
||||
const onGoogleAuth = createOnGoogleAuthStore()
|
||||
let modal
|
||||
|
||||
const handleStoreChanges = (store, modal, goto) => {
|
||||
store.stage === null ? modal?.hide() : modal?.show()
|
||||
|
||||
if (store.finished) {
|
||||
goto(`./datasource/${store.datasource._id}`)
|
||||
}
|
||||
}
|
||||
|
||||
$: handleStoreChanges($store, modal, $goto)
|
||||
|
||||
export function show(integration) {
|
||||
if (integration.name === IntegrationTypes.REST) {
|
||||
// A REST integration is created immediately, we don't need to display a config modal.
|
||||
loading = true
|
||||
datasources
|
||||
.create({ integration, fields: configFromIntegration(integration) })
|
||||
.then(datasource => {
|
||||
store.setIntegration(integration)
|
||||
store.setDatasource(datasource)
|
||||
})
|
||||
.finally(() => (loading = false))
|
||||
} else if (integration.name === IntegrationTypes.GOOGLE_SHEETS) {
|
||||
// This prompt redirects users to the Google OAuth flow, they'll be returned to this modal afterwards
|
||||
// with query params populated that trigger the `onGoogleAuth` store.
|
||||
store.googleAuthStage()
|
||||
} else {
|
||||
// All other integrations can generate config from data in the integration object.
|
||||
store.setIntegration(integration)
|
||||
store.setConfig(configFromIntegration(integration))
|
||||
store.editConfigStage()
|
||||
}
|
||||
}
|
||||
|
||||
// Triggers opening the config editor whenever Google OAuth returns the user to the page
|
||||
$: $onGoogleAuth((integration, config) => {
|
||||
store.setIntegration(integration)
|
||||
store.setConfig(config)
|
||||
store.editConfigStage()
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal on:hide={store.cancel} bind:this={modal}>
|
||||
{#if $store.stage === "googleAuth"}
|
||||
<GoogleAuthPrompt />
|
||||
{:else if $store.stage === "editConfig"}
|
||||
<DatasourceConfigEditor
|
||||
integration={$store.integration}
|
||||
config={$store.config}
|
||||
onDatasourceCreated={store.setDatasource}
|
||||
/>
|
||||
{:else if $store.stage === "selectTables"}
|
||||
<TableImportSelection
|
||||
integration={$store.integration}
|
||||
datasource={$store.datasource}
|
||||
onComplete={store.markAsFinished}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
|
@ -0,0 +1,92 @@
|
|||
import { get, writable } from "svelte/store"
|
||||
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
||||
|
||||
export const defaultStore = {
|
||||
finished: false,
|
||||
stage: null,
|
||||
integration: null,
|
||||
config: null,
|
||||
datasource: null,
|
||||
}
|
||||
|
||||
export const createDatasourceCreationStore = () => {
|
||||
const store = writable(defaultStore)
|
||||
|
||||
store.cancel = () => {
|
||||
const $store = get(store)
|
||||
// If the datasource has already been created, mark the store as finished.
|
||||
if ($store.stage === "selectTables") {
|
||||
store.markAsFinished()
|
||||
} else {
|
||||
store.set(defaultStore)
|
||||
}
|
||||
}
|
||||
|
||||
// Used only by Google Sheets
|
||||
store.googleAuthStage = () => {
|
||||
store.update($store => ({
|
||||
...$store,
|
||||
stage: "googleAuth",
|
||||
}))
|
||||
}
|
||||
|
||||
store.setIntegration = integration => {
|
||||
store.update($store => ({
|
||||
...$store,
|
||||
integration,
|
||||
}))
|
||||
}
|
||||
|
||||
store.setConfig = config => {
|
||||
store.update($store => ({
|
||||
...$store,
|
||||
config,
|
||||
}))
|
||||
}
|
||||
|
||||
// Used for every flow but REST
|
||||
store.editConfigStage = () => {
|
||||
store.update($store => ({
|
||||
...$store,
|
||||
stage: "editConfig",
|
||||
}))
|
||||
}
|
||||
|
||||
store.setDatasource = datasource => {
|
||||
const $store = get(store)
|
||||
store.set({ ...$store, datasource })
|
||||
|
||||
if (shouldIntegrationFetchTableNames($store.integration)) {
|
||||
store.selectTablesStage()
|
||||
} else {
|
||||
store.markAsFinished()
|
||||
}
|
||||
}
|
||||
|
||||
// Only used for datasource plus
|
||||
store.selectTablesStage = () => {
|
||||
store.update($store => ({
|
||||
...$store,
|
||||
stage: "selectTables",
|
||||
}))
|
||||
}
|
||||
|
||||
store.markAsFinished = () => {
|
||||
store.update($store => ({
|
||||
...$store,
|
||||
finished: true,
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
cancel: store.cancel,
|
||||
googleAuthStage: store.googleAuthStage,
|
||||
setIntegration: store.setIntegration,
|
||||
setConfig: store.setConfig,
|
||||
editConfigStage: store.editConfigStage,
|
||||
setDatasource: store.setDatasource,
|
||||
selectTablesStage: store.selectTablesStage,
|
||||
markAsFinished: store.markAsFinished,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import {
|
||||
defaultStore,
|
||||
createDatasourceCreationStore,
|
||||
} from "./datasourceCreation"
|
||||
import { get } from "svelte/store"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { shouldIntegrationFetchTableNames } from "stores/selectors"
|
||||
|
||||
vi.mock("stores/selectors", () => ({
|
||||
shouldIntegrationFetchTableNames: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("datasource creation store", () => {
|
||||
beforeEach(ctx => {
|
||||
vi.clearAllMocks()
|
||||
// eslint-disable-next-line no-import-assign
|
||||
ctx.store = createDatasourceCreationStore()
|
||||
|
||||
ctx.integration = { data: "integration" }
|
||||
ctx.config = { data: "config" }
|
||||
ctx.datasource = { data: "datasource" }
|
||||
})
|
||||
|
||||
describe("store creation", () => {
|
||||
it("returns the default values", ctx => {
|
||||
expect(get(ctx.store)).toEqual(defaultStore)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancel", () => {
|
||||
describe("when at the `selectTables` stage", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.selectTablesStage()
|
||||
ctx.store.cancel()
|
||||
})
|
||||
|
||||
it("marks the store as finished", ctx => {
|
||||
expect(get(ctx.store)).toEqual({
|
||||
...defaultStore,
|
||||
stage: "selectTables",
|
||||
finished: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("When at any previous stage", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.cancel()
|
||||
})
|
||||
|
||||
it("resets to the default values", ctx => {
|
||||
expect(get(ctx.store)).toEqual(defaultStore)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("googleAuthStage", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.googleAuthStage()
|
||||
})
|
||||
|
||||
it("sets the stage", ctx => {
|
||||
expect(get(ctx.store)).toEqual({ ...defaultStore, stage: "googleAuth" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("setIntegration", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.setIntegration(ctx.integration)
|
||||
})
|
||||
|
||||
it("sets the integration", ctx => {
|
||||
expect(get(ctx.store)).toEqual({
|
||||
...defaultStore,
|
||||
integration: ctx.integration,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("setConfig", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.setConfig(ctx.config)
|
||||
})
|
||||
|
||||
it("sets the config", ctx => {
|
||||
expect(get(ctx.store)).toEqual({
|
||||
...defaultStore,
|
||||
config: ctx.config,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("editConfigStage", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.editConfigStage()
|
||||
})
|
||||
|
||||
it("sets the stage", ctx => {
|
||||
expect(get(ctx.store)).toEqual({ ...defaultStore, stage: "editConfig" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("markAsFinished", () => {
|
||||
beforeEach(ctx => {
|
||||
ctx.store.markAsFinished()
|
||||
})
|
||||
|
||||
it("marks the store as finished", ctx => {
|
||||
expect(get(ctx.store)).toEqual({
|
||||
...defaultStore,
|
||||
finished: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import { derived } from "svelte/store"
|
||||
import { params } from "@roxi/routify"
|
||||
import { integrations } from "stores/backend"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
export const createOnGoogleAuthStore = () => {
|
||||
return derived([params, integrations], ([$params, $integrations]) => {
|
||||
const id = $params["?continue_google_setup"]
|
||||
|
||||
return callback => {
|
||||
if ($integrations && id) {
|
||||
history.replaceState({}, null, window.location.pathname)
|
||||
const integration = {
|
||||
name: IntegrationTypes.GOOGLE_SHEETS,
|
||||
...$integrations[IntegrationTypes.GOOGLE_SHEETS],
|
||||
}
|
||||
|
||||
const fields = { continueSetupId: id, sheetId: "" }
|
||||
|
||||
callback(integration, fields)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { createOnGoogleAuthStore } from "./onGoogleAuth"
|
||||
import { writable, get } from "svelte/store"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { params } from "@roxi/routify"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { integrations } from "stores/backend"
|
||||
import { IntegrationTypes } from "constants/backend"
|
||||
|
||||
vi.mock("@roxi/routify", () => ({
|
||||
params: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("stores/backend", () => ({
|
||||
integrations: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.stubGlobal("history", { replaceState: vi.fn() })
|
||||
vi.stubGlobal("window", { location: { pathname: "/current-path" } })
|
||||
|
||||
describe("google auth store", () => {
|
||||
beforeEach(ctx => {
|
||||
vi.clearAllMocks()
|
||||
// eslint-disable-next-line no-import-assign
|
||||
integrations = writable({
|
||||
[IntegrationTypes.GOOGLE_SHEETS]: { data: "integration" },
|
||||
})
|
||||
ctx.callback = vi.fn()
|
||||
})
|
||||
|
||||
describe("with id present", () => {
|
||||
beforeEach(ctx => {
|
||||
// eslint-disable-next-line no-import-assign
|
||||
params = writable({ "?continue_google_setup": "googleId" })
|
||||
get(createOnGoogleAuthStore())(ctx.callback)
|
||||
})
|
||||
|
||||
it("invokes the provided callback with an integration and fields", ctx => {
|
||||
expect(ctx.callback).toHaveBeenCalledTimes(1)
|
||||
expect(ctx.callback).toHaveBeenCalledWith(
|
||||
{
|
||||
name: IntegrationTypes.GOOGLE_SHEETS,
|
||||
data: "integration",
|
||||
},
|
||||
{ continueSetupId: "googleId", sheetId: "" }
|
||||
)
|
||||
})
|
||||
|
||||
it("clears the query param", () => {
|
||||
expect(history.replaceState).toHaveBeenCalledTimes(1)
|
||||
expect(history.replaceState).toHaveBeenCalledWith(
|
||||
{},
|
||||
null,
|
||||
`/current-path`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("without id present", () => {
|
||||
beforeEach(ctx => {
|
||||
// eslint-disable-next-line no-import-assign
|
||||
params = writable({})
|
||||
get(createOnGoogleAuthStore())(ctx.callback)
|
||||
})
|
||||
|
||||
it("doesn't invoke the provided callback", ctx => {
|
||||
expect(ctx.callback).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
<script>
|
||||
import { Modal, notifications } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
|
||||
let modal
|
||||
let promptUpload = false
|
||||
|
||||
export function show({ promptUpload: newPromptUpload = false }) {
|
||||
promptUpload = newPromptUpload
|
||||
modal.show()
|
||||
}
|
||||
|
||||
const handleInternalTableSave = table => {
|
||||
notifications.success(`Table created successfully.`)
|
||||
$goto(`./table/${table._id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||
</Modal>
|
|
@ -32,7 +32,7 @@
|
|||
</header>
|
||||
<Body size="M">
|
||||
Budibase internal tables are part of your app, so the data will be
|
||||
stored in your apps context.
|
||||
stored in your app's context.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
|
|
|
@ -1,39 +1,31 @@
|
|||
<script>
|
||||
import { API } from "api"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
|
||||
import { Icon, Modal, notifications, Heading, Body } from "@budibase/bbui"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import {
|
||||
IntegrationTypes,
|
||||
DatasourceTypes,
|
||||
DEFAULT_BB_DATASOURCE_ID,
|
||||
} from "constants/backend"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import GoogleDatasourceConfigModal from "components/backend/DatasourceNavigator/modals/GoogleDatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
tables,
|
||||
datasources,
|
||||
sortedIntegrations as integrations,
|
||||
} from "stores/backend"
|
||||
|
||||
import { hasData } from "stores/selectors"
|
||||
import { Icon, notifications, Heading, Body } from "@budibase/bbui"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
|
||||
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
|
||||
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
let integrations = []
|
||||
let integration = null
|
||||
let disabled = false
|
||||
let promptUpload = false
|
||||
|
||||
$: hasData = $datasources.list.length > 1 || $tables.list.length > 1
|
||||
$: hasDefaultData =
|
||||
$datasources.list.findIndex(
|
||||
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||
) !== -1
|
||||
let sampleDataLoading = false
|
||||
let externalDatasourceLoading = false
|
||||
|
||||
$: disabled = sampleDataLoading || externalDatasourceLoading
|
||||
|
||||
const createSampleData = async () => {
|
||||
disabled = true
|
||||
sampleDataLoading = true
|
||||
|
||||
try {
|
||||
await API.addSampleData($params.application)
|
||||
|
@ -41,136 +33,22 @@
|
|||
await datasources.fetch()
|
||||
$goto("./table")
|
||||
} catch (e) {
|
||||
disabled = false
|
||||
sampleDataLoading = false
|
||||
notifications.error("Error creating datasource")
|
||||
}
|
||||
}
|
||||
|
||||
const handleIntegrationSelect = integrationType => {
|
||||
const selected = integrations.find(([type]) => type === integrationType)[1]
|
||||
|
||||
// build the schema
|
||||
const config = {}
|
||||
|
||||
for (let key of Object.keys(selected.datasource)) {
|
||||
config[key] = selected.datasource[key].default
|
||||
}
|
||||
|
||||
integration = {
|
||||
type: integrationType,
|
||||
plus: selected.plus,
|
||||
config,
|
||||
schema: selected.datasource,
|
||||
auth: selected.auth,
|
||||
features: selected.features || [],
|
||||
}
|
||||
|
||||
if (selected.friendlyName) {
|
||||
integration.name = selected.friendlyName
|
||||
}
|
||||
|
||||
if (integration.type === IntegrationTypes.REST) {
|
||||
disabled = true
|
||||
|
||||
// Skip modal for rest, create straight away
|
||||
createRestDatasource(integration)
|
||||
.then(response => {
|
||||
$goto(`./datasource/${response._id}`)
|
||||
})
|
||||
.catch(() => {
|
||||
disabled = false
|
||||
notifications.error("Error creating datasource")
|
||||
})
|
||||
} else {
|
||||
externalDatasourceModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInternalTable = () => {
|
||||
promptUpload = false
|
||||
internalTableModal.show()
|
||||
}
|
||||
|
||||
const handleDataImport = () => {
|
||||
promptUpload = true
|
||||
internalTableModal.show()
|
||||
}
|
||||
|
||||
const handleInternalTableSave = table => {
|
||||
notifications.success(`Table created successfully.`)
|
||||
$goto(`./table/${table._id}`)
|
||||
}
|
||||
|
||||
function sortIntegrations(integrations) {
|
||||
let integrationsArray = Object.entries(integrations)
|
||||
|
||||
function getTypeOrder(schema) {
|
||||
if (schema.type === DatasourceTypes.API) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (schema.type === DatasourceTypes.RELATIONAL) {
|
||||
return 2
|
||||
}
|
||||
|
||||
return schema.type?.charCodeAt(0)
|
||||
}
|
||||
|
||||
integrationsArray.sort((a, b) => {
|
||||
let typeOrderA = getTypeOrder(a[1])
|
||||
let typeOrderB = getTypeOrder(b[1])
|
||||
|
||||
if (typeOrderA === typeOrderB) {
|
||||
return a[1].friendlyName?.localeCompare(b[1].friendlyName)
|
||||
}
|
||||
|
||||
return typeOrderA < typeOrderB ? -1 : 1
|
||||
})
|
||||
|
||||
return integrationsArray
|
||||
}
|
||||
|
||||
let continueGoogleSetup
|
||||
onMount(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
continueGoogleSetup = urlParams.get("continue_google_setup")
|
||||
})
|
||||
|
||||
const fetchIntegrations = async () => {
|
||||
const unsortedIntegrations = await API.getIntegrations()
|
||||
integrations = sortIntegrations(unsortedIntegrations)
|
||||
|
||||
if (continueGoogleSetup) {
|
||||
handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS)
|
||||
}
|
||||
}
|
||||
|
||||
$: fetchIntegrations()
|
||||
</script>
|
||||
|
||||
<Modal bind:this={internalTableModal}>
|
||||
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
|
||||
</Modal>
|
||||
<CreateInternalTableModal bind:this={internalTableModal} />
|
||||
|
||||
<Modal
|
||||
<CreateExternalDatasourceModal
|
||||
bind:loading={externalDatasourceLoading}
|
||||
bind:this={externalDatasourceModal}
|
||||
on:hide={() => {
|
||||
continueGoogleSetup = null
|
||||
}}
|
||||
>
|
||||
{#if integration?.auth?.type === "google"}
|
||||
<GoogleDatasourceConfigModal
|
||||
continueSetupId={continueGoogleSetup}
|
||||
{integration}
|
||||
/>
|
||||
{:else}
|
||||
<DatasourceConfigModal {integration} />
|
||||
{/if}
|
||||
</Modal>
|
||||
/>
|
||||
|
||||
<div class="page">
|
||||
<div class="closeButton">
|
||||
{#if hasData}
|
||||
{#if hasData($datasources, $tables)}
|
||||
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -191,7 +69,7 @@
|
|||
|
||||
<div class="options">
|
||||
<DatasourceOption
|
||||
on:click={handleInternalTable}
|
||||
on:click={internalTableModal.show}
|
||||
title="Create new table"
|
||||
description="Non-relational"
|
||||
{disabled}
|
||||
|
@ -202,12 +80,12 @@
|
|||
on:click={createSampleData}
|
||||
title="Use sample data"
|
||||
description="Non-relational"
|
||||
disabled={disabled || hasDefaultData}
|
||||
disabled={disabled || $datasources.hasDefaultData}
|
||||
>
|
||||
<svelte:component this={ICONS.BUDIBASE} height="20" width="20" />
|
||||
</DatasourceOption>
|
||||
<DatasourceOption
|
||||
on:click={handleDataImport}
|
||||
on:click={() => internalTableModal.show({ promptUpload: true })}
|
||||
title="Upload data"
|
||||
description="Non-relational"
|
||||
{disabled}
|
||||
|
@ -221,14 +99,17 @@
|
|||
</div>
|
||||
|
||||
<div class="options">
|
||||
{#each integrations as [key, value]}
|
||||
{#each $integrations as integration}
|
||||
<DatasourceOption
|
||||
on:click={() => handleIntegrationSelect(key)}
|
||||
title={value.friendlyName}
|
||||
description={value.type}
|
||||
on:click={() => externalDatasourceModal.show(integration)}
|
||||
title={integration.friendlyName}
|
||||
description={integration.type}
|
||||
{disabled}
|
||||
>
|
||||
<IntegrationIcon integrationType={key} schema={value} />
|
||||
<IntegrationIcon
|
||||
integrationType={integration.name}
|
||||
schema={integration}
|
||||
/>
|
||||
</DatasourceOption>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"name": "Blocks",
|
||||
"icon": "Article",
|
||||
"children": [
|
||||
"gridblock",
|
||||
"tableblock",
|
||||
"cardsblock",
|
||||
"repeaterblock",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<script>
|
||||
export let value
|
||||
</script>
|
||||
|
||||
<div style="display: flex; ">
|
||||
{#if value === "Unavailable"}
|
||||
Email already in use. Please use a different email.
|
||||
{:else}
|
||||
{value}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Body, ModalContent, Table } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import InviteResponseRenderer from "./InviteResponseRenderer.svelte"
|
||||
|
||||
export let inviteUsersResponse
|
||||
|
||||
|
@ -50,7 +51,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ModalContent size="M" showCancelButton={false} {title} confirmText="Done">
|
||||
<ModalContent size="L" showCancelButton={false} {title} confirmText="Done">
|
||||
{#if hasSuccess}
|
||||
<Body size="XS">
|
||||
Your users should now receive an email invite to get access to their
|
||||
|
@ -67,6 +68,9 @@
|
|||
allowEditColumns={false}
|
||||
allowEditRows={false}
|
||||
allowSelectRows={false}
|
||||
customRenderers={[
|
||||
{ column: "reason", component: InviteResponseRenderer },
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { IntegrationTypes, DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||
import { queries, tables } from "./"
|
||||
import { API } from "api"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export class ImportTableError extends Error {
|
||||
constructor(message) {
|
||||
super(message)
|
||||
const [title, description] = message.split(" - ")
|
||||
|
||||
this.name = "TableSelectionError"
|
||||
// Capitalize the first character of both the title and description
|
||||
this.title = title[0].toUpperCase() + title.substr(1)
|
||||
this.description = description[0].toUpperCase() + description.substr(1)
|
||||
}
|
||||
}
|
||||
|
||||
export function createDatasourcesStore() {
|
||||
const store = writable({
|
||||
|
@ -8,9 +23,13 @@ export function createDatasourcesStore() {
|
|||
selectedDatasourceId: null,
|
||||
schemaError: null,
|
||||
})
|
||||
|
||||
const derivedStore = derived(store, $store => ({
|
||||
...$store,
|
||||
selected: $store.list?.find(ds => ds._id === $store.selectedDatasourceId),
|
||||
hasDefaultData: $store.list.some(
|
||||
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||
),
|
||||
}))
|
||||
|
||||
const fetch = async () => {
|
||||
|
@ -50,27 +69,62 @@ export function createDatasourcesStore() {
|
|||
}
|
||||
|
||||
const updateSchema = async (datasource, tablesFilter) => {
|
||||
const response = await API.buildDatasourceSchema({
|
||||
datasourceId: datasource?._id,
|
||||
tablesFilter,
|
||||
})
|
||||
return updateDatasource(response)
|
||||
}
|
||||
|
||||
const save = async (body, { fetchSchema, tablesFilter } = {}) => {
|
||||
if (fetchSchema == null) {
|
||||
fetchSchema = false
|
||||
}
|
||||
let response
|
||||
if (body._id) {
|
||||
response = await API.updateDatasource(body)
|
||||
} else {
|
||||
response = await API.createDatasource({
|
||||
datasource: body,
|
||||
fetchSchema,
|
||||
try {
|
||||
const response = await API.buildDatasourceSchema({
|
||||
datasourceId: datasource?._id,
|
||||
tablesFilter,
|
||||
})
|
||||
updateDatasource(response)
|
||||
} catch (e) {
|
||||
// buildDatasourceSchema call returns user presentable errors with two parts divided with a " - ".
|
||||
if (e.message.split(" - ").length === 2) {
|
||||
throw new ImportTableError(e.message)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sourceCount = source => {
|
||||
return get(store).list.filter(datasource => datasource.source === source)
|
||||
.length
|
||||
}
|
||||
|
||||
const create = async ({ integration, fields }) => {
|
||||
try {
|
||||
const datasource = {
|
||||
type: "datasource",
|
||||
source: integration.name,
|
||||
config: fields,
|
||||
name: `${integration.friendlyName}-${
|
||||
sourceCount(integration.name) + 1
|
||||
}`,
|
||||
plus: integration.plus && integration.name !== IntegrationTypes.REST,
|
||||
}
|
||||
|
||||
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
|
||||
const { connected } = await API.validateDatasource(datasource)
|
||||
if (!connected) throw new Error("Unable to connect")
|
||||
}
|
||||
|
||||
const response = await API.createDatasource({
|
||||
datasource,
|
||||
fetchSchema:
|
||||
integration.plus &&
|
||||
integration.name !== IntegrationTypes.GOOGLE_SHEETS,
|
||||
})
|
||||
|
||||
notifications.success("Datasource created successfully.")
|
||||
|
||||
return updateDatasource(response)
|
||||
} catch (e) {
|
||||
notifications.error(`Error creating datasource: ${e.message}`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const save = async body => {
|
||||
const response = await API.updateDatasource(body)
|
||||
return updateDatasource(response)
|
||||
}
|
||||
|
||||
|
@ -117,6 +171,10 @@ export function createDatasourcesStore() {
|
|||
...state,
|
||||
list: [...state.list, datasource],
|
||||
}))
|
||||
|
||||
// If this is a new datasource then we should refresh the tables list,
|
||||
// because otherwise we'll never see the new tables
|
||||
tables.fetch()
|
||||
}
|
||||
|
||||
// Update existing datasource
|
||||
|
@ -128,16 +186,23 @@ export function createDatasourcesStore() {
|
|||
}
|
||||
}
|
||||
|
||||
const getTableNames = async datasource => {
|
||||
const info = await API.fetchInfoForDatasource(datasource)
|
||||
return info.tableNames || []
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
fetch,
|
||||
init: fetch,
|
||||
select,
|
||||
updateSchema,
|
||||
create,
|
||||
save,
|
||||
delete: deleteDatasource,
|
||||
removeSchemaError,
|
||||
replaceDatasource,
|
||||
getTableNames,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ export { tables } from "./tables"
|
|||
export { views } from "./views"
|
||||
export { permissions } from "./permissions"
|
||||
export { roles } from "./roles"
|
||||
export { datasources } from "./datasources"
|
||||
export { datasources, ImportTableError } from "./datasources"
|
||||
export { integrations } from "./integrations"
|
||||
export { sortedIntegrations } from "./sortedIntegrations"
|
||||
export { queries } from "./queries"
|
||||
export { flags } from "./flags"
|
||||
|
|
|
@ -2,14 +2,16 @@ import { writable } from "svelte/store"
|
|||
import { API } from "api"
|
||||
|
||||
const createIntegrationsStore = () => {
|
||||
const store = writable(null)
|
||||
const store = writable({})
|
||||
|
||||
const init = async () => {
|
||||
const integrations = await API.getIntegrations()
|
||||
store.set(integrations)
|
||||
}
|
||||
|
||||
return {
|
||||
...store,
|
||||
init: async () => {
|
||||
const integrations = await API.getIntegrations()
|
||||
store.set(integrations)
|
||||
},
|
||||
init,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { integrations } from "./integrations"
|
||||
import { derived } from "svelte/store"
|
||||
|
||||
import { DatasourceTypes } from "constants/backend"
|
||||
|
||||
const getIntegrationOrder = type => {
|
||||
if (type === DatasourceTypes.API) return 1
|
||||
if (type === DatasourceTypes.RELATIONAL) return 2
|
||||
if (type === DatasourceTypes.NON_RELATIONAL) return 3
|
||||
|
||||
// Sort all others arbitrarily by the first character of their name.
|
||||
// Character codes can technically be as low as 0, so make sure the number is at least 4
|
||||
return type.charCodeAt(0) + 4
|
||||
}
|
||||
|
||||
export const createSortedIntegrationsStore = () => {
|
||||
return derived(integrations, $integrations => {
|
||||
const integrationsAsArray = Object.entries($integrations).map(
|
||||
([name, integration]) => ({
|
||||
name,
|
||||
...integration,
|
||||
})
|
||||
)
|
||||
|
||||
return integrationsAsArray.sort((integrationA, integrationB) => {
|
||||
const integrationASortOrder = getIntegrationOrder(integrationA.type)
|
||||
const integrationBSortOrder = getIntegrationOrder(integrationB.type)
|
||||
if (integrationASortOrder === integrationBSortOrder) {
|
||||
return integrationA.friendlyName.localeCompare(
|
||||
integrationB.friendlyName
|
||||
)
|
||||
}
|
||||
|
||||
return integrationASortOrder < integrationBSortOrder ? -1 : 1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const sortedIntegrations = createSortedIntegrationsStore()
|
|
@ -0,0 +1,127 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { createSortedIntegrationsStore } from "./sortedIntegrations"
|
||||
import { DatasourceTypes } from "constants/backend"
|
||||
|
||||
import { derived } from "svelte/store"
|
||||
import { integrations } from "stores/backend/integrations"
|
||||
|
||||
vi.mock("svelte/store", () => ({
|
||||
derived: vi.fn(() => {}),
|
||||
}))
|
||||
|
||||
vi.mock("stores/backend/integrations", () => ({ integrations: vi.fn() }))
|
||||
|
||||
const inputA = {
|
||||
nonRelationalA: {
|
||||
friendlyName: "non-relational A",
|
||||
type: DatasourceTypes.NON_RELATIONAL,
|
||||
},
|
||||
relationalB: {
|
||||
friendlyName: "relational B",
|
||||
type: DatasourceTypes.RELATIONAL,
|
||||
},
|
||||
relationalA: {
|
||||
friendlyName: "relational A",
|
||||
type: DatasourceTypes.RELATIONAL,
|
||||
},
|
||||
api: {
|
||||
friendlyName: "api",
|
||||
type: DatasourceTypes.API,
|
||||
},
|
||||
relationalC: {
|
||||
friendlyName: "relational C",
|
||||
type: DatasourceTypes.RELATIONAL,
|
||||
},
|
||||
nonRelationalB: {
|
||||
friendlyName: "non-relational B",
|
||||
type: DatasourceTypes.NON_RELATIONAL,
|
||||
},
|
||||
otherC: {
|
||||
friendlyName: "other C",
|
||||
type: "random",
|
||||
},
|
||||
otherB: {
|
||||
friendlyName: "other B",
|
||||
type: "arbitrary",
|
||||
},
|
||||
otherA: {
|
||||
friendlyName: "other A",
|
||||
type: "arbitrary",
|
||||
},
|
||||
}
|
||||
|
||||
const inputB = Object.fromEntries(Object.entries(inputA).reverse())
|
||||
|
||||
const expectedOutput = [
|
||||
{
|
||||
name: "api",
|
||||
friendlyName: "api",
|
||||
type: DatasourceTypes.API,
|
||||
},
|
||||
{
|
||||
name: "relationalA",
|
||||
friendlyName: "relational A",
|
||||
type: DatasourceTypes.RELATIONAL,
|
||||
},
|
||||
{
|
||||
name: "relationalB",
|
||||
friendlyName: "relational B",
|
||||
type: DatasourceTypes.RELATIONAL,
|
||||
},
|
||||
{
|
||||
name: "relationalC",
|
||||
friendlyName: "relational C",
|
||||
type: DatasourceTypes.RELATIONAL,
|
||||
},
|
||||
{
|
||||
name: "nonRelationalA",
|
||||
friendlyName: "non-relational A",
|
||||
type: DatasourceTypes.NON_RELATIONAL,
|
||||
},
|
||||
{
|
||||
name: "nonRelationalB",
|
||||
friendlyName: "non-relational B",
|
||||
type: DatasourceTypes.NON_RELATIONAL,
|
||||
},
|
||||
{
|
||||
name: "otherA",
|
||||
friendlyName: "other A",
|
||||
type: "arbitrary",
|
||||
},
|
||||
{
|
||||
name: "otherB",
|
||||
friendlyName: "other B",
|
||||
type: "arbitrary",
|
||||
},
|
||||
{
|
||||
name: "otherC",
|
||||
friendlyName: "other C",
|
||||
type: "random",
|
||||
},
|
||||
]
|
||||
|
||||
describe("sorted integrations store", () => {
|
||||
beforeEach(ctx => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
ctx.returnedStore = createSortedIntegrationsStore()
|
||||
|
||||
ctx.derivedCallback = derived.mock.calls[0][1]
|
||||
})
|
||||
|
||||
it("calls derived with the correct parameters", () => {
|
||||
expect(derived).toHaveBeenCalledTimes(1)
|
||||
expect(derived).toHaveBeenCalledWith(integrations, expect.toBeFunc())
|
||||
})
|
||||
|
||||
describe("derived callback", () => {
|
||||
it("When no integrations are loaded", ctx => {
|
||||
expect(ctx.derivedCallback({})).toEqual([])
|
||||
})
|
||||
|
||||
it("When integrations are present", ctx => {
|
||||
expect(ctx.derivedCallback(inputA)).toEqual(expectedOutput)
|
||||
expect(ctx.derivedCallback(inputB)).toEqual(expectedOutput)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
import { get, writable, derived } from "svelte/store"
|
||||
import { datasources } from "./"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { API } from "api"
|
||||
import { SWITCHABLE_TYPES } from "constants/backend"
|
||||
|
@ -63,7 +62,6 @@ export function createTablesStore() {
|
|||
|
||||
const savedTable = await API.saveTable(updatedTable)
|
||||
replaceTable(savedTable._id, savedTable)
|
||||
await datasources.fetch()
|
||||
select(savedTable._id)
|
||||
return savedTable
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||
import { DatasourceFeature } from "@budibase/types"
|
||||
|
||||
export const integrationForDatasource = (integrations, datasource) => ({
|
||||
name: datasource.source,
|
||||
...integrations[datasource.source],
|
||||
})
|
||||
|
||||
export const hasData = (datasources, tables) =>
|
||||
datasources.list.length > 1 || tables.list.length > 1
|
||||
|
||||
export const hasDefaultData = datasources =>
|
||||
datasources.list.some(
|
||||
datasource => datasource._id === DEFAULT_BB_DATASOURCE_ID
|
||||
)
|
||||
|
||||
export const configFromIntegration = integration => {
|
||||
const config = {}
|
||||
|
||||
Object.entries(integration?.datasource || {}).forEach(([key, properties]) => {
|
||||
if (properties.type === "fieldGroup") {
|
||||
Object.keys(properties.fields).forEach(fieldKey => {
|
||||
config[fieldKey] = null
|
||||
})
|
||||
} else {
|
||||
config[key] = properties.default ?? null
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export const shouldIntegrationFetchTableNames = integration => {
|
||||
return integration.features?.[DatasourceFeature.FETCH_TABLE_NAMES]
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { DEFAULT_BB_DATASOURCE_ID } from "constants/backend"
|
||||
import { integrationForDatasource, hasData, hasDefaultData } from "./selectors"
|
||||
|
||||
describe("selectors", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("integrationForDatasource", () => {
|
||||
it("returns the integration corresponding to the given datasource", () => {
|
||||
expect(
|
||||
integrationForDatasource(
|
||||
{ integrationOne: { some: "data" } },
|
||||
{ source: "integrationOne" }
|
||||
)
|
||||
).toEqual({ some: "data", name: "integrationOne" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasData", () => {
|
||||
describe("when the user has created a datasource in addition to the premade Budibase DB source", () => {
|
||||
it("returns true", () => {
|
||||
expect(hasData({ list: [1, 1] }, { list: [] })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when the user has created a table in addition to the premade users table", () => {
|
||||
it("returns true", () => {
|
||||
expect(hasData({ list: [] }, { list: [1, 1] })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when the user doesn't have data", () => {
|
||||
it("returns false", () => {
|
||||
expect(hasData({ list: [] }, { list: [] })).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasDefaultData", () => {
|
||||
describe("when the user has default data", () => {
|
||||
it("returns true", () => {
|
||||
expect(
|
||||
hasDefaultData({ list: [{ _id: DEFAULT_BB_DATASOURCE_ID }] })
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when the user doesn't have default data", () => {
|
||||
it("returns false", () => {
|
||||
expect(hasDefaultData({ list: [{ _id: "some other id" }] })).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -2221,7 +2221,8 @@
|
|||
"ValidateForm",
|
||||
"ClearForm",
|
||||
"ChangeFormStep",
|
||||
"UpdateFieldValue"
|
||||
"UpdateFieldValue",
|
||||
"ScrollTo"
|
||||
],
|
||||
"styles": ["size"],
|
||||
"size": {
|
||||
|
@ -3543,7 +3544,8 @@
|
|||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort column",
|
||||
"key": "sortColumn"
|
||||
"key": "sortColumn",
|
||||
"placeholder": "None"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -4322,7 +4324,8 @@
|
|||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort by",
|
||||
"key": "sortColumn"
|
||||
"key": "sortColumn",
|
||||
"placeholder": "None"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -4566,7 +4569,8 @@
|
|||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort column",
|
||||
"key": "sortColumn"
|
||||
"key": "sortColumn",
|
||||
"placeholder": "None"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -4734,7 +4738,8 @@
|
|||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort column",
|
||||
"key": "sortColumn"
|
||||
"key": "sortColumn",
|
||||
"placeholder": "None"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -5225,5 +5230,91 @@
|
|||
"type": "schema",
|
||||
"suffix": "repeater"
|
||||
}
|
||||
},
|
||||
"gridblock": {
|
||||
"name": "Grid block",
|
||||
"icon": "Table",
|
||||
"styles": ["size"],
|
||||
"size": {
|
||||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"info": "Grid Blocks are only compatible with internal or SQL tables",
|
||||
"settings": [
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Table",
|
||||
"key": "table",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "columns/basic",
|
||||
"label": "Columns",
|
||||
"key": "columns",
|
||||
"dependsOn": "table"
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "initialFilter"
|
||||
},
|
||||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort column",
|
||||
"key": "initialSortColumn",
|
||||
"placeholder": "Default"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort order",
|
||||
"key": "initialSortOrder",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Ascending"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Row height",
|
||||
"key": "initialRowHeight",
|
||||
"placeholder": "Default",
|
||||
"options": [
|
||||
{
|
||||
"label": "Small",
|
||||
"value": 36
|
||||
},
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": 64
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": 92
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Add rows",
|
||||
"key": "allowAddRows",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Edit rows",
|
||||
"key": "allowEditRows",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Delete rows",
|
||||
"key": "allowDeleteRows",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "High contrast",
|
||||
"key": "stripeRows",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,8 @@ export const API = createAPIClient({
|
|||
// We could also log these to sentry.
|
||||
// Or we could check error.status and redirect to login on a 403 etc.
|
||||
onError: error => {
|
||||
const { status, method, url, message, handled } = error || {}
|
||||
const { status, method, url, message, handled, suppressErrors } =
|
||||
error || {}
|
||||
const ignoreErrorUrls = [
|
||||
"bbtel",
|
||||
"/api/global/self",
|
||||
|
@ -49,7 +50,7 @@ export const API = createAPIClient({
|
|||
}
|
||||
|
||||
// Notify all errors
|
||||
if (message) {
|
||||
if (message && !suppressErrors) {
|
||||
// Don't notify if the URL contains the word analytics as it may be
|
||||
// blocked by browser extensions
|
||||
let ignore = false
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
<script>
|
||||
// NOTE: this is not a block - it's just named as such to avoid confusing users,
|
||||
// because it functions similarly to one
|
||||
import { getContext } from "svelte"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
||||
export let table
|
||||
export let allowAddRows = true
|
||||
export let allowEditRows = true
|
||||
export let allowDeleteRows = true
|
||||
export let stripeRows = false
|
||||
export let initialFilter = null
|
||||
export let initialSortColumn = null
|
||||
export let initialSortOrder = null
|
||||
export let initialRowHeight = null
|
||||
export let columns = null
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable, API, builderStore } = getContext("sdk")
|
||||
|
||||
$: columnWhitelist = columns?.map(col => col.name)
|
||||
$: schemaOverrides = getSchemaOverrides(columns)
|
||||
|
||||
const getSchemaOverrides = columns => {
|
||||
let overrides = {}
|
||||
columns?.forEach(column => {
|
||||
overrides[column.name] = {
|
||||
displayName: column.displayName || column.name,
|
||||
}
|
||||
})
|
||||
return overrides
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
use:styleable={$component.styles}
|
||||
class:in-builder={$builderStore.inBuilder}
|
||||
>
|
||||
<Grid
|
||||
tableId={table?.tableId}
|
||||
{API}
|
||||
{allowAddRows}
|
||||
{allowEditRows}
|
||||
{allowDeleteRows}
|
||||
{stripeRows}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
{initialSortOrder}
|
||||
{initialRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
showControls={false}
|
||||
allowExpandRows={false}
|
||||
allowSchemaChanges={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
min-height: 410px;
|
||||
}
|
||||
div.in-builder :global(*) {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
|
@ -283,7 +283,7 @@
|
|||
|
||||
// Skip if the value is the same
|
||||
if (!skipCheck && fieldState.value === value) {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
// Update field state
|
||||
|
@ -295,7 +295,7 @@
|
|||
return state
|
||||
})
|
||||
|
||||
return !error
|
||||
return true
|
||||
}
|
||||
|
||||
// Clears the value of a certain field back to the default value
|
||||
|
@ -376,8 +376,9 @@
|
|||
deregister,
|
||||
validate: () => {
|
||||
// Validate the field by force setting the same value again
|
||||
const { fieldState } = get(getField(field))
|
||||
return setValue(fieldState.value, true)
|
||||
const fieldInfo = getField(field)
|
||||
setValue(get(fieldInfo).fieldState.value, true)
|
||||
return !get(fieldInfo).fieldState.error
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -404,12 +405,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
const handleScrollToField = ({ field }) => {
|
||||
const fieldId = get(getField(field)).fieldState.fieldId
|
||||
const label = document.querySelector(`label[for="${fieldId}"]`)
|
||||
document.getElementById(fieldId).focus({ preventScroll: true })
|
||||
label.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
|
||||
// Action context to pass to children
|
||||
const actions = [
|
||||
{ type: ActionTypes.ValidateForm, callback: formApi.validate },
|
||||
{ type: ActionTypes.ClearForm, callback: formApi.reset },
|
||||
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
||||
{ type: ActionTypes.UpdateFieldValue, callback: handleUpdateFieldValue },
|
||||
{ type: ActionTypes.ScrollTo, callback: handleScrollToField },
|
||||
]
|
||||
</script>
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ export { default as markdownviewer } from "./MarkdownViewer.svelte"
|
|||
export { default as embeddedmap } from "./embedded-map/EmbeddedMap.svelte"
|
||||
export { default as grid } from "./Grid.svelte"
|
||||
export { default as sidepanel } from "./SidePanel.svelte"
|
||||
export { default as gridblock } from "./GridBlock.svelte"
|
||||
export * from "./charts"
|
||||
export * from "./forms"
|
||||
export * from "./table"
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<div class="notifications">
|
||||
{#if $notificationStore}
|
||||
{#each $notificationStore as { type, icon, message, id, dismissable } (id)}
|
||||
{#each $notificationStore as { type, icon, message, id, dismissable, count } (id)}
|
||||
<div
|
||||
in:fly={{
|
||||
duration: 300,
|
||||
|
@ -17,7 +17,7 @@
|
|||
>
|
||||
<Notification
|
||||
{type}
|
||||
{message}
|
||||
message={count > 1 ? `(${count}) ${message}` : message}
|
||||
{icon}
|
||||
{dismissable}
|
||||
on:dismiss={() => notificationStore.actions.dismiss(id)}
|
||||
|
|
|
@ -29,6 +29,7 @@ export const ActionTypes = {
|
|||
SetDataProviderSorting: "SetDataProviderSorting",
|
||||
ClearForm: "ClearForm",
|
||||
ChangeFormStep: "ChangeFormStep",
|
||||
ScrollTo: "ScrollTo",
|
||||
}
|
||||
|
||||
export const DNDPlaceholderID = "dnd-placeholder"
|
||||
|
|
|
@ -13,7 +13,13 @@ const createNotificationStore = () => {
|
|||
setTimeout(() => (block = false), timeout)
|
||||
}
|
||||
|
||||
const send = (message, type = "info", icon, autoDismiss = true) => {
|
||||
const send = (
|
||||
message,
|
||||
type = "info",
|
||||
icon,
|
||||
autoDismiss = true,
|
||||
count = 1
|
||||
) => {
|
||||
if (block) {
|
||||
return
|
||||
}
|
||||
|
@ -33,6 +39,11 @@ const createNotificationStore = () => {
|
|||
}
|
||||
const _id = id()
|
||||
store.update(state => {
|
||||
const duplicateError = state.find(err => err.message === message)
|
||||
if (duplicateError) {
|
||||
duplicateError.count += 1
|
||||
return [...state]
|
||||
}
|
||||
return [
|
||||
...state,
|
||||
{
|
||||
|
@ -42,6 +53,7 @@ const createNotificationStore = () => {
|
|||
icon,
|
||||
dismissable: !autoDismiss,
|
||||
delay: get(store) != null,
|
||||
count,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
|
|
@ -153,6 +153,17 @@ const navigationHandler = action => {
|
|||
routeStore.actions.navigate(url, peek, externalNewTab)
|
||||
}
|
||||
|
||||
const scrollHandler = async (action, context) => {
|
||||
return await executeActionHandler(
|
||||
context,
|
||||
action.parameters.componentId,
|
||||
ActionTypes.ScrollTo,
|
||||
{
|
||||
field: action.parameters.field,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const queryExecutionHandler = async action => {
|
||||
const { datasourceId, queryId, queryParams, notificationOverride } =
|
||||
action.parameters
|
||||
|
@ -369,6 +380,7 @@ const handlerMap = {
|
|||
["Duplicate Row"]: duplicateRowHandler,
|
||||
["Delete Row"]: deleteRowHandler,
|
||||
["Navigate To"]: navigationHandler,
|
||||
["Scroll To Field"]: scrollHandler,
|
||||
["Execute Query"]: queryExecutionHandler,
|
||||
["Trigger Automation"]: triggerAutomationHandler,
|
||||
["Validate Form"]: validateFormHandler,
|
||||
|
|
|
@ -75,7 +75,11 @@ export const createAPIClient = config => {
|
|||
let cache = {}
|
||||
|
||||
// Generates an error object from an API response
|
||||
const makeErrorFromResponse = async (response, method) => {
|
||||
const makeErrorFromResponse = async (
|
||||
response,
|
||||
method,
|
||||
suppressErrors = false
|
||||
) => {
|
||||
// Try to read a message from the error
|
||||
let message = response.statusText
|
||||
let json = null
|
||||
|
@ -96,6 +100,7 @@ export const createAPIClient = config => {
|
|||
url: response.url,
|
||||
method,
|
||||
handled: true,
|
||||
suppressErrors,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +124,7 @@ export const createAPIClient = config => {
|
|||
json = true,
|
||||
external = false,
|
||||
parseResponse,
|
||||
suppressErrors = false,
|
||||
}) => {
|
||||
// Ensure we don't do JSON processing if sending a GET request
|
||||
json = json && method !== "GET"
|
||||
|
@ -174,7 +180,7 @@ export const createAPIClient = config => {
|
|||
}
|
||||
} else {
|
||||
delete cache[url]
|
||||
throw await makeErrorFromResponse(response, method)
|
||||
throw await makeErrorFromResponse(response, method, suppressErrors)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,6 +234,14 @@ export const createAPIClient = config => {
|
|||
invalidateCache: () => {
|
||||
cache = {}
|
||||
},
|
||||
|
||||
// Generic utility to extract the current app ID. Assumes that any client
|
||||
// that exists in an app context will be attaching our app ID header.
|
||||
getAppID: () => {
|
||||
let headers = {}
|
||||
config?.attachHeaders(headers)
|
||||
return headers?.["x-budibase-app-id"]
|
||||
},
|
||||
}
|
||||
|
||||
// Attach all endpoints
|
||||
|
|
|
@ -16,14 +16,16 @@ export const buildRowEndpoints = API => ({
|
|||
/**
|
||||
* Creates or updates a row in a table.
|
||||
* @param row the row to save
|
||||
* @param suppressErrors whether or not to suppress error notifications
|
||||
*/
|
||||
saveRow: async row => {
|
||||
saveRow: async (row, suppressErrors = false) => {
|
||||
if (!row?.tableId) {
|
||||
return
|
||||
}
|
||||
return await API.post({
|
||||
url: `/api/${row.tableId}/rows`,
|
||||
body: row,
|
||||
suppressErrors,
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -138,10 +138,12 @@
|
|||
top: 100%;
|
||||
left: 0;
|
||||
width: 320px;
|
||||
background: var(--background);
|
||||
background: var(--grid-background-alt);
|
||||
border: var(--cell-border);
|
||||
padding: var(--cell-padding);
|
||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
.dropzone.invertX {
|
||||
left: auto;
|
||||
|
|
|
@ -132,7 +132,7 @@
|
|||
--cell-color: var(--user-color);
|
||||
}
|
||||
.cell.focused {
|
||||
--cell-color: var(--spectrum-global-color-blue-400);
|
||||
--cell-color: var(--accent-color);
|
||||
}
|
||||
.cell.error {
|
||||
--cell-color: var(--spectrum-global-color-red-500);
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
export let rowFocused = false
|
||||
export let rowHovered = false
|
||||
export let rowSelected = false
|
||||
export let disableExpand = false
|
||||
export let expandable = false
|
||||
export let disableNumber = false
|
||||
export let defaultHeight = false
|
||||
export let disabled = false
|
||||
|
@ -24,13 +24,6 @@
|
|||
selectedRows.actions.toggleRow(id)
|
||||
}
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
svelteDispatch("expand")
|
||||
if (row) {
|
||||
dispatch("edit-row", row)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<GridCell
|
||||
|
@ -70,12 +63,14 @@
|
|||
color="var(--spectrum-global-color-red-400)"
|
||||
/>
|
||||
</div>
|
||||
{:else if $config.allowExpandRows}
|
||||
<div
|
||||
class="expand"
|
||||
class:visible={!disableExpand && (rowFocused || rowHovered)}
|
||||
>
|
||||
<Icon name="Maximize" hoverable size="S" on:click={expand} />
|
||||
{:else}
|
||||
<div class="expand" class:visible={$config.allowExpandRows && expandable}>
|
||||
<Icon
|
||||
size="S"
|
||||
name="Maximize"
|
||||
hoverable
|
||||
on:click={() => svelteDispatch("expand")}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -163,7 +163,7 @@
|
|||
<MenuItem
|
||||
icon="Edit"
|
||||
on:click={editColumn}
|
||||
disabled={!$config.allowEditColumns || column.schema.disabled}
|
||||
disabled={!$config.allowSchemaChanges || column.schema.disabled}
|
||||
>
|
||||
Edit column
|
||||
</MenuItem>
|
||||
|
@ -171,7 +171,7 @@
|
|||
icon="Label"
|
||||
on:click={makeDisplayColumn}
|
||||
disabled={idx === "sticky" ||
|
||||
!$config.allowEditColumns ||
|
||||
!$config.allowSchemaChanges ||
|
||||
bannedDisplayColumnTypes.includes(column.schema.type)}
|
||||
>
|
||||
Use as display column
|
||||
|
@ -197,10 +197,12 @@
|
|||
Move right
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={idx === "sticky"}
|
||||
disabled={idx === "sticky" || !$config.showControls}
|
||||
icon="VisibilityOff"
|
||||
on:click={hideColumn}>Hide column</MenuItem
|
||||
on:click={hideColumn}
|
||||
>
|
||||
Hide column
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Popover>
|
||||
|
||||
|
@ -218,7 +220,7 @@
|
|||
.header-cell :global(.cell) {
|
||||
padding: 0 var(--cell-padding);
|
||||
gap: calc(2 * var(--cell-spacing));
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
background: var(--grid-background-alt);
|
||||
}
|
||||
|
||||
.name {
|
||||
|
|
|
@ -102,7 +102,7 @@
|
|||
top: 0;
|
||||
left: 0;
|
||||
width: calc(100% + var(--max-cell-render-width-overflow));
|
||||
height: var(--max-cell-render-height);
|
||||
height: calc(var(--row-height) + var(--max-cell-render-height));
|
||||
z-index: 1;
|
||||
border-radius: 2px;
|
||||
resize: none;
|
||||
|
|
|
@ -132,10 +132,7 @@
|
|||
{option}
|
||||
</div>
|
||||
{#if values.includes(option)}
|
||||
<Icon
|
||||
name="Checkmark"
|
||||
color="var(--spectrum-global-color-blue-400)"
|
||||
/>
|
||||
<Icon name="Checkmark" color="var(--accent-color)" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -223,6 +220,8 @@
|
|||
overflow-y: auto;
|
||||
border: var(--cell-border);
|
||||
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
.options.invertX {
|
||||
left: auto;
|
||||
|
@ -240,7 +239,7 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--cell-spacing);
|
||||
background-color: var(--background);
|
||||
background-color: var(--grid-background-alt);
|
||||
}
|
||||
.option:hover,
|
||||
.option.focused {
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
let candidateIndex
|
||||
let lastSearchId
|
||||
let searching = false
|
||||
let valuesHeight = 0
|
||||
let container
|
||||
|
||||
$: oneRowOnly = schema?.relationshipType === "one-to-many"
|
||||
$: editable = focused && !readonly
|
||||
|
@ -138,6 +140,7 @@
|
|||
|
||||
const open = async () => {
|
||||
isOpen = true
|
||||
valuesHeight = container.getBoundingClientRect().height
|
||||
|
||||
// Find the primary display for the related table
|
||||
if (!primaryDisplay) {
|
||||
|
@ -242,8 +245,14 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="wrapper" class:editable class:focused style="--color:{color};">
|
||||
<div class="container">
|
||||
<div
|
||||
class="wrapper"
|
||||
class:editable
|
||||
class:focused
|
||||
class:invertY
|
||||
style="--color:{color};"
|
||||
>
|
||||
<div class="container" bind:this={container}>
|
||||
<div
|
||||
class="values"
|
||||
class:wrap={editable || contentLines > 1}
|
||||
|
@ -290,6 +299,7 @@
|
|||
class:invertY
|
||||
on:wheel|stopPropagation
|
||||
use:clickOutside={close}
|
||||
style="--values-height:{valuesHeight}px;"
|
||||
>
|
||||
<div class="search">
|
||||
<Input
|
||||
|
@ -319,11 +329,7 @@
|
|||
</span>
|
||||
</div>
|
||||
{#if isRowSelected(row)}
|
||||
<Icon
|
||||
size="S"
|
||||
name="Checkmark"
|
||||
color="var(--spectrum-global-color-blue-400)"
|
||||
/>
|
||||
<Icon size="S" name="Checkmark" color="var(--accent-color)" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -340,7 +346,7 @@
|
|||
min-height: var(--row-height);
|
||||
max-height: var(--row-height);
|
||||
overflow: hidden;
|
||||
--max-relationship-height: 120px;
|
||||
--max-relationship-height: 96px;
|
||||
}
|
||||
.wrapper.focused {
|
||||
position: absolute;
|
||||
|
@ -352,6 +358,10 @@
|
|||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.wrapper.invertY {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: var(--row-height);
|
||||
|
@ -450,16 +460,17 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
max-height: calc(
|
||||
var(--max-cell-render-height) + var(--row-height) -
|
||||
var(--max-relationship-height)
|
||||
var(--max-cell-render-height) + var(--row-height) - var(--values-height)
|
||||
);
|
||||
background: var(--background);
|
||||
background: var(--grid-background-alt);
|
||||
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;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
.dropdown.invertY {
|
||||
transform: translateY(-100%);
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
quiet
|
||||
size="M"
|
||||
on:click={() => dispatch("add-column")}
|
||||
disabled={!$config.allowAddColumns}
|
||||
disabled={!$config.allowSchemaChanges}
|
||||
>
|
||||
Add column
|
||||
</ActionButton>
|
||||
|
|
|
@ -6,15 +6,9 @@
|
|||
|
||||
let modal
|
||||
|
||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||
$: selectedRowCount = Object.values($selectedRows).length
|
||||
$: rowsToDelete = Object.entries($selectedRows)
|
||||
.map(entry => {
|
||||
if (entry[1] === true) {
|
||||
return $rows.find(x => x._id === entry[0])
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
.map(entry => $rows.find(x => x._id === entry[0]))
|
||||
.filter(x => x != null)
|
||||
|
||||
// Deletion callback when confirmed
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover } from "@budibase/bbui"
|
||||
import { DefaultColumnWidth } from "../lib/constants"
|
||||
|
||||
const { stickyColumn, columns, compact } = getContext("grid")
|
||||
const smallSize = 120
|
||||
const mediumSize = DefaultColumnWidth
|
||||
const largeSize = DefaultColumnWidth * 1.5
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
||||
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
|
||||
$: allSmall = allCols.every(col => col.width === smallSize)
|
||||
$: allMedium = allCols.every(col => col.width === mediumSize)
|
||||
$: allLarge = allCols.every(col => col.width === largeSize)
|
||||
$: custom = !allSmall && !allMedium && !allLarge
|
||||
$: sizeOptions = [
|
||||
{
|
||||
label: "Small",
|
||||
size: smallSize,
|
||||
selected: allSmall,
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
size: mediumSize,
|
||||
selected: allMedium,
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
size: largeSize,
|
||||
selected: allLarge,
|
||||
},
|
||||
]
|
||||
|
||||
const changeColumnWidth = async width => {
|
||||
columns.update(state => {
|
||||
state.forEach(column => {
|
||||
column.width = width
|
||||
})
|
||||
return state
|
||||
})
|
||||
if ($stickyColumn) {
|
||||
stickyColumn.update(state => ({
|
||||
...state,
|
||||
width,
|
||||
}))
|
||||
}
|
||||
await columns.actions.saveChanges()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
<ActionButton
|
||||
icon="MoveLeftRight"
|
||||
quiet
|
||||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open}
|
||||
disabled={!allCols.length}
|
||||
tooltip={$compact ? "Width" : null}
|
||||
>
|
||||
{$compact ? "" : "Width"}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
||||
<div class="content">
|
||||
{#each sizeOptions as option}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => changeColumnWidth(option.size)}
|
||||
selected={option.selected}
|
||||
>
|
||||
{option.label}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
{#if custom}
|
||||
<ActionButton selected={custom} quiet>Custom</ActionButton>
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -3,12 +3,13 @@
|
|||
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
||||
import { getColumnIcon } from "../lib/utils"
|
||||
|
||||
const { columns, stickyColumn, compact } = getContext("grid")
|
||||
const { columns, stickyColumn } = getContext("grid")
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
||||
$: anyHidden = $columns.some(col => !col.visible)
|
||||
$: text = getText($columns)
|
||||
|
||||
const toggleVisibility = (column, visible) => {
|
||||
columns.update(state => {
|
||||
|
@ -38,6 +39,11 @@
|
|||
})
|
||||
columns.actions.saveChanges()
|
||||
}
|
||||
|
||||
const getText = columns => {
|
||||
const hidden = columns.filter(col => !col.visible).length
|
||||
return hidden ? `Hide columns (${hidden})` : "Hide columns"
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
|
@ -48,13 +54,12 @@
|
|||
on:click={() => (open = !open)}
|
||||
selected={open || anyHidden}
|
||||
disabled={!$columns.length}
|
||||
tooltip={$compact ? "Columns" : ""}
|
||||
>
|
||||
{$compact ? "" : "Columns"}
|
||||
{text}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
||||
<Popover bind:open {anchor} align="left">
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
{#if $stickyColumn}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover } from "@budibase/bbui"
|
||||
import {
|
||||
LargeRowHeight,
|
||||
MediumRowHeight,
|
||||
SmallRowHeight,
|
||||
} from "../lib/constants"
|
||||
|
||||
const { rowHeight, columns, table, compact } = 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="MoveUpDown"
|
||||
quiet
|
||||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open}
|
||||
tooltip={$compact ? "Height" : null}
|
||||
>
|
||||
{$compact ? "" : "Height"}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align={$compact ? "right" : "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>
|
|
@ -0,0 +1,135 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover, Label } from "@budibase/bbui"
|
||||
import {
|
||||
DefaultColumnWidth,
|
||||
LargeRowHeight,
|
||||
MediumRowHeight,
|
||||
SmallRowHeight,
|
||||
} from "../lib/constants"
|
||||
|
||||
const { stickyColumn, columns, rowHeight, table } = getContext("grid")
|
||||
|
||||
// Some constants for column width options
|
||||
const smallColSize = 120
|
||||
const mediumColSize = DefaultColumnWidth
|
||||
const largeColSize = DefaultColumnWidth * 1.5
|
||||
|
||||
// Row height sizes
|
||||
const rowSizeOptions = [
|
||||
{
|
||||
label: "Small",
|
||||
size: SmallRowHeight,
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
size: MediumRowHeight,
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
size: LargeRowHeight,
|
||||
},
|
||||
]
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
||||
// Column width sizes
|
||||
$: allCols = $columns.concat($stickyColumn ? [$stickyColumn] : [])
|
||||
$: allSmall = allCols.every(col => col.width === smallColSize)
|
||||
$: allMedium = allCols.every(col => col.width === mediumColSize)
|
||||
$: allLarge = allCols.every(col => col.width === largeColSize)
|
||||
$: custom = !allSmall && !allMedium && !allLarge
|
||||
$: columnSizeOptions = [
|
||||
{
|
||||
label: "Small",
|
||||
size: smallColSize,
|
||||
selected: allSmall,
|
||||
},
|
||||
{
|
||||
label: "Medium",
|
||||
size: mediumColSize,
|
||||
selected: allMedium,
|
||||
},
|
||||
{
|
||||
label: "Large",
|
||||
size: largeColSize,
|
||||
selected: allLarge,
|
||||
},
|
||||
]
|
||||
|
||||
const changeRowHeight = height => {
|
||||
columns.actions.saveTable({
|
||||
...$table,
|
||||
rowHeight: height,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
<ActionButton
|
||||
icon="MoveUpDown"
|
||||
quiet
|
||||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open}
|
||||
disabled={!allCols.length}
|
||||
>
|
||||
Size
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align="left">
|
||||
<div class="content">
|
||||
<div class="size">
|
||||
<Label>Row height</Label>
|
||||
<div class="options">
|
||||
{#each rowSizeOptions as option}
|
||||
<ActionButton
|
||||
quiet
|
||||
selected={$rowHeight === option.size}
|
||||
on:click={() => changeRowHeight(option.size)}
|
||||
>
|
||||
{option.label}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="size">
|
||||
<Label>Column width</Label>
|
||||
<div class="options">
|
||||
{#each columnSizeOptions as option}
|
||||
<ActionButton
|
||||
quiet
|
||||
on:click={() => columns.actions.changeAllColumnWidths(option.size)}
|
||||
selected={option.selected}
|
||||
>
|
||||
{option.label}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
{#if custom}
|
||||
<ActionButton selected={custom} quiet>Custom</ActionButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 12px;
|
||||
}
|
||||
.size {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.size:first-child {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||
|
||||
const { sort, columns, stickyColumn, compact } = getContext("grid")
|
||||
const { sort, columns, stickyColumn } = getContext("grid")
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
@ -90,13 +90,12 @@
|
|||
on:click={() => (open = !open)}
|
||||
selected={open}
|
||||
disabled={!columnOptions.length}
|
||||
tooltip={$compact ? "Sort" : ""}
|
||||
>
|
||||
{$compact ? "" : "Sort"}
|
||||
Sort
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<Popover bind:open {anchor} align={$compact ? "right" : "left"}>
|
||||
<Popover bind:open {anchor} align="left">
|
||||
<div class="content">
|
||||
<Select
|
||||
placeholder={null}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { setContext, onMount } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { fade } from "svelte/transition"
|
||||
import { clickOutside, ProgressCircle } from "@budibase/bbui"
|
||||
import { createEventManagers } from "../lib/events"
|
||||
|
@ -17,11 +16,8 @@
|
|||
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 ColumnWidthButton from "../controls/ColumnWidthButton.svelte"
|
||||
import SizeButton from "../controls/SizeButton.svelte"
|
||||
import NewRow from "./NewRow.svelte"
|
||||
import { createGridWebsocket } from "../lib/websocket"
|
||||
import {
|
||||
|
@ -33,48 +29,37 @@
|
|||
|
||||
export let API = null
|
||||
export let tableId = null
|
||||
export let tableType = null
|
||||
export let schemaOverrides = null
|
||||
export let columnWhitelist = 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
|
||||
export let allowSchemaChanges = true
|
||||
export let stripeRows = false
|
||||
export let collaboration = true
|
||||
export let showAvatars = true
|
||||
export let showControls = true
|
||||
export let initialFilter = null
|
||||
export let initialSortColumn = null
|
||||
export let initialSortOrder = null
|
||||
export let initialRowHeight = null
|
||||
|
||||
// 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,
|
||||
stripeRows,
|
||||
})
|
||||
|
||||
// Build up context
|
||||
let context = {
|
||||
API: API || createAPIClient(),
|
||||
rand,
|
||||
config,
|
||||
tableId: tableIdStore,
|
||||
tableType,
|
||||
schemaOverrides: schemaOverridesStore,
|
||||
props: $$props,
|
||||
}
|
||||
context = { ...context, ...createEventManagers() }
|
||||
context = attachStores(context)
|
||||
|
||||
// Reference some stores for local use
|
||||
const {
|
||||
config,
|
||||
isResizing,
|
||||
isReordering,
|
||||
ui,
|
||||
|
@ -82,19 +67,27 @@
|
|||
loading,
|
||||
rowHeight,
|
||||
contentLines,
|
||||
gridFocused,
|
||||
} = context
|
||||
|
||||
// Keep stores up to date
|
||||
$: tableIdStore.set(tableId)
|
||||
$: schemaOverridesStore.set(schemaOverrides)
|
||||
// Keep config store up to date with props
|
||||
$: config.set({
|
||||
tableId,
|
||||
schemaOverrides,
|
||||
columnWhitelist,
|
||||
allowAddRows,
|
||||
allowAddColumns,
|
||||
allowEditColumns,
|
||||
allowExpandRows,
|
||||
allowEditRows,
|
||||
allowDeleteRows,
|
||||
allowSchemaChanges,
|
||||
stripeRows,
|
||||
collaboration,
|
||||
showAvatars,
|
||||
showControls,
|
||||
initialFilter,
|
||||
initialSortColumn,
|
||||
initialSortOrder,
|
||||
initialRowHeight,
|
||||
})
|
||||
|
||||
// Set context for children to consume
|
||||
|
@ -116,25 +109,27 @@
|
|||
id="grid-{rand}"
|
||||
class:is-resizing={$isResizing}
|
||||
class:is-reordering={$isReordering}
|
||||
class:stripe={$config.stripeRows}
|
||||
class:stripe={stripeRows}
|
||||
on:mouseenter={() => gridFocused.set(true)}
|
||||
on:mouseleave={() => gridFocused.set(false)}
|
||||
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" />
|
||||
<SortButton />
|
||||
<HideColumnsButton />
|
||||
<ColumnWidthButton />
|
||||
<RowHeightButton />
|
||||
{#if showControls}
|
||||
<div class="controls">
|
||||
<div class="controls-left">
|
||||
<slot name="filter" />
|
||||
<SortButton />
|
||||
<HideColumnsButton />
|
||||
<SizeButton />
|
||||
<slot name="controls" />
|
||||
</div>
|
||||
<div class="controls-right">
|
||||
{#if showAvatars}
|
||||
<UserAvatars />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls-right">
|
||||
{#if showAvatars}
|
||||
<UserAvatars />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $loaded}
|
||||
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
|
||||
<div class="grid-data-inner">
|
||||
|
@ -167,7 +162,20 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
/* Core grid */
|
||||
.grid {
|
||||
/* Variables */
|
||||
--accent-color: var(--primaryColor, var(--spectrum-global-color-blue-400));
|
||||
--grid-background: var(--spectrum-global-color-gray-50);
|
||||
--grid-background-alt: var(--spectrum-global-color-gray-100);
|
||||
--cell-background: var(--grid-background);
|
||||
--cell-background-hover: var(--grid-background-alt);
|
||||
--cell-background-alt: var(--cell-background);
|
||||
--cell-padding: 8px;
|
||||
--cell-spacing: 4px;
|
||||
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
||||
--cell-font-size: 14px;
|
||||
--controls-height: 50px;
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -175,17 +183,7 @@
|
|||
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-background-alt: var(--cell-background);
|
||||
--cell-padding: 8px;
|
||||
--cell-spacing: 4px;
|
||||
--cell-border: 1px solid var(--spectrum-global-color-gray-200);
|
||||
--cell-font-size: 14px;
|
||||
--controls-height: 50px;
|
||||
background: var(--grid-background);
|
||||
}
|
||||
.grid,
|
||||
.grid :global(*) {
|
||||
|
@ -201,6 +199,7 @@
|
|||
--cell-background-alt: var(--spectrum-global-color-gray-75);
|
||||
}
|
||||
|
||||
/* Data layers */
|
||||
.grid-data-outer,
|
||||
.grid-data-inner {
|
||||
flex: 1 1 auto;
|
||||
|
@ -234,7 +233,7 @@
|
|||
border-bottom: 2px solid var(--spectrum-global-color-gray-200);
|
||||
padding: var(--cell-padding);
|
||||
gap: var(--cell-spacing);
|
||||
background: var(--background);
|
||||
background: var(--grid-background-alt);
|
||||
z-index: 2;
|
||||
}
|
||||
.controls-left,
|
||||
|
@ -270,7 +269,15 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--background);
|
||||
background: var(--grid-background-alt);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Disable checkbox animation anywhere in the grid data */
|
||||
.grid-data-outer :global(.spectrum-Checkbox-box:before),
|
||||
.grid-data-outer :global(.spectrum-Checkbox-box:after),
|
||||
.grid-data-outer :global(.spectrum-Checkbox-checkmark),
|
||||
.grid-data-outer :global(.spectrum-Checkbox-partialCheckmark) {
|
||||
transition: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
config,
|
||||
hoveredRowId,
|
||||
dispatch,
|
||||
isDragging,
|
||||
} = getContext("grid")
|
||||
|
||||
let body
|
||||
|
@ -47,8 +48,8 @@
|
|||
class="blank"
|
||||
class:highlighted={$hoveredRowId === BlankRowID}
|
||||
style="width:{renderColumnsWidth}px"
|
||||
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
focusedRow,
|
||||
columnHorizontalInversionIndex,
|
||||
contentLines,
|
||||
isDragging,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
|
@ -27,8 +28,8 @@
|
|||
<div
|
||||
class="row"
|
||||
on:focus
|
||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
>
|
||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
||||
{@const cellId = `${row._id}-${column.name}`}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
bounds,
|
||||
hoveredRowId,
|
||||
hiddenColumnsWidth,
|
||||
menu,
|
||||
} = getContext("grid")
|
||||
|
||||
export let scrollVertically = false
|
||||
|
@ -30,6 +31,11 @@
|
|||
const handleWheel = e => {
|
||||
e.preventDefault()
|
||||
debouncedHandleWheel(e.deltaX, e.deltaY, e.clientY)
|
||||
|
||||
// If a context menu was visible, hide it
|
||||
if ($menu.visible) {
|
||||
menu.actions.close()
|
||||
}
|
||||
}
|
||||
const debouncedHandleWheel = domDebounce((deltaX, deltaY, clientY) => {
|
||||
const { top, left } = $scroll
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
{#if $config.allowAddColumns}
|
||||
{#if $config.allowSchemaChanges}
|
||||
<div
|
||||
class="add"
|
||||
style="left:{left}px"
|
||||
|
@ -42,7 +42,7 @@
|
|||
|
||||
<style>
|
||||
.header {
|
||||
background: var(--background);
|
||||
background: var(--grid-background-alt);
|
||||
border-bottom: var(--cell-border);
|
||||
position: relative;
|
||||
height: var(--default-row-height);
|
||||
|
@ -60,7 +60,7 @@
|
|||
border-left: var(--cell-border);
|
||||
border-right: var(--cell-border);
|
||||
border-bottom: var(--cell-border);
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
background: var(--grid-background-alt);
|
||||
z-index: 1;
|
||||
}
|
||||
.add:hover {
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
maxScrollTop,
|
||||
rowVerticalInversionIndex,
|
||||
columnHorizontalInversionIndex,
|
||||
selectedRows,
|
||||
config,
|
||||
} = getContext("grid")
|
||||
|
||||
let visible = false
|
||||
|
@ -37,6 +39,7 @@
|
|||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
$: $tableId, (visible = false)
|
||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
||||
$: selectedRowCount = Object.values($selectedRows).length
|
||||
|
||||
const shouldInvertY = (offset, inversionIndex, rows) => {
|
||||
if (offset === 0) {
|
||||
|
@ -75,7 +78,7 @@
|
|||
}
|
||||
|
||||
const startAdding = async () => {
|
||||
if (visible) {
|
||||
if (visible || !firstColumn) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -129,9 +132,6 @@
|
|||
e.preventDefault()
|
||||
clear()
|
||||
}
|
||||
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
addRow()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,6 +141,18 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<!-- New row FAB -->
|
||||
{#if !visible && !selectedRowCount && $config.allowAddRows && firstColumn}
|
||||
<div
|
||||
class="new-row-fab"
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
transition:fade|local={{ duration: 130 }}
|
||||
class:offset={!$stickyColumn}
|
||||
>
|
||||
<Icon name="Add" size="S" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Only show new row functionality if we have any columns -->
|
||||
{#if visible}
|
||||
<div
|
||||
|
@ -151,7 +163,7 @@
|
|||
<div class="underlay sticky" transition:fade|local={{ duration: 130 }} />
|
||||
<div class="underlay" transition:fade|local={{ duration: 130 }} />
|
||||
<div class="sticky-column" transition:fade|local={{ duration: 130 }}>
|
||||
<GutterCell on:expand={addViaModal} rowHovered>
|
||||
<GutterCell expandable on:expand={addViaModal} rowHovered>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
{#if isAdding}
|
||||
<div in:fade={{ duration: 130 }} class="loading-overlay" />
|
||||
|
@ -227,6 +239,26 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
/* New row FAB */
|
||||
.new-row-fab {
|
||||
position: absolute;
|
||||
top: var(--default-row-height);
|
||||
left: calc(var(--gutter-width) / 2);
|
||||
transform: translateX(6px) translateY(-50%);
|
||||
background: var(--cell-background);
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
border: var(--cell-border);
|
||||
z-index: 10;
|
||||
}
|
||||
.new-row-fab:hover {
|
||||
background: var(--cell-background-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
.new-row-fab.offset {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
top: var(--default-row-height);
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
scrollLeft,
|
||||
dispatch,
|
||||
contentLines,
|
||||
isDragging,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowCount = $rows.length
|
||||
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
|
||||
$: selectedRowCount = Object.values($selectedRows).length
|
||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
|
||||
const selectAll = () => {
|
||||
|
@ -50,7 +51,6 @@
|
|||
>
|
||||
<div class="header row">
|
||||
<GutterCell
|
||||
disableExpand
|
||||
disableNumber
|
||||
on:select={selectAll}
|
||||
defaultHeight
|
||||
|
@ -71,8 +71,8 @@
|
|||
{@const cellId = `${row._id}-${$stickyColumn?.name}`}
|
||||
<div
|
||||
class="row"
|
||||
on:mouseenter={() => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
>
|
||||
<GutterCell {row} {rowFocused} {rowHovered} {rowSelected} />
|
||||
{#if $stickyColumn}
|
||||
|
@ -96,11 +96,13 @@
|
|||
{#if $config.allowAddRows && ($renderedColumns.length || $stickyColumn)}
|
||||
<div
|
||||
class="row new"
|
||||
on:mouseenter={() => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={() => ($hoveredRowId = null)}
|
||||
on:mouseenter={$isDragging
|
||||
? null
|
||||
: () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
>
|
||||
<GutterCell disableExpand rowHovered={$hoveredRowId === BlankRowID}>
|
||||
<GutterCell rowHovered={$hoveredRowId === BlankRowID}>
|
||||
<Icon name="Add" color="var(--spectrum-global-color-gray-500)" />
|
||||
</GutterCell>
|
||||
{#if $stickyColumn}
|
||||
|
@ -159,7 +161,7 @@
|
|||
z-index: 1;
|
||||
}
|
||||
.header :global(.cell) {
|
||||
background: var(--spectrum-global-color-gray-100);
|
||||
background: var(--grid-background-alt);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export const Padding = 256
|
||||
export const MaxCellRenderHeight = 252
|
||||
export const MaxCellRenderWidthOverflow = 200
|
||||
export const Padding = 246
|
||||
export const MaxCellRenderHeight = 222
|
||||
export const ScrollBarSize = 8
|
||||
export const GutterWidth = 72
|
||||
export const DefaultColumnWidth = 200
|
||||
|
@ -12,3 +11,5 @@ export const DefaultRowHeight = SmallRowHeight
|
|||
export const NewRowID = "new"
|
||||
export const BlankRowID = "blank"
|
||||
export const RowPageSize = 100
|
||||
export const FocusedCellMinOffset = 48
|
||||
export const MaxCellRenderWidthOverflow = Padding - 3 * ScrollBarSize
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createWebsocket } from "../../../utils"
|
|||
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||
|
||||
export const createGridWebsocket = context => {
|
||||
const { rows, tableId, users, focusedCellId, table } = context
|
||||
const { rows, tableId, users, focusedCellId, table, API } = context
|
||||
const socket = createWebsocket("/socket/grid")
|
||||
|
||||
const connectToTable = tableId => {
|
||||
|
@ -11,9 +11,10 @@ export const createGridWebsocket = context => {
|
|||
return
|
||||
}
|
||||
// Identify which table we are editing
|
||||
const appId = API.getAppID()
|
||||
socket.emit(
|
||||
GridSocketEvent.SelectTable,
|
||||
{ tableId },
|
||||
{ tableId, appId },
|
||||
({ users: gridUsers }) => {
|
||||
users.set(gridUsers)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
selectedRows,
|
||||
config,
|
||||
menu,
|
||||
gridFocused,
|
||||
} = getContext("grid")
|
||||
|
||||
const ignoredOriginSelectors = [
|
||||
|
@ -24,6 +25,11 @@
|
|||
|
||||
// Global key listener which intercepts all key events
|
||||
const handleKeyDown = e => {
|
||||
// Ignore completely if the grid is not focused
|
||||
if (!$gridFocused) {
|
||||
return
|
||||
}
|
||||
|
||||
// Avoid processing events sourced from certain origins
|
||||
if (e.target?.closest) {
|
||||
for (let selector of ignoredOriginSelectors) {
|
||||
|
|
|
@ -72,7 +72,9 @@
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Maximize"
|
||||
disabled={isNewRow || !$config.allowEditRows}
|
||||
disabled={isNewRow ||
|
||||
!$config.allowEditRows ||
|
||||
!$config.allowExpandRows}
|
||||
on:click={() => dispatch("edit-row", $focusedRow)}
|
||||
on:click={menu.actions.close}
|
||||
>
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
background: var(--spectrum-global-color-blue-400);
|
||||
background: var(--accent-color);
|
||||
margin-left: -2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -65,6 +65,6 @@
|
|||
margin-left: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: var(--spectrum-global-color-blue-400);
|
||||
background: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,11 +15,18 @@
|
|||
scrollLeft,
|
||||
scrollTop,
|
||||
height,
|
||||
isDragging,
|
||||
menu,
|
||||
} = getContext("grid")
|
||||
|
||||
// State for dragging bars
|
||||
let initialMouse
|
||||
let initialScroll
|
||||
let isDraggingV = false
|
||||
let isDraggingH = false
|
||||
|
||||
// Update state to reflect if we are dragging
|
||||
$: isDragging.set(isDraggingV || isDraggingH)
|
||||
|
||||
// Calculate V scrollbar size and offset
|
||||
// Terminology is the same for both axes:
|
||||
|
@ -39,6 +46,13 @@
|
|||
$: availWidth = renderWidth - barWidth
|
||||
$: barLeft = ScrollBarSize + availWidth * ($scrollLeft / $maxScrollLeft)
|
||||
|
||||
// Helper to close the context menu if it's open
|
||||
const closeMenu = () => {
|
||||
if ($menu.visible) {
|
||||
menu.actions.close()
|
||||
}
|
||||
}
|
||||
|
||||
// V scrollbar drag handlers
|
||||
const startVDragging = e => {
|
||||
e.preventDefault()
|
||||
|
@ -46,6 +60,8 @@
|
|||
initialScroll = $scrollTop
|
||||
document.addEventListener("mousemove", moveVDragging)
|
||||
document.addEventListener("mouseup", stopVDragging)
|
||||
isDraggingV = true
|
||||
closeMenu()
|
||||
}
|
||||
const moveVDragging = domDebounce(e => {
|
||||
const delta = e.clientY - initialMouse
|
||||
|
@ -59,6 +75,7 @@
|
|||
const stopVDragging = () => {
|
||||
document.removeEventListener("mousemove", moveVDragging)
|
||||
document.removeEventListener("mouseup", stopVDragging)
|
||||
isDraggingV = false
|
||||
}
|
||||
|
||||
// H scrollbar drag handlers
|
||||
|
@ -68,6 +85,8 @@
|
|||
initialScroll = $scrollLeft
|
||||
document.addEventListener("mousemove", moveHDragging)
|
||||
document.addEventListener("mouseup", stopHDragging)
|
||||
isDraggingH = true
|
||||
closeMenu()
|
||||
}
|
||||
const moveHDragging = domDebounce(e => {
|
||||
const delta = e.clientX - initialMouse
|
||||
|
@ -81,6 +100,7 @@
|
|||
const stopHDragging = () => {
|
||||
document.removeEventListener("mousemove", moveHDragging)
|
||||
document.removeEventListener("mouseup", stopHDragging)
|
||||
isDraggingH = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -89,6 +109,7 @@
|
|||
class="v-scrollbar"
|
||||
style="--size:{ScrollBarSize}px; top:{barTop}px; height:{barHeight}px;"
|
||||
on:mousedown={startVDragging}
|
||||
class:dragging={isDraggingV}
|
||||
/>
|
||||
{/if}
|
||||
{#if $showHScrollbar}
|
||||
|
@ -96,6 +117,7 @@
|
|||
class="h-scrollbar"
|
||||
style="--size:{ScrollBarSize}px; left:{barLeft}px; width:{barWidth}px;"
|
||||
on:mousedown={startHDragging}
|
||||
class:dragging={isDraggingH}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -103,11 +125,12 @@
|
|||
div {
|
||||
position: absolute;
|
||||
background: var(--spectrum-global-color-gray-500);
|
||||
opacity: 0.7;
|
||||
opacity: 0.5;
|
||||
border-radius: 4px;
|
||||
transition: opacity 130ms ease-out;
|
||||
}
|
||||
div:hover {
|
||||
div:hover,
|
||||
div.dragging {
|
||||
opacity: 1;
|
||||
}
|
||||
.v-scrollbar {
|
||||
|
|
|
@ -46,7 +46,7 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { table, columns, stickyColumn, API, dispatch } = context
|
||||
const { table, columns, stickyColumn, API, dispatch, config } = context
|
||||
|
||||
// Updates the tables primary display column
|
||||
const changePrimaryDisplay = async column => {
|
||||
|
@ -56,6 +56,23 @@ export const deriveStores = context => {
|
|||
})
|
||||
}
|
||||
|
||||
// Updates the width of all columns
|
||||
const changeAllColumnWidths = async width => {
|
||||
columns.update(state => {
|
||||
return state.map(col => ({
|
||||
...col,
|
||||
width,
|
||||
}))
|
||||
})
|
||||
if (get(stickyColumn)) {
|
||||
stickyColumn.update(state => ({
|
||||
...state,
|
||||
width,
|
||||
}))
|
||||
}
|
||||
await saveChanges()
|
||||
}
|
||||
|
||||
// Persists column changes by saving metadata against table schema
|
||||
const saveChanges = async () => {
|
||||
const $columns = get(columns)
|
||||
|
@ -90,12 +107,14 @@ export const deriveStores = context => {
|
|||
// Update local state
|
||||
table.set(newTable)
|
||||
|
||||
// Update server
|
||||
if (get(config).allowSchemaChanges) {
|
||||
await API.saveTable(newTable)
|
||||
}
|
||||
|
||||
// Broadcast change to external state can be updated, as this change
|
||||
// will not be received by the builder websocket because we caused it ourselves
|
||||
dispatch("updatetable", newTable)
|
||||
|
||||
// Update server
|
||||
await API.saveTable(newTable)
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -105,17 +124,19 @@ export const deriveStores = context => {
|
|||
saveChanges,
|
||||
saveTable,
|
||||
changePrimaryDisplay,
|
||||
changeAllColumnWidths,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const initialise = context => {
|
||||
const { table, columns, stickyColumn, schemaOverrides } = context
|
||||
const { table, columns, stickyColumn, schemaOverrides, columnWhitelist } =
|
||||
context
|
||||
|
||||
const schema = derived(
|
||||
[table, schemaOverrides],
|
||||
([$table, $schemaOverrides]) => {
|
||||
[table, schemaOverrides, columnWhitelist],
|
||||
([$table, $schemaOverrides, $columnWhitelist]) => {
|
||||
if (!$table?.schema) {
|
||||
return null
|
||||
}
|
||||
|
@ -142,6 +163,16 @@ export const initialise = context => {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Apply whitelist if specified
|
||||
if ($columnWhitelist?.length) {
|
||||
Object.keys(newSchema).forEach(key => {
|
||||
if (!$columnWhitelist.includes(key)) {
|
||||
delete newSchema[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newSchema
|
||||
}
|
||||
)
|
||||
|
@ -209,7 +240,7 @@ export const initialise = context => {
|
|||
}
|
||||
stickyColumn.set({
|
||||
name: primaryDisplay,
|
||||
label: $schema[primaryDisplay].name || primaryDisplay,
|
||||
label: $schema[primaryDisplay].displayName || primaryDisplay,
|
||||
schema: $schema[primaryDisplay],
|
||||
width: $schema[primaryDisplay].width || DefaultColumnWidth,
|
||||
visible: true,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue