Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes
This commit is contained in:
commit
bc8ebc42f9
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.9.33-alpha.5",
|
||||
"version": "2.9.33-alpha.7",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
import FancyField from "./FancyField.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import Popover from "../Popover/Popover.svelte"
|
||||
import FancyFieldLabel from "./FancyFieldLabel.svelte"
|
||||
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||
import Picker from "../Form/Core/Picker.svelte"
|
||||
|
||||
export let label
|
||||
export let value
|
||||
|
@ -11,18 +12,30 @@
|
|||
export let error = null
|
||||
export let validate = null
|
||||
export let options = []
|
||||
export let isOptionEnabled = () => true
|
||||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
|
||||
export let getOptionSubtitle = option => extractProperty(option, "subtitle")
|
||||
export let getOptionColour = () => null
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let open = false
|
||||
let popover
|
||||
let wrapper
|
||||
|
||||
$: placeholder = !value
|
||||
$: selectedLabel = getSelectedLabel(value)
|
||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||
|
||||
const getFieldAttribute = (getAttribute, value, options) => {
|
||||
// Wait for options to load if there is a value but no options
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
const index = options.findIndex(
|
||||
(option, idx) => getOptionValue(option, idx) === value
|
||||
)
|
||||
return index !== -1 ? getAttribute(options[index], index) : null
|
||||
}
|
||||
const extractProperty = (value, property) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
|
@ -64,46 +77,45 @@
|
|||
<FancyFieldLabel {placeholder}>{label}</FancyFieldLabel>
|
||||
{/if}
|
||||
|
||||
{#if fieldColour}
|
||||
<span class="align">
|
||||
<StatusLight square color={fieldColour} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div class="value" class:placeholder>
|
||||
{selectedLabel || ""}
|
||||
</div>
|
||||
|
||||
<div class="arrow">
|
||||
<div class="align arrow-alignment">
|
||||
<Icon name="ChevronDown" />
|
||||
</div>
|
||||
</FancyField>
|
||||
|
||||
<Popover
|
||||
anchor={wrapper}
|
||||
align="left"
|
||||
portalTarget={document.documentElement}
|
||||
bind:this={popover}
|
||||
{open}
|
||||
on:close={() => (open = false)}
|
||||
useAnchorWidth={true}
|
||||
maxWidth={null}
|
||||
>
|
||||
<div class="popover-content">
|
||||
{#if options.length}
|
||||
{#each options as option, idx}
|
||||
<div
|
||||
class="popover-option"
|
||||
tabindex="0"
|
||||
on:click={() => onChange(getOptionValue(option, idx))}
|
||||
>
|
||||
<span class="option-text">
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
{#if value === getOptionValue(option, idx)}
|
||||
<Icon name="Checkmark" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
<div id="picker-wrapper">
|
||||
<Picker
|
||||
customAnchor={wrapper}
|
||||
onlyPopover={true}
|
||||
bind:open
|
||||
{error}
|
||||
{disabled}
|
||||
{options}
|
||||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
{getOptionSubtitle}
|
||||
{getOptionColour}
|
||||
{isOptionEnabled}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
onSelectOption={onChange}
|
||||
isOptionSelected={option => option === value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#picker-wrapper :global(.spectrum-Picker) {
|
||||
display: none;
|
||||
}
|
||||
.value {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
|
@ -118,30 +130,23 @@
|
|||
width: 0;
|
||||
transform: translateY(9px);
|
||||
}
|
||||
|
||||
.align {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 17px;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
transition: transform 130ms ease-out, opacity 130ms ease-out;
|
||||
transform: translateY(9px);
|
||||
}
|
||||
|
||||
.arrow-alignment {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.value.placeholder {
|
||||
transform: translateY(0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
.popover-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
padding: 7px 0;
|
||||
}
|
||||
.popover-option {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 7px 16px;
|
||||
transition: background 130ms ease-out;
|
||||
font-size: 15px;
|
||||
}
|
||||
.popover-option:hover {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import Icon from "../../Icon/Icon.svelte"
|
||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||
import Popover from "../../Popover/Popover.svelte"
|
||||
import Tags from "../../Tags/Tags.svelte"
|
||||
import Tag from "../../Tags/Tag.svelte"
|
||||
|
||||
export let id = null
|
||||
export let disabled = false
|
||||
|
@ -26,6 +28,7 @@
|
|||
export let getOptionIcon = () => null
|
||||
export let useOptionIconImage = false
|
||||
export let getOptionColour = () => null
|
||||
export let getOptionSubtitle = () => null
|
||||
export let open = false
|
||||
export let readonly = false
|
||||
export let quiet = false
|
||||
|
@ -37,7 +40,7 @@
|
|||
export let customPopoverHeight
|
||||
export let align = "left"
|
||||
export let footer = null
|
||||
|
||||
export let customAnchor = null
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let searchTerm = null
|
||||
|
@ -99,7 +102,7 @@
|
|||
bind:this={button}
|
||||
>
|
||||
{#if fieldIcon}
|
||||
{#if !useOptionIconImage}
|
||||
{#if !useOptionIconImage}x
|
||||
<span class="option-extra icon">
|
||||
<Icon size="S" name={fieldIcon} />
|
||||
</span>
|
||||
|
@ -139,9 +142,8 @@
|
|||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Popover
|
||||
anchor={button}
|
||||
anchor={customAnchor ? customAnchor : button}
|
||||
align={align || "left"}
|
||||
bind:this={popover}
|
||||
{open}
|
||||
|
@ -215,8 +217,21 @@
|
|||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{#if getOptionSubtitle(option, idx)}
|
||||
<span class="subtitle-text"
|
||||
>{getOptionSubtitle(option, idx)}</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
{#if option.tag}
|
||||
<span class="option-tag">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">{option.tag}</Tag>
|
||||
</Tags>
|
||||
</span>
|
||||
{/if}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
|
@ -242,6 +257,17 @@
|
|||
width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
top: 10px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.spectrum-Picker-label.auto-width {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
@ -321,4 +347,12 @@
|
|||
.option-extra.icon.field-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.option-tag {
|
||||
margin: 0 var(--spacing-m) 0 var(--spacing-m);
|
||||
}
|
||||
|
||||
.option-tag :global(.spectrum-Tags-item > .spectrum-Icon) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
export let sort = false
|
||||
export let align
|
||||
export let footer = null
|
||||
|
||||
export let tag = null
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let open = false
|
||||
|
@ -83,6 +83,7 @@
|
|||
{isOptionEnabled}
|
||||
{autocomplete}
|
||||
{sort}
|
||||
{tag}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
isOptionSelected={option => option === value}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
export let customPopoverHeight
|
||||
export let align
|
||||
export let footer = null
|
||||
|
||||
export let tag = null
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -61,6 +61,7 @@
|
|||
{isOptionEnabled}
|
||||
{autocomplete}
|
||||
{customPopoverHeight}
|
||||
{tag}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let fixed = false
|
||||
export let inline = false
|
||||
export let disableCancel = false
|
||||
export let autoFocus = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let visible = fixed || inline
|
||||
|
@ -53,6 +54,9 @@
|
|||
}
|
||||
|
||||
async function focusModal(node) {
|
||||
if (!autoFocus) {
|
||||
return
|
||||
}
|
||||
await tick()
|
||||
|
||||
// Try to focus first input
|
||||
|
|
|
@ -57,10 +57,8 @@
|
|||
function calculateIndicatorLength() {
|
||||
if (!vertical) {
|
||||
width = $tab.info?.width + "px"
|
||||
height = $tab.info?.height
|
||||
} else {
|
||||
height = $tab.info?.height + 4 + "px"
|
||||
width = $tab.info?.width
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -351,12 +351,19 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
schema = info.schema
|
||||
table = info.table
|
||||
|
||||
// For JSON arrays, use the array name as the readable prefix.
|
||||
// Otherwise use the table name
|
||||
// Determine what to prefix bindings with
|
||||
if (datasource.type === "jsonarray") {
|
||||
// For JSON arrays, use the array name as the readable prefix
|
||||
const split = datasource.label.split(".")
|
||||
readablePrefix = split[split.length - 1]
|
||||
} else if (datasource.type === "viewV2") {
|
||||
// For views, use the view name
|
||||
const view = Object.values(table?.views || {}).find(
|
||||
view => view.id === datasource.id
|
||||
)
|
||||
readablePrefix = view?.name
|
||||
} else {
|
||||
// Otherwise use the table name
|
||||
readablePrefix = info.table?.name
|
||||
}
|
||||
}
|
||||
|
@ -464,7 +471,7 @@ const getComponentBindingCategory = (component, context, def) => {
|
|||
*/
|
||||
export const getUserBindings = () => {
|
||||
let bindings = []
|
||||
const { schema } = getSchemaForTable(TableNames.USERS)
|
||||
const { schema } = getSchemaForDatasourcePlus(TableNames.USERS)
|
||||
const keys = Object.keys(schema).sort()
|
||||
const safeUser = makePropSafe("user")
|
||||
|
||||
|
@ -714,17 +721,25 @@ export const getActionBindings = (actions, actionId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the schema for a certain table ID.
|
||||
* Gets the schema for a certain datasource plus.
|
||||
* The options which can be passed in are:
|
||||
* formSchema: whether the schema is for a form
|
||||
* searchableSchema: whether to generate a searchable schema, which may have
|
||||
* fewer fields than a readable schema
|
||||
* @param tableId the table ID to get the schema for
|
||||
* @param resourceId the DS+ resource ID
|
||||
* @param options options for generating the schema
|
||||
* @return {{schema: Object, table: Object}}
|
||||
*/
|
||||
export const getSchemaForTable = (tableId, options) => {
|
||||
return getSchemaForDatasource(null, { type: "table", tableId }, options)
|
||||
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
||||
const isViewV2 = resourceId?.includes("view_")
|
||||
const datasource = isViewV2
|
||||
? {
|
||||
type: "viewV2",
|
||||
id: resourceId,
|
||||
tableId: resourceId.split("_").slice(1, 3).join("_"),
|
||||
}
|
||||
: { type: "table", tableId: resourceId }
|
||||
return getSchemaForDatasource(null, datasource, options)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -801,9 +816,21 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
// Determine the schema from the backing entity if not already determined
|
||||
if (table && !schema) {
|
||||
if (type === "view") {
|
||||
// For views, the schema is pulled from the `views` property of the
|
||||
// table
|
||||
// Old views
|
||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||
} else if (type === "viewV2") {
|
||||
// New views which are DS+
|
||||
const view = Object.values(table.views || {}).find(
|
||||
view => view.id === datasource.id
|
||||
)
|
||||
schema = cloneDeep(view?.schema)
|
||||
|
||||
// Strip hidden fields
|
||||
Object.keys(schema || {}).forEach(field => {
|
||||
if (!schema[field].visible) {
|
||||
delete schema[field]
|
||||
}
|
||||
})
|
||||
} else if (
|
||||
type === "query" &&
|
||||
(options.formSchema || options.searchableSchema)
|
||||
|
@ -849,12 +876,12 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
|
||||
// Determine if we should add ID and rev to the schema
|
||||
const isInternal = table && !table.sql
|
||||
const isTable = ["table", "link"].includes(datasource.type)
|
||||
const isDSPlus = ["table", "link", "viewV2"].includes(datasource.type)
|
||||
|
||||
// ID is part of the readable schema for all tables
|
||||
// Rev is part of the readable schema for internal tables only
|
||||
let addId = isTable
|
||||
let addRev = isTable && isInternal
|
||||
let addId = isDSPlus
|
||||
let addRev = isDSPlus && isInternal
|
||||
|
||||
// Don't add ID or rev for form schemas
|
||||
if (options.formSchema) {
|
||||
|
@ -864,7 +891,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
|
||||
// ID is only searchable for internal tables
|
||||
else if (options.searchableSchema) {
|
||||
addId = isTable && isInternal
|
||||
addId = isDSPlus && isInternal
|
||||
}
|
||||
|
||||
// Add schema properties if required
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import rowListScreen from "./rowListScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
|
||||
const allTemplates = tables => [...rowListScreen(tables)]
|
||||
const allTemplates = datasources => [...rowListScreen(datasources)]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, template) => () => {
|
||||
const createTemplateOverride = template => () => {
|
||||
const screen = template.create()
|
||||
screen.name = screen.props._id
|
||||
screen.routing.route = screen.routing.route.toLowerCase()
|
||||
|
@ -12,14 +12,13 @@ const createTemplateOverride = (frontendState, template) => () => {
|
|||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, tables) => {
|
||||
export default datasources => {
|
||||
const enrichTemplate = template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template),
|
||||
create: createTemplateOverride(template),
|
||||
})
|
||||
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
const tableTemplates = allTemplates(tables).map(enrichTemplate)
|
||||
const tableTemplates = allTemplates(datasources).map(enrichTemplate)
|
||||
return [
|
||||
fromScratch,
|
||||
...tableTemplates.sort((templateA, templateB) => {
|
||||
|
|
|
@ -2,31 +2,26 @@ import sanitizeUrl from "./utils/sanitizeUrl"
|
|||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
|
||||
export default function (tables) {
|
||||
return tables.map(table => {
|
||||
export default function (datasources) {
|
||||
return datasources.map(datasource => {
|
||||
return {
|
||||
name: `${table.name} - List`,
|
||||
create: () => createScreen(table),
|
||||
name: `${datasource.name} - List`,
|
||||
create: () => createScreen(datasource),
|
||||
id: ROW_LIST_TEMPLATE,
|
||||
table: table._id,
|
||||
resourceId: datasource.resourceId,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
|
||||
export const rowListUrl = table => sanitizeUrl(`/${table.name}`)
|
||||
export const rowListUrl = datasource => sanitizeUrl(`/${datasource.name}`)
|
||||
|
||||
const generateTableBlock = table => {
|
||||
const generateTableBlock = datasource => {
|
||||
const tableBlock = new Component("@budibase/standard-components/tableblock")
|
||||
tableBlock
|
||||
.customProps({
|
||||
title: table.name,
|
||||
dataSource: {
|
||||
label: table.name,
|
||||
name: table._id,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
title: datasource.name,
|
||||
dataSource: datasource,
|
||||
sortOrder: "Ascending",
|
||||
size: "spectrum--medium",
|
||||
paginate: true,
|
||||
|
@ -36,14 +31,14 @@ const generateTableBlock = table => {
|
|||
titleButtonText: "Create row",
|
||||
titleButtonClickBehaviour: "new",
|
||||
})
|
||||
.instanceName(`${table.name} - Table block`)
|
||||
.instanceName(`${datasource.name} - Table block`)
|
||||
return tableBlock
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
const createScreen = datasource => {
|
||||
return new Screen()
|
||||
.route(rowListUrl(table))
|
||||
.instanceName(`${table.name} - List`)
|
||||
.addChild(generateTableBlock(table))
|
||||
.route(rowListUrl(datasource))
|
||||
.instanceName(`${datasource.name} - List`)
|
||||
.addChild(generateTableBlock(datasource))
|
||||
.json()
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
import { LuceneUtils } from "@budibase/frontend-core"
|
||||
import {
|
||||
getSchemaForTable,
|
||||
getSchemaForDatasourcePlus,
|
||||
getEnvironmentBindings,
|
||||
} from "builderStore/dataBinding"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
@ -67,7 +67,9 @@
|
|||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === inputData.tableId)
|
||||
: { schema: {} }
|
||||
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
||||
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === "TRIGGER"
|
||||
|
@ -158,7 +160,7 @@
|
|||
// instead fetch the schema in the backend at runtime.
|
||||
let schema
|
||||
if (e.detail?.tableId) {
|
||||
schema = getSchemaForTable(e.detail.tableId, {
|
||||
schema = getSchemaForDatasourcePlus(e.detail.tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
}
|
||||
|
|
|
@ -26,12 +26,14 @@
|
|||
$: id = $tables.selected?._id
|
||||
$: isUsersTable = id === TableNames.USERS
|
||||
$: isInternal = $tables.selected?.type !== "external"
|
||||
|
||||
$: datasource = $datasources.list.find(datasource => {
|
||||
$: gridDatasource = {
|
||||
type: "table",
|
||||
tableId: id,
|
||||
}
|
||||
$: tableDatasource = $datasources.list.find(datasource => {
|
||||
return datasource._id === $tables.selected?.sourceId
|
||||
})
|
||||
|
||||
$: relationshipsEnabled = relationshipSupport(datasource)
|
||||
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
||||
|
||||
const relationshipSupport = datasource => {
|
||||
const integration = $integrations[datasource?.source]
|
||||
|
@ -54,12 +56,12 @@
|
|||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
datasource={gridDatasource}
|
||||
canAddRows={!isUsersTable}
|
||||
canDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
on:updatetable={handleGridTableUpdate}
|
||||
on:updatedatasource={handleGridTableUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
|
@ -72,9 +74,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
<GridCreateViewButton />
|
||||
{/if}
|
||||
<GridCreateViewButton />
|
||||
<GridManageAccessButton />
|
||||
{#if relationshipsEnabled}
|
||||
<GridRelationshipButton />
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import { viewsV2 } from "stores/backend"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
|
||||
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
|
||||
|
||||
$: id = $viewsV2.selected?.id
|
||||
$: datasource = {
|
||||
type: "viewV2",
|
||||
id,
|
||||
tableId: $viewsV2.selected?.tableId,
|
||||
}
|
||||
|
||||
const handleGridViewUpdate = async e => {
|
||||
viewsV2.replaceView(id, e.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
{datasource}
|
||||
allowAddRows
|
||||
allowDeleteRows
|
||||
showAvatars={false}
|
||||
on:updatedatasource={handleGridViewUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls">
|
||||
<GridCreateEditRowModal />
|
||||
<GridManageAccessButton />
|
||||
</svelte:fragment>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
flex: 1 1 auto;
|
||||
margin: -28px -40px -40px -40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export let resourceId
|
||||
export let disabled = false
|
||||
export let requiresLicence
|
||||
|
||||
let modal
|
||||
let resourcePermissions
|
||||
|
@ -21,6 +22,7 @@
|
|||
<Modal bind:this={modal}>
|
||||
<ManageAccessModal
|
||||
{resourceId}
|
||||
{requiresLicence}
|
||||
levels={$permissions}
|
||||
permissions={resourcePermissions}
|
||||
/>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
$: tempValue = filters || []
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(filters)
|
||||
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
|
||||
|
||||
const getText = filters => {
|
||||
const count = filters?.filter(filter => filter.field)?.length
|
||||
|
@ -22,13 +23,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
icon="Filter"
|
||||
quiet
|
||||
{disabled}
|
||||
on:click={modal.show}
|
||||
selected={tempValue?.length > 0}
|
||||
>
|
||||
<ActionButton icon="Filter" quiet {disabled} on:click={modal.show} {selected}>
|
||||
{text}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../../modals/CreateViewModal.svelte"
|
||||
import GridCreateViewModal from "../../modals/grid/GridCreateViewModal.svelte"
|
||||
|
||||
const { rows, columns } = getContext("grid")
|
||||
|
||||
|
@ -14,5 +14,5 @@
|
|||
Add view
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
<GridCreateViewModal />
|
||||
</Modal>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import ExportButton from "../ExportButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { rows, columns, tableId, sort, selectedRows, filter } =
|
||||
const { rows, columns, datasource, sort, selectedRows, filter } =
|
||||
getContext("grid")
|
||||
|
||||
$: disabled = !$rows.length || !$columns.length
|
||||
|
@ -12,7 +12,7 @@
|
|||
<span data-ignore-click-outside="true">
|
||||
<ExportButton
|
||||
{disabled}
|
||||
view={$tableId}
|
||||
view={$datasource.tableId}
|
||||
filters={$filter}
|
||||
sorting={{
|
||||
sortColumn: $sort.column,
|
||||
|
|
|
@ -2,22 +2,19 @@
|
|||
import TableFilterButton from "../TableFilterButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { columns, tableId, filter, table } = getContext("grid")
|
||||
|
||||
// Wipe filter whenever table ID changes to avoid using stale filters
|
||||
$: $tableId, filter.set([])
|
||||
const { columns, datasource, filter, definition } = getContext("grid")
|
||||
|
||||
const onFilter = e => {
|
||||
filter.set(e.detail || [])
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key $tableId}
|
||||
{#key $datasource}
|
||||
<TableFilterButton
|
||||
schema={$table?.schema}
|
||||
schema={$definition?.schema}
|
||||
filters={$filter}
|
||||
on:change={onFilter}
|
||||
disabled={!$columns.length}
|
||||
tableId={$tableId}
|
||||
tableId={$datasource.tableId}
|
||||
/>
|
||||
{/key}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId, table } = getContext("grid")
|
||||
const { rows, datasource, definition } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
tableType={$table?.type}
|
||||
tableId={$datasource?.tableId}
|
||||
tableType={$definition?.type}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
||||
|
|
|
@ -1,8 +1,29 @@
|
|||
<script>
|
||||
import { licensing, admin } from "stores/portal"
|
||||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { tableId } = getContext("grid")
|
||||
const { datasource } = getContext("grid")
|
||||
|
||||
$: resourceId = getResourceID($datasource)
|
||||
|
||||
const getResourceID = datasource => {
|
||||
if (!datasource) {
|
||||
return null
|
||||
}
|
||||
return datasource.type === "table" ? datasource.tableId : datasource.id
|
||||
}
|
||||
|
||||
var requiresLicence
|
||||
$: {
|
||||
if ($datasource.type === "viewV2" && !$licensing.isViewPermissionsEnabled) {
|
||||
const requiredLicense = $admin?.cloud ? "Premium" : "Business"
|
||||
requiresLicence = {
|
||||
tier: requiredLicense,
|
||||
message: `A ${requiredLicense} subscription is required to specify access level roles for this view.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ManageAccessButton resourceId={$tableId} />
|
||||
<ManageAccessButton {resourceId} {requiresLicence} />
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { table, rows } = getContext("grid")
|
||||
const { definition, rows } = getContext("grid")
|
||||
</script>
|
||||
|
||||
{#if $table}
|
||||
{#if $definition}
|
||||
<ExistingRelationshipButton
|
||||
table={$table}
|
||||
table={$definition}
|
||||
on:updatecolumns={() => rows.actions.refreshData()}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
<script>
|
||||
import { Input, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { views as viewsStore } from "stores/backend"
|
||||
import { tables } from "stores/backend"
|
||||
|
||||
let name
|
||||
let field
|
||||
|
||||
$: views = $tables.list.flatMap(table => Object.keys(table.views || {}))
|
||||
|
||||
const saveView = async () => {
|
||||
name = name?.trim()
|
||||
if (views.includes(name)) {
|
||||
notifications.error(`View exists with name ${name}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await viewsStore.save({
|
||||
name,
|
||||
tableId: $tables.selected._id,
|
||||
field,
|
||||
})
|
||||
notifications.success(`View ${name} created`)
|
||||
$goto(`../../view/${encodeURIComponent(name)}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating view")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create View"
|
||||
confirmText="Create View"
|
||||
onConfirm={saveView}
|
||||
>
|
||||
<Input label="View Name" thin bind:value={name} />
|
||||
</ModalContent>
|
|
@ -7,11 +7,14 @@
|
|||
notifications,
|
||||
Body,
|
||||
ModalContent,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let resourceId
|
||||
export let permissions
|
||||
export let requiresLicence
|
||||
|
||||
async function changePermission(level, role) {
|
||||
try {
|
||||
|
@ -30,22 +33,36 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ModalContent title="Manage Access" showCancelButton={false} confirmText="Done">
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(permissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
value={permissions[level]}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={$roles}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<ModalContent showCancelButton={false} confirmText="Done">
|
||||
<span slot="header">
|
||||
Manage Access
|
||||
{#if requiresLicence}
|
||||
<span class="lock-tag">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">{requiresLicence.tier}</Tag>
|
||||
</Tags>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
{#if requiresLicence}
|
||||
<Body size="S">{requiresLicence.message}</Body>
|
||||
{:else}
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(permissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
value={permissions[level]}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={$roles}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
|
@ -54,4 +71,8 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.lock-tag {
|
||||
padding-left: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
|
||||
|
||||
const { rows } = getContext("grid")
|
||||
const { datasource } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />
|
||||
<CreateEditColumn on:updatecolumns={datasource.actions.refreshDefinition} />
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Input, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { viewsV2 } from "stores/backend"
|
||||
|
||||
const { filter, sort, definition } = getContext("grid")
|
||||
|
||||
let name
|
||||
|
||||
$: views = Object.keys($definition?.views || {}).map(x => x.toLowerCase())
|
||||
$: nameExists = views.includes(name?.trim().toLowerCase())
|
||||
|
||||
const enrichSchema = schema => {
|
||||
// We need to sure that "visible" is set to true for any fields which have
|
||||
// not yet been saved with grid metadata attached
|
||||
const cloned = { ...schema }
|
||||
Object.entries(cloned).forEach(([field, fieldSchema]) => {
|
||||
if (fieldSchema.visible == null) {
|
||||
cloned[field] = { ...cloned[field], visible: true }
|
||||
}
|
||||
})
|
||||
return cloned
|
||||
}
|
||||
|
||||
const saveView = async () => {
|
||||
name = name?.trim()
|
||||
try {
|
||||
const newView = await viewsV2.create({
|
||||
name,
|
||||
tableId: $definition._id,
|
||||
query: $filter,
|
||||
sort: {
|
||||
field: $sort.column,
|
||||
order: $sort.order,
|
||||
},
|
||||
schema: enrichSchema($definition.schema),
|
||||
primaryDisplay: $definition.primaryDisplay,
|
||||
})
|
||||
notifications.success(`View ${name} created`)
|
||||
$goto(`../../view/v2/${newView.id}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating view")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create View"
|
||||
confirmText="Create View"
|
||||
onConfirm={saveView}
|
||||
disabled={nameExists}
|
||||
>
|
||||
<Input
|
||||
label="View Name"
|
||||
thin
|
||||
bind:value={name}
|
||||
error={nameExists ? "A view already exists with that name" : null}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -1,7 +1,14 @@
|
|||
<script>
|
||||
import { goto, isActive, params } from "@roxi/routify"
|
||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||
import { database, datasources, queries, tables, views } from "stores/backend"
|
||||
import {
|
||||
database,
|
||||
datasources,
|
||||
queries,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
} from "stores/backend"
|
||||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -24,6 +31,7 @@
|
|||
$tables,
|
||||
$queries,
|
||||
$views,
|
||||
$viewsV2,
|
||||
openDataSources
|
||||
)
|
||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||
|
@ -41,6 +49,7 @@
|
|||
tables,
|
||||
queries,
|
||||
views,
|
||||
viewsV2,
|
||||
openDataSources
|
||||
) => {
|
||||
if (!datasources?.list?.length) {
|
||||
|
@ -57,7 +66,8 @@
|
|||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
views,
|
||||
viewsV2
|
||||
)
|
||||
const onlySource = datasources.list.length === 1
|
||||
return {
|
||||
|
@ -106,7 +116,8 @@
|
|||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
views,
|
||||
viewsV2
|
||||
) => {
|
||||
// Check for being on a datasource page
|
||||
if (params.datasourceId === datasource._id) {
|
||||
|
@ -152,10 +163,16 @@
|
|||
|
||||
// Check for a matching view
|
||||
const selectedView = views.selected?.name
|
||||
const table = options.find(table => {
|
||||
const viewTable = options.find(table => {
|
||||
return table.views?.[selectedView] != null
|
||||
})
|
||||
return table != null
|
||||
if (viewTable) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for a matching viewV2
|
||||
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
|
||||
return viewV2Table != null
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { tables, views, database } from "stores/backend"
|
||||
import { tables, views, viewsV2, database } from "stores/backend"
|
||||
import { TableNames } from "constants"
|
||||
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||
|
@ -7,9 +7,6 @@
|
|||
import { goto, isActive } from "@roxi/routify"
|
||||
import { userSelectedResourceMap } from "builderStore"
|
||||
|
||||
const alphabetical = (a, b) =>
|
||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
|
||||
export let sourceId
|
||||
export let selectTable
|
||||
|
||||
|
@ -18,6 +15,17 @@
|
|||
table => table.sourceId === sourceId && table._id !== TableNames.USERS
|
||||
)
|
||||
.sort(alphabetical)
|
||||
|
||||
const alphabetical = (a, b) => {
|
||||
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
}
|
||||
|
||||
const isViewActive = (view, isActive, views, viewsV2) => {
|
||||
return (
|
||||
(isActive("./view/v1") && views.selected?.name === view.name) ||
|
||||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $database?._id}
|
||||
|
@ -37,18 +45,23 @@
|
|||
<EditTablePopover {table} />
|
||||
{/if}
|
||||
</NavItem>
|
||||
{#each [...Object.keys(table.views || {})].sort() as viewName, idx (idx)}
|
||||
{#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
|
||||
<NavItem
|
||||
indentLevel={2}
|
||||
icon="Remove"
|
||||
text={viewName}
|
||||
selected={$isActive("./view") && $views.selected?.name === viewName}
|
||||
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
||||
selectedBy={$userSelectedResourceMap[viewName]}
|
||||
text={name}
|
||||
selected={isViewActive(view, $isActive, $views, $viewsV2)}
|
||||
on:click={() => {
|
||||
if (view.version === 2) {
|
||||
$goto(`./view/v2/${view.id}`)
|
||||
} else {
|
||||
$goto(`./view/v1/${encodeURIComponent(name)}`)
|
||||
}
|
||||
}}
|
||||
selectedBy={$userSelectedResourceMap[name] ||
|
||||
$userSelectedResourceMap[view.id]}
|
||||
>
|
||||
<EditViewPopover
|
||||
view={{ name: viewName, ...table.views[viewName] }}
|
||||
/>
|
||||
<EditViewPopover {view} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
{/each}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
screen => screen.autoTableId === table._id
|
||||
)
|
||||
willBeDeleted = ["All table data"].concat(
|
||||
templateScreens.map(screen => `Screen ${screen.props._instanceName}`)
|
||||
templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`)
|
||||
)
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
@ -44,7 +44,10 @@
|
|||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete(table)
|
||||
await store.actions.screens.delete(templateScreens)
|
||||
// Screens need deleted one at a time because of undo/redo
|
||||
for (let screen of templateScreens) {
|
||||
await store.actions.screens.delete(screen)
|
||||
}
|
||||
if (table.type === "external") {
|
||||
await datasources.fetch()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { views } from "stores/backend"
|
||||
import { views, viewsV2 } from "stores/backend"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
|
@ -24,23 +23,29 @@
|
|||
const updatedView = cloneDeep(view)
|
||||
updatedView.name = updatedName
|
||||
|
||||
await views.save({
|
||||
originalName,
|
||||
...updatedView,
|
||||
})
|
||||
if (view.version === 2) {
|
||||
await viewsV2.save({
|
||||
originalName,
|
||||
...updatedView,
|
||||
})
|
||||
} else {
|
||||
await views.save({
|
||||
originalName,
|
||||
...updatedView,
|
||||
})
|
||||
}
|
||||
|
||||
notifications.success("View renamed successfully")
|
||||
}
|
||||
|
||||
async function deleteView() {
|
||||
try {
|
||||
const isSelected =
|
||||
decodeURIComponent($params.viewName) === $views.selectedViewName
|
||||
const id = view.tableId
|
||||
await views.delete(view)
|
||||
notifications.success("View deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./table/${id}`)
|
||||
if (view.version === 2) {
|
||||
await viewsV2.delete(view)
|
||||
} else {
|
||||
await views.delete(view)
|
||||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
|
|
|
@ -109,7 +109,13 @@
|
|||
type: "View",
|
||||
name: view.name,
|
||||
icon: "Remove",
|
||||
action: () => $goto(`./data/view/${view.name}`),
|
||||
action: () => {
|
||||
if (view.version === 2) {
|
||||
$goto(`./data/view/v2/${view.id}`)
|
||||
} else {
|
||||
$goto(`./data/view/${view.name}`)
|
||||
}
|
||||
},
|
||||
})) ?? []),
|
||||
...($queries?.list?.map(query => ({
|
||||
type: "Query",
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, FancySelect } from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
|
@ -15,17 +18,43 @@
|
|||
export let align
|
||||
export let footer = null
|
||||
export let allowedRoles = null
|
||||
export let allowCreator = false
|
||||
export let fancySelect = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const RemoveID = "remove"
|
||||
|
||||
$: options = getOptions($roles, allowPublic, allowRemove, allowedRoles)
|
||||
|
||||
const getOptions = (roles, allowPublic, allowRemove, allowedRoles) => {
|
||||
$: options = getOptions(
|
||||
$roles,
|
||||
allowPublic,
|
||||
allowRemove,
|
||||
allowedRoles,
|
||||
allowCreator
|
||||
)
|
||||
const getOptions = (
|
||||
roles,
|
||||
allowPublic,
|
||||
allowRemove,
|
||||
allowedRoles,
|
||||
allowCreator
|
||||
) => {
|
||||
if (allowedRoles?.length) {
|
||||
return roles.filter(role => allowedRoles.includes(role._id))
|
||||
}
|
||||
let newRoles = [...roles]
|
||||
|
||||
if (allowCreator) {
|
||||
newRoles = [
|
||||
{
|
||||
_id: Constants.Roles.CREATOR,
|
||||
name: "Creator",
|
||||
tag:
|
||||
!$licensing.perAppBuildersEnabled &&
|
||||
capitalise(Constants.PlanType.BUSINESS),
|
||||
},
|
||||
...newRoles,
|
||||
]
|
||||
}
|
||||
if (allowRemove) {
|
||||
newRoles = [
|
||||
...newRoles,
|
||||
|
@ -64,19 +93,45 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Select
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
{options}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
{#if fancySelect}
|
||||
<FancySelect
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
{options}
|
||||
label="Access on this app"
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
isOptionEnabled={option =>
|
||||
option._id !== Constants.Roles.CREATOR ||
|
||||
$licensing.perAppBuildersEnabled}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
{:else}
|
||||
<Select
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
{options}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
isOptionEnabled={option =>
|
||||
option._id !== Constants.Roles.CREATOR ||
|
||||
$licensing.perAppBuildersEnabled}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
<script>
|
||||
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
$: tableOptions = $tables.list || []
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -15,9 +23,9 @@
|
|||
<Label>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
{options}
|
||||
getOptionLabel={x => x.label}
|
||||
getOptionValue={x => x.resourceId}
|
||||
/>
|
||||
|
||||
<Label small>Row IDs</Label>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import {
|
||||
getContextProviderComponents,
|
||||
getSchemaForTable,
|
||||
getSchemaForDatasourcePlus,
|
||||
} from "builderStore/dataBinding"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
|
||||
|
@ -23,7 +23,15 @@
|
|||
)
|
||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
||||
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
|
||||
$: tableOptions = $tables.list || []
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
|
||||
// Gets a context definition of a certain type from a component definition
|
||||
const extractComponentContext = (component, contextType) => {
|
||||
|
@ -60,7 +68,7 @@
|
|||
}
|
||||
|
||||
const getSchemaFields = (asset, tableId) => {
|
||||
const { schema } = getSchemaForTable(tableId)
|
||||
const { schema } = getSchemaForDatasourcePlus(tableId)
|
||||
delete schema._id
|
||||
delete schema._rev
|
||||
return Object.values(schema || {})
|
||||
|
@ -89,9 +97,9 @@
|
|||
<Label small>Duplicate to Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
{options}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.resourceId}
|
||||
/>
|
||||
|
||||
<Label small />
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
$: tableOptions = $tables.list || []
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
{options}
|
||||
getOptionLabel={table => table.label}
|
||||
getOptionValue={table => table.resourceId}
|
||||
/>
|
||||
|
||||
<Label small>Row ID</Label>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import {
|
||||
getContextProviderComponents,
|
||||
getSchemaForTable,
|
||||
getSchemaForDatasourcePlus,
|
||||
} from "builderStore/dataBinding"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
|
||||
|
@ -24,8 +24,16 @@
|
|||
"schema"
|
||||
)
|
||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
||||
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
|
||||
$: tableOptions = $tables.list || []
|
||||
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
|
||||
// Gets a context definition of a certain type from a component definition
|
||||
const extractComponentContext = (component, contextType) => {
|
||||
|
@ -61,8 +69,8 @@
|
|||
})
|
||||
}
|
||||
|
||||
const getSchemaFields = (asset, tableId) => {
|
||||
const { schema } = getSchemaForTable(tableId)
|
||||
const getSchemaFields = resourceId => {
|
||||
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||
return Object.values(schema || {})
|
||||
}
|
||||
|
||||
|
@ -89,9 +97,9 @@
|
|||
<Label small>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
{options}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.resourceId}
|
||||
/>
|
||||
|
||||
<Label small />
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
import {
|
||||
tables as tablesStore,
|
||||
queries as queriesStore,
|
||||
viewsV2 as viewsV2Store,
|
||||
views as viewsStore,
|
||||
} from "stores/backend"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
|
@ -39,15 +41,17 @@
|
|||
tableId: m._id,
|
||||
type: "table",
|
||||
}))
|
||||
$: views = $tablesStore.list.reduce((acc, cur) => {
|
||||
let viewsArr = Object.entries(cur.views || {}).map(([key, value]) => ({
|
||||
label: key,
|
||||
name: key,
|
||||
...value,
|
||||
type: "view",
|
||||
}))
|
||||
return [...acc, ...viewsArr]
|
||||
}, [])
|
||||
$: viewsV1 = $viewsStore.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "view",
|
||||
}))
|
||||
$: viewsV2 = $viewsV2Store.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "viewV2",
|
||||
}))
|
||||
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
||||
$: queries = $queriesStore.list
|
||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||
.map(query => ({
|
||||
|
|
|
@ -1,28 +1,47 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables as tablesStore } from "stores/backend"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { tables as tablesStore, viewsV2 } from "stores/backend"
|
||||
|
||||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: tables = $tablesStore.list.map(m => ({
|
||||
label: m.name,
|
||||
tableId: m._id,
|
||||
$: tables = $tablesStore.list.map(table => ({
|
||||
...table,
|
||||
type: "table",
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: views = $viewsV2.list.map(view => ({
|
||||
...view,
|
||||
type: "viewV2",
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tables || []), ...(views || [])]
|
||||
|
||||
const onChange = e => {
|
||||
const dataSource = tables?.find(x => x.tableId === e.detail)
|
||||
dispatch("change", dataSource)
|
||||
dispatch(
|
||||
"change",
|
||||
options.find(x => x.resourceId === e.detail)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Migrate old values before "resourceId" existed
|
||||
if (value && !value.resourceId) {
|
||||
const view = views.find(x => x.resourceId === value.id)
|
||||
const table = tables.find(x => x.resourceId === value._id)
|
||||
dispatch("change", view || table)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Select
|
||||
on:change={onChange}
|
||||
value={value?.tableId}
|
||||
options={tables}
|
||||
getOptionValue={x => x.tableId}
|
||||
value={value?.resourceId}
|
||||
{options}
|
||||
getOptionValue={x => x.resourceId}
|
||||
getOptionLabel={x => x.label}
|
||||
/>
|
||||
|
|
|
@ -26,6 +26,9 @@ export const capitalise = s => {
|
|||
|
||||
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||
|
||||
export const lowercaseExceptFirst = s =>
|
||||
s.charAt(0) + s.substring(1).toLowerCase()
|
||||
|
||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
<script>
|
||||
import {
|
||||
Icon,
|
||||
Divider,
|
||||
Heading,
|
||||
Layout,
|
||||
Input,
|
||||
clickOutside,
|
||||
notifications,
|
||||
ActionButton,
|
||||
CopyInput,
|
||||
Modal,
|
||||
FancyForm,
|
||||
FancyInput,
|
||||
Button,
|
||||
FancySelect,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
||||
import { fetchData, Constants, Utils } from "@budibase/frontend-core"
|
||||
import {
|
||||
fetchData,
|
||||
Constants,
|
||||
Utils,
|
||||
RoleUtils,
|
||||
} from "@budibase/frontend-core"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { API } from "api"
|
||||
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
|
||||
|
@ -26,10 +35,15 @@
|
|||
let loaded = false
|
||||
let inviting = false
|
||||
let searchFocus = false
|
||||
|
||||
let invitingFlow = false
|
||||
// Initially filter entities without app access
|
||||
// Show all when false
|
||||
let filterByAppAccess = true
|
||||
let filterByAppAccess = false
|
||||
let email
|
||||
let error
|
||||
let form
|
||||
let creationRoleType = Constants.BudibaseRoles.AppUser
|
||||
let creationAccessType = Constants.Roles.BASIC
|
||||
|
||||
let appInvites = []
|
||||
let filteredInvites = []
|
||||
|
@ -40,8 +54,7 @@
|
|||
let userLimitReachedModal
|
||||
|
||||
let inviteFailureResponse = ""
|
||||
|
||||
$: queryIsEmail = emailValidator(query) === true
|
||||
$: validEmail = emailValidator(email) === true
|
||||
$: prodAppId = apps.getProdAppID($store.appId)
|
||||
$: promptInvite = showInvite(
|
||||
filteredInvites,
|
||||
|
@ -50,7 +63,6 @@
|
|||
query
|
||||
)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const showInvite = (invites, users, groups, query) => {
|
||||
return !invites?.length && !users?.length && !groups?.length && query
|
||||
}
|
||||
|
@ -66,9 +78,9 @@
|
|||
if (!filterByAppAccess && !query) {
|
||||
filteredInvites =
|
||||
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
||||
filteredInvites.sort(sortInviteRoles)
|
||||
return
|
||||
}
|
||||
|
||||
filteredInvites = appInvites.filter(invite => {
|
||||
const inviteInfo = invite.info?.apps
|
||||
if (!query && inviteInfo && prodAppId) {
|
||||
|
@ -76,8 +88,8 @@
|
|||
}
|
||||
return invite.email.includes(query)
|
||||
})
|
||||
filteredInvites.sort(sortInviteRoles)
|
||||
}
|
||||
|
||||
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||
$: if (searchFocus === true) {
|
||||
filterByAppAccess = false
|
||||
|
@ -107,24 +119,66 @@
|
|||
})
|
||||
await usersFetch.refresh()
|
||||
|
||||
filteredUsers = $usersFetch.rows.map(user => {
|
||||
const isAdminOrBuilder = sdk.users.isAdminOrBuilder(user, prodAppId)
|
||||
let role = undefined
|
||||
if (isAdminOrBuilder) {
|
||||
role = Constants.Roles.ADMIN
|
||||
} else {
|
||||
const appRole = Object.keys(user.roles).find(x => x === prodAppId)
|
||||
if (appRole) {
|
||||
role = user.roles[appRole]
|
||||
filteredUsers = $usersFetch.rows
|
||||
.filter(user => !user?.admin?.global) // filter out global admins
|
||||
.map(user => {
|
||||
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
|
||||
user,
|
||||
prodAppId
|
||||
)
|
||||
const isAppBuilder = sdk.users.hasAppBuilderPermissions(user, prodAppId)
|
||||
let role
|
||||
if (isAdminOrGlobalBuilder) {
|
||||
role = Constants.Roles.ADMIN
|
||||
} else if (isAppBuilder) {
|
||||
role = Constants.Roles.CREATOR
|
||||
} else {
|
||||
const appRole = user.roles[prodAppId]
|
||||
if (appRole) {
|
||||
role = appRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
role,
|
||||
isAdminOrBuilder,
|
||||
}
|
||||
})
|
||||
return {
|
||||
...user,
|
||||
role,
|
||||
isAdminOrGlobalBuilder,
|
||||
isAppBuilder,
|
||||
}
|
||||
})
|
||||
.sort(sortRoles)
|
||||
}
|
||||
|
||||
const sortInviteRoles = (a, b) => {
|
||||
const aEmpty =
|
||||
!a.info?.appBuilders?.length && Object.keys(a.info.apps).length === 0
|
||||
const bEmpty =
|
||||
!b.info?.appBuilders?.length && Object.keys(b.info.apps).length === 0
|
||||
|
||||
if (aEmpty && !bEmpty) return 1
|
||||
if (!aEmpty && bEmpty) return -1
|
||||
}
|
||||
|
||||
const sortRoles = (a, b) => {
|
||||
const roleA = a.role
|
||||
const roleB = b.role
|
||||
|
||||
const priorityA = RoleUtils.getRolePriority(roleA)
|
||||
const priorityB = RoleUtils.getRolePriority(roleB)
|
||||
|
||||
if (roleA === undefined && roleB !== undefined) {
|
||||
return 1
|
||||
} else if (roleA !== undefined && roleB === undefined) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (priorityA < priorityB) {
|
||||
return 1
|
||||
} else if (priorityA > priorityB) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||
|
@ -160,6 +214,12 @@
|
|||
if (user.role === role) {
|
||||
return
|
||||
}
|
||||
if (user.isAppBuilder) {
|
||||
await removeAppBuilder(user._id, prodAppId)
|
||||
}
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
await removeAppBuilder(user._id, prodAppId)
|
||||
}
|
||||
await updateAppUser(user, role)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -189,6 +249,9 @@
|
|||
return
|
||||
}
|
||||
try {
|
||||
if (group?.builder?.apps.includes(prodAppId)) {
|
||||
await removeGroupAppBuilder(group._id)
|
||||
}
|
||||
await updateAppGroup(group, role)
|
||||
} catch {
|
||||
notifications.error("Group update failed")
|
||||
|
@ -225,14 +288,17 @@
|
|||
return nameMatch
|
||||
})
|
||||
.map(enrichGroupRole)
|
||||
.sort(sortRoles)
|
||||
}
|
||||
|
||||
const enrichGroupRole = group => {
|
||||
return {
|
||||
...group,
|
||||
role: group.roles?.[
|
||||
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
||||
],
|
||||
role: group?.builder?.apps.includes(prodAppId)
|
||||
? Constants.Roles.CREATOR
|
||||
: group.roles?.[
|
||||
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,8 +311,7 @@
|
|||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||
|
||||
/*
|
||||
/*
|
||||
Create pseudo users from the "users" attribute on app groups.
|
||||
These users will appear muted in the UI and show the ROLE
|
||||
inherited from their parent group. The users allow assigning of user
|
||||
|
@ -291,21 +356,28 @@
|
|||
}
|
||||
|
||||
async function inviteUser() {
|
||||
if (!queryIsEmail) {
|
||||
if (!validEmail) {
|
||||
notifications.error("Email is not valid")
|
||||
return
|
||||
}
|
||||
const newUserEmail = query + ""
|
||||
const newUserEmail = email + ""
|
||||
inviting = true
|
||||
|
||||
const payload = [
|
||||
{
|
||||
email: newUserEmail,
|
||||
builder: false,
|
||||
admin: false,
|
||||
apps: { [prodAppId]: Constants.Roles.BASIC },
|
||||
builder: !!creationRoleType === Constants.BudibaseRoles.Admin,
|
||||
admin: !!creationRoleType === Constants.BudibaseRoles.Admin,
|
||||
},
|
||||
]
|
||||
|
||||
if (creationAccessType === Constants.Roles.CREATOR) {
|
||||
payload[0].appBuilders = [prodAppId]
|
||||
} else {
|
||||
payload[0].apps = {
|
||||
[prodAppId]: creationAccessType,
|
||||
}
|
||||
}
|
||||
let userInviteResponse
|
||||
try {
|
||||
userInviteResponse = await users.onboard(payload)
|
||||
|
@ -317,16 +389,23 @@
|
|||
return userInviteResponse
|
||||
}
|
||||
|
||||
const openInviteFlow = () => {
|
||||
$licensing.userLimitReached
|
||||
? userLimitReachedModal.show()
|
||||
: (invitingFlow = true)
|
||||
}
|
||||
|
||||
const onInviteUser = async () => {
|
||||
form.validate()
|
||||
userOnboardResponse = await inviteUser()
|
||||
const originalQuery = query + ""
|
||||
query = null
|
||||
const originalQuery = email + ""
|
||||
email = null
|
||||
|
||||
const newUser = userOnboardResponse?.successful.find(
|
||||
user => user.email === originalQuery
|
||||
)
|
||||
if (newUser) {
|
||||
query = originalQuery
|
||||
email = originalQuery
|
||||
notifications.success(
|
||||
userOnboardResponse.created
|
||||
? "User created successfully"
|
||||
|
@ -344,16 +423,27 @@
|
|||
notifications.error(inviteFailureResponse)
|
||||
}
|
||||
userOnboardResponse = null
|
||||
invitingFlow = false
|
||||
// trigger reload of the users
|
||||
query = ""
|
||||
}
|
||||
|
||||
const onUpdateUserInvite = async (invite, role) => {
|
||||
await users.updateInvite({
|
||||
let updateBody = {
|
||||
code: invite.code,
|
||||
apps: {
|
||||
...invite.apps,
|
||||
[prodAppId]: role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
updateBody.appBuilders = [...(updateBody.appBuilders ?? []), prodAppId]
|
||||
delete updateBody?.apps?.[prodAppId]
|
||||
} else if (role !== Constants.Roles.CREATOR && invite?.appBuilders) {
|
||||
invite.appBuilders = []
|
||||
}
|
||||
await users.updateInvite(updateBody)
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
|
@ -373,6 +463,22 @@
|
|||
})
|
||||
}
|
||||
|
||||
const addAppBuilder = async userId => {
|
||||
await users.addAppBuilder(userId, prodAppId)
|
||||
}
|
||||
|
||||
const removeAppBuilder = async userId => {
|
||||
await users.removeAppBuilder(userId, prodAppId)
|
||||
}
|
||||
|
||||
const addGroupAppBuilder = async groupId => {
|
||||
await groups.actions.addGroupAppBuilder(groupId, prodAppId)
|
||||
}
|
||||
|
||||
const removeGroupAppBuilder = async groupId => {
|
||||
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
||||
}
|
||||
|
||||
const initSidePanel = async sidePaneOpen => {
|
||||
if (sidePaneOpen === true) {
|
||||
await groups.actions.init()
|
||||
|
@ -383,27 +489,17 @@
|
|||
$: initSidePanel($store.builderSidePanel)
|
||||
|
||||
function handleKeyDown(evt) {
|
||||
if (evt.key === "Enter" && queryIsEmail && !inviting) {
|
||||
if (evt.key === "Enter" && validEmail && !inviting) {
|
||||
onInviteUser()
|
||||
}
|
||||
}
|
||||
|
||||
const userTitle = user => {
|
||||
if (sdk.users.isAdmin(user)) {
|
||||
return "Admin"
|
||||
} else if (sdk.users.isBuilder(user, prodAppId)) {
|
||||
return "Developer"
|
||||
} else {
|
||||
return "App user"
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleFooter = user => {
|
||||
if (user.group) {
|
||||
const role = $roles.find(role => role._id === user.role)
|
||||
return `This user has been given ${role?.name} access from the ${user.group} group`
|
||||
}
|
||||
if (user.isAdminOrBuilder) {
|
||||
if (user.isAdminOrGlobalBuilder) {
|
||||
return "This user's role grants admin access to all apps"
|
||||
}
|
||||
return null
|
||||
|
@ -423,227 +519,300 @@
|
|||
}}
|
||||
>
|
||||
<div class="builder-side-panel-header">
|
||||
<Heading size="S">Users</Heading>
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
name="RailRightClose"
|
||||
hoverable
|
||||
<div
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = false
|
||||
return state
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="search" class:focused={searchFocus}>
|
||||
<span class="search-input">
|
||||
<Input
|
||||
placeholder={"Add users and groups to your app"}
|
||||
autocomplete="off"
|
||||
disabled={inviting}
|
||||
value={query}
|
||||
on:input={e => {
|
||||
query = e.target.value.trim()
|
||||
}}
|
||||
on:focus={() => (searchFocus = true)}
|
||||
on:blur={() => (searchFocus = false)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="search-input-icon"
|
||||
class:searching={query || !filterByAppAccess}
|
||||
on:click={() => {
|
||||
if (!filterByAppAccess) {
|
||||
filterByAppAccess = true
|
||||
}
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
query = null
|
||||
userOnboardResponse = null
|
||||
filterByAppAccess = true
|
||||
invitingFlow = false
|
||||
}}
|
||||
class="header"
|
||||
>
|
||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||
</span>
|
||||
{#if invitingFlow}
|
||||
<Icon name="BackAndroid" />
|
||||
{/if}
|
||||
<Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading>
|
||||
</div>
|
||||
<div class="header">
|
||||
<Button on:click={openInviteFlow} size="S" cta>Invite user</Button>
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
name="RailRightClose"
|
||||
hoverable
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = false
|
||||
return state
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if !invitingFlow}
|
||||
<div class="search" class:focused={searchFocus}>
|
||||
<span class="search-input">
|
||||
<Input
|
||||
placeholder={"Add users and groups to your app"}
|
||||
autocomplete="off"
|
||||
disabled={inviting}
|
||||
value={query}
|
||||
on:input={e => {
|
||||
query = e.target.value.trim()
|
||||
}}
|
||||
on:focus={() => (searchFocus = true)}
|
||||
on:blur={() => (searchFocus = false)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div class="body">
|
||||
{#if promptInvite && !userOnboardResponse}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">No user found</Heading>
|
||||
<div class="invite-directions">
|
||||
Add a valid email to invite a new user
|
||||
</div>
|
||||
</div>
|
||||
<div class="invite-form">
|
||||
<span>{query || ""}</span>
|
||||
<ActionButton
|
||||
icon="UserAdd"
|
||||
disabled={!queryIsEmail || inviting}
|
||||
on:click={$licensing.userLimitReached
|
||||
? userLimitReachedModal.show
|
||||
: onInviteUser}
|
||||
>
|
||||
Add user
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
<span
|
||||
class="search-input-icon"
|
||||
class:searching={query || !filterByAppAccess}
|
||||
on:click={() => {
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
query = null
|
||||
userOnboardResponse = null
|
||||
}}
|
||||
>
|
||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if !promptInvite}
|
||||
<Layout gap="L" noPadding>
|
||||
{#if filteredInvites?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Pending invites</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredInvites as invite}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={invite.email}>
|
||||
{invite.email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={invite.info.apps?.[prodAppId]}
|
||||
allowRemove={invite.info.apps?.[prodAppId]}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateUserInvite(invite, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUninviteAppUser(invite)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if $licensing.groupsEnabled && filteredGroups?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Groups</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredGroups as group}
|
||||
<div
|
||||
class="auth-entity group"
|
||||
on:click={() => {
|
||||
if (selectedGroup != group._id) {
|
||||
selectedGroup = group._id
|
||||
} else {
|
||||
selectedGroup = null
|
||||
}
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
<div class="body">
|
||||
{#if promptInvite && !userOnboardResponse}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">No user found</Heading>
|
||||
<div class="invite-directions">
|
||||
Try searching a different email or <span
|
||||
class="underlined"
|
||||
on:click={openInviteFlow}>invite a new user</span
|
||||
>
|
||||
<div class="details">
|
||||
<GroupIcon {group} size="S" />
|
||||
<div>
|
||||
{group.name}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{`${group.users?.length} user${
|
||||
group.users?.length != 1 ? "s" : ""
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={group.role}
|
||||
allowRemove={group.role}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateGroup(group, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateGroup(group)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if filteredUsers?.length}
|
||||
<div class="auth-entity-section">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Users</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each allUsers as user}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={user.email}>
|
||||
{user.email}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{userTitle(user)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access" class:muted={user.group}>
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={user.role}
|
||||
allowRemove={user.role && !user.group}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateUser(user, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateUser(user)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if userOnboardResponse?.created}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">User added!</Heading>
|
||||
<div class="invite-directions">
|
||||
Email invites are not available without SMTP configuration. Here is
|
||||
the password that has been generated for this user.
|
||||
{#if !promptInvite}
|
||||
<Layout gap="L" noPadding>
|
||||
{#if filteredInvites?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Pending invites</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredInvites as invite}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={invite.email}>
|
||||
{invite.email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={invite.info?.appBuilders?.includes(prodAppId)
|
||||
? Constants.Roles.CREATOR
|
||||
: invite.info.apps?.[prodAppId]}
|
||||
allowRemove={invite.info.apps?.[prodAppId]}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateUserInvite(invite, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUninviteAppUser(invite)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if $licensing.groupsEnabled && filteredGroups?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Groups</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredGroups as group}
|
||||
<div
|
||||
class="auth-entity group"
|
||||
on:click={() => {
|
||||
if (selectedGroup != group._id) {
|
||||
selectedGroup = group._id
|
||||
} else {
|
||||
selectedGroup = null
|
||||
}
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<div class="details">
|
||||
<GroupIcon {group} size="S" />
|
||||
<div>
|
||||
{group.name}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{`${group.users?.length} user${
|
||||
group.users?.length != 1 ? "s" : ""
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={group.role}
|
||||
allowRemove={group.role}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
allowCreator={true}
|
||||
on:change={e => {
|
||||
if (e.detail === Constants.Roles.CREATOR) {
|
||||
addGroupAppBuilder(group._id)
|
||||
} else {
|
||||
onUpdateGroup(group, e.detail)
|
||||
}
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateGroup(group)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if filteredUsers?.length}
|
||||
<div class="auth-entity-section">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Users</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each allUsers as user}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={user.email}>
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access" class:muted={user.group}>
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={user.role}
|
||||
allowRemove={user.role && !user.group}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
quiet={true}
|
||||
on:addcreator={() => {}}
|
||||
on:change={e => {
|
||||
if (e.detail === Constants.Roles.CREATOR) {
|
||||
addAppBuilder(user._id)
|
||||
} else {
|
||||
onUpdateUser(user, e.detail)
|
||||
}
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateUser(user)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrGlobalBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if userOnboardResponse?.created}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">User added!</Heading>
|
||||
<div class="invite-directions">
|
||||
Email invites are not available without SMTP configuration. Here
|
||||
is the password that has been generated for this user.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CopyInput
|
||||
value={userOnboardResponse.successful[0]?.password}
|
||||
label="Password"
|
||||
/>
|
||||
<div>
|
||||
<CopyInput
|
||||
value={userOnboardResponse.successful[0]?.password}
|
||||
label="Password"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Divider />
|
||||
<div class="body">
|
||||
<Layout gap="L" noPadding>
|
||||
<div class="user-invite-form">
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
disabled={false}
|
||||
label="Email"
|
||||
value={email}
|
||||
on:change={e => {
|
||||
email = e.detail
|
||||
}}
|
||||
validate={() => {
|
||||
if (!email) {
|
||||
return "Please enter an email"
|
||||
}
|
||||
return null
|
||||
}}
|
||||
{error}
|
||||
/>
|
||||
<FancySelect
|
||||
bind:value={creationRoleType}
|
||||
options={sdk.users.isAdmin($auth.user)
|
||||
? Constants.BudibaseRoleOptionsNew
|
||||
: Constants.BudibaseRoleOptionsNew.filter(
|
||||
option => option.value !== Constants.BudibaseRoles.Admin
|
||||
)}
|
||||
label="Role"
|
||||
/>
|
||||
{#if creationRoleType !== Constants.BudibaseRoles.Admin}
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
bind:value={creationAccessType}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
quiet={true}
|
||||
autoWidth
|
||||
align="right"
|
||||
fancySelect
|
||||
/>
|
||||
{/if}
|
||||
</FancyForm>
|
||||
{#if creationRoleType === Constants.BudibaseRoles.Admin}
|
||||
<div class="admin-info">
|
||||
<Icon name="Info" />
|
||||
Admins will get full access to all apps and settings
|
||||
</div>
|
||||
{/if}
|
||||
<span class="add-user">
|
||||
<Button
|
||||
newStyles
|
||||
cta
|
||||
disabled={!email?.length}
|
||||
on:click={onInviteUser}>Add user</Button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Modal bind:this={userLimitReachedModal}>
|
||||
<UpgradeModal {isOwner} />
|
||||
</Modal>
|
||||
|
@ -659,6 +828,27 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.add-user {
|
||||
padding-top: var(--spacing-xl);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.admin-info {
|
||||
margin-top: var(--spacing-xl);
|
||||
padding: var(--spacing-l) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
height: 30px;
|
||||
background-color: var(--background-alt);
|
||||
}
|
||||
|
||||
.underlined {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -746,12 +936,6 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.invite-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#builder-side-panel-container .search {
|
||||
padding-top: var(--spacing-m);
|
||||
padding-bottom: var(--spacing-m);
|
||||
|
@ -798,6 +982,16 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.user-invite-form {
|
||||
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import TableDataTable from "components/backend/DataTable/DataTable.svelte"
|
||||
import TableDataTable from "components/backend/DataTable/TableDataTable.svelte"
|
||||
import { tables, database } from "stores/backend"
|
||||
import { Banner } from "@budibase/bbui"
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { views } from "stores/backend"
|
||||
import { views, viewsV2 } from "stores/backend"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(async () => {
|
||||
const { list, selected } = $views
|
||||
if (selected) {
|
||||
$redirect(`./${encodeURIComponent(selected?.name)}`)
|
||||
} else if (list?.length) {
|
||||
$redirect(`./${encodeURIComponent(list[0].name)}`)
|
||||
if ($viewsV2.selected) {
|
||||
$redirect(`./v2/${$viewsV2.selected.id}`)
|
||||
} else if ($viewsV2.list?.length) {
|
||||
$redirect(`./v2/${$viewsV2.list[0].id}`)
|
||||
} else if ($views.selected) {
|
||||
$redirect(`./${encodeURIComponent($views.selected?.name)}`)
|
||||
} else if ($views.list?.length) {
|
||||
$redirect(`./${encodeURIComponent($views.list[0].name)}`)
|
||||
} else {
|
||||
$redirect("../")
|
||||
}
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
import { onDestroy } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
$: viewName = $views.selectedViewName
|
||||
$: store.actions.websocket.selectResource(viewName)
|
||||
$: name = $views.selectedViewName
|
||||
$: store.actions.websocket.selectResource(name)
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "viewName",
|
||||
stateKey: "selectedViewName",
|
||||
validate: name => $views.list?.some(view => view.name === name),
|
||||
update: views.select,
|
||||
fallbackUrl: "../",
|
||||
fallbackUrl: "../../",
|
||||
store: views,
|
||||
routify,
|
||||
decode: decodeURIComponent,
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { viewsV2 } from "stores/backend"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
$: id = $viewsV2.selectedViewId
|
||||
$: store.actions.websocket.selectResource(id)
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "viewId",
|
||||
stateKey: "selectedViewId",
|
||||
validate: id => $viewsV2.list?.some(view => view.id === id),
|
||||
update: viewsV2.select,
|
||||
fallbackUrl: "../../",
|
||||
store: viewsV2,
|
||||
routify,
|
||||
decode: decodeURIComponent,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
|
||||
</script>
|
||||
|
||||
<ViewV2DataTable />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -297,8 +297,12 @@
|
|||
width: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.divider:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
.divider:hover:after {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -131,8 +131,7 @@
|
|||
const completeDatasourceScreenCreation = async () => {
|
||||
const screens = selectedTemplates.map(template => {
|
||||
let screenTemplate = template.create()
|
||||
screenTemplate.datasource = template.datasource
|
||||
screenTemplate.autoTableId = template.table
|
||||
screenTemplate.autoTableId = template.resourceId
|
||||
return screenTemplate
|
||||
})
|
||||
await createScreens({ screens, screenAccessRole })
|
||||
|
@ -176,10 +175,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={datasourceModal}>
|
||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||
<DatasourceModal
|
||||
onConfirm={confirmScreenDatasources}
|
||||
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
initialScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
|
|
@ -1,41 +1,30 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import {
|
||||
ModalContent,
|
||||
Layout,
|
||||
notifications,
|
||||
Icon,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import getTemplates from "builderStore/store/screenTemplates"
|
||||
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
|
||||
import { datasources } from "stores/backend"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { IntegrationNames } from "constants"
|
||||
import { onMount } from "svelte"
|
||||
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
|
||||
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
|
||||
|
||||
export let onCancel
|
||||
export let onConfirm
|
||||
export let initalScreens = []
|
||||
export let initialScreens = []
|
||||
|
||||
let selectedScreens = [...initalScreens]
|
||||
let selectedScreens = [...initialScreens]
|
||||
|
||||
const toggleScreenSelection = (table, datasource) => {
|
||||
if (selectedScreens.find(s => s.table === table._id)) {
|
||||
$: filteredSources = $datasources.list?.filter(datasource => {
|
||||
return datasource.source !== IntegrationNames.REST && datasource["entities"]
|
||||
})
|
||||
|
||||
const toggleSelection = datasource => {
|
||||
const { resourceId } = datasource
|
||||
if (selectedScreens.find(s => s.resourceId === resourceId)) {
|
||||
selectedScreens = selectedScreens.filter(
|
||||
screen => screen.table !== table._id
|
||||
screen => screen.resourceId !== resourceId
|
||||
)
|
||||
} else {
|
||||
let partialTemplates = getTemplates($store, $tables.list).reduce(
|
||||
(acc, template) => {
|
||||
if (template.table === table._id) {
|
||||
template.datasource = datasource.name
|
||||
acc.push(template)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
[]
|
||||
)
|
||||
selectedScreens = [...partialTemplates, ...selectedScreens]
|
||||
selectedScreens = [...selectedScreens, rowListScreen([datasource])[0]]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,18 +34,6 @@
|
|||
})
|
||||
}
|
||||
|
||||
$: filteredSources = Array.isArray($datasources.list)
|
||||
? $datasources.list.reduce((acc, datasource) => {
|
||||
if (
|
||||
datasource.source !== IntegrationNames.REST &&
|
||||
datasource["entities"]
|
||||
) {
|
||||
acc.push(datasource)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
: []
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await datasources.fetch()
|
||||
|
@ -81,6 +58,9 @@
|
|||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
{#each filteredSources as datasource}
|
||||
{@const entities = Array.isArray(datasource.entities)
|
||||
? datasource.entities
|
||||
: Object.values(datasource.entities || {})}
|
||||
<div class="data-source-wrap">
|
||||
<div class="data-source-header">
|
||||
<svelte:component
|
||||
|
@ -90,64 +70,51 @@
|
|||
/>
|
||||
<div class="data-source-name">{datasource.name}</div>
|
||||
</div>
|
||||
{#if Array.isArray(datasource.entities)}
|
||||
{#each datasource.entities.filter(table => table._id !== "ta_users") as table}
|
||||
<div
|
||||
class="data-source-entry"
|
||||
class:selected={selectedScreens.find(
|
||||
x => x.table === table._id
|
||||
)}
|
||||
on:click={() => toggleScreenSelection(table, datasource)}
|
||||
>
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
class="spectrum-Icon"
|
||||
style="color: white"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Table" />
|
||||
</svg>
|
||||
{table.name}
|
||||
{#if selectedScreens.find(x => x.table === table._id)}
|
||||
<span class="data-source-check">
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- List all tables -->
|
||||
{#each entities.filter(table => table._id !== "ta_users") as table}
|
||||
{@const views = Object.values(table.views || {}).filter(
|
||||
view => view.version === 2
|
||||
)}
|
||||
{@const datasource = {
|
||||
...table,
|
||||
// Legacy properties
|
||||
tableId: table._id,
|
||||
label: table.name,
|
||||
// New consistent properties
|
||||
resourceId: table._id,
|
||||
name: table.name,
|
||||
type: "table",
|
||||
}}
|
||||
{@const selected = selectedScreens.find(
|
||||
screen => screen.resourceId === datasource.resourceId
|
||||
)}
|
||||
<DatasourceTemplateRow
|
||||
on:click={() => toggleSelection(datasource)}
|
||||
{selected}
|
||||
{datasource}
|
||||
/>
|
||||
|
||||
<!-- List all views inside this table -->
|
||||
{#each views as view}
|
||||
{@const datasource = {
|
||||
...view,
|
||||
// Legacy properties
|
||||
label: view.name,
|
||||
// New consistent properties
|
||||
resourceId: view.id,
|
||||
name: view.name,
|
||||
type: "viewV2",
|
||||
}}
|
||||
{@const selected = selectedScreens.find(
|
||||
x => x.resourceId === datasource.resourceId
|
||||
)}
|
||||
<DatasourceTemplateRow
|
||||
on:click={() => toggleSelection(datasource)}
|
||||
{selected}
|
||||
{datasource}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if datasource["entities"] && !Array.isArray(datasource.entities)}
|
||||
{#each Object.keys(datasource.entities).filter(table => table._id !== "ta_users") as table_key}
|
||||
<div
|
||||
class="data-source-entry"
|
||||
class:selected={selectedScreens.find(
|
||||
x => x.table === datasource.entities[table_key]._id
|
||||
)}
|
||||
on:click={() =>
|
||||
toggleScreenSelection(
|
||||
datasource.entities[table_key],
|
||||
datasource
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
class="spectrum-Icon"
|
||||
style="color: white"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Table" />
|
||||
</svg>
|
||||
{datasource.entities[table_key].name}
|
||||
{#if selectedScreens.find(x => x.table === datasource.entities[table_key]._id)}
|
||||
<span class="data-source-check">
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
|
@ -160,42 +127,10 @@
|
|||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.data-source-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.data-source-entry {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: 0.3s all;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.data-source-entry:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.data-source-entry .data-source-check {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.data-source-entry :global(.spectrum-Icon) {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.data-source-entry .data-source-check :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-green-600);
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
export let datasource
|
||||
export let selected = false
|
||||
|
||||
$: icon = datasource.type === "viewV2" ? "Remove" : "Table"
|
||||
</script>
|
||||
|
||||
<div class="data-source-entry" class:selected on:click>
|
||||
<Icon name={icon} color="var(--spectrum-global-color-gray-600)" />
|
||||
{datasource.name}
|
||||
{#if selected}
|
||||
<span class="data-source-check">
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.data-source-entry {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spacing-m);
|
||||
padding: var(--spectrum-alias-item-padding-s);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: 0.3s all;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.data-source-entry:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.data-source-check {
|
||||
margin-left: auto;
|
||||
}
|
||||
.data-source-check :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-green-600);
|
||||
}
|
||||
</style>
|
|
@ -39,7 +39,7 @@
|
|||
return publishedApps
|
||||
}
|
||||
return publishedApps.filter(app => {
|
||||
if (sdk.users.isBuilder(user, app.appId)) {
|
||||
if (sdk.users.isBuilder(user, app.prodId)) {
|
||||
return true
|
||||
}
|
||||
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
||||
|
@ -142,7 +142,12 @@
|
|||
<div class="group">
|
||||
<Layout gap="S" noPadding>
|
||||
{#each userApps as app (app.appId)}
|
||||
<a class="app" target="_blank" href={getUrl(app)}>
|
||||
<a
|
||||
class="app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={getUrl(app)}
|
||||
>
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="app-info">
|
||||
<Heading size="XS">{app.name}</Heading>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
import GroupIcon from "./_components/GroupIcon.svelte"
|
||||
import GroupUsers from "./_components/GroupUsers.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export let groupId
|
||||
|
||||
|
@ -45,7 +46,7 @@
|
|||
|
||||
let loaded = false
|
||||
let editModal, deleteModal
|
||||
|
||||
$: console.log(group)
|
||||
$: scimEnabled = $features.isScimEnabled
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
$: group = $groups.find(x => x._id === groupId)
|
||||
|
@ -57,8 +58,11 @@
|
|||
)
|
||||
.map(app => ({
|
||||
...app,
|
||||
role: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||
role: group?.builder?.apps.includes(apps.getProdAppID(app.devId))
|
||||
? Constants.Roles.CREATOR
|
||||
: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||
}))
|
||||
$: console.log(groupApps)
|
||||
$: {
|
||||
if (loaded && !group?._id) {
|
||||
$goto("./")
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
$: count = getCount(Object.keys(value || {}).length)
|
||||
|
||||
$: count = Object.keys(value || {}).length
|
||||
const getCount = () => {
|
||||
return sdk.users.hasAppBuilderPermissions(row)
|
||||
? row.builder.apps.length +
|
||||
Object.keys(row.roles || {}).filter(appId =>
|
||||
row.builder.apps.includes(appId)
|
||||
).length
|
||||
: value?.length || 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
$: scimEnabled = $features.isScimEnabled
|
||||
$: isSSO = !!user?.provider
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
$: privileged = sdk.users.isAdminOrBuilder(user)
|
||||
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||
$: nameLabel = getNameLabel(user)
|
||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||
|
@ -98,17 +98,14 @@
|
|||
return y._id === userId
|
||||
})
|
||||
})
|
||||
$: globalRole = sdk.users.isAdmin(user)
|
||||
? "admin"
|
||||
: sdk.users.isBuilder(user)
|
||||
? "developer"
|
||||
: "appUser"
|
||||
$: globalRole = sdk.users.isAdmin(user) ? "admin" : "appUser"
|
||||
|
||||
const getAvailableApps = (appList, privileged, roles) => {
|
||||
let availableApps = appList.slice()
|
||||
if (!privileged) {
|
||||
availableApps = availableApps.filter(x => {
|
||||
return Object.keys(roles || {}).find(y => {
|
||||
let roleKeys = Object.keys(roles || {})
|
||||
return roleKeys.concat(user?.builder?.apps).find(y => {
|
||||
return x.appId === apps.extractAppId(y)
|
||||
})
|
||||
})
|
||||
|
@ -119,7 +116,7 @@
|
|||
name: app.name,
|
||||
devId: app.devId,
|
||||
icon: app.icon,
|
||||
role: privileged ? Constants.Roles.ADMIN : roles[prodAppId],
|
||||
role: getRole(prodAppId, roles),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -132,6 +129,18 @@
|
|||
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
||||
}
|
||||
|
||||
const getRole = (prodAppId, roles) => {
|
||||
if (privileged) {
|
||||
return Constants.Roles.ADMIN
|
||||
}
|
||||
|
||||
if (user?.builder?.apps?.includes(prodAppId)) {
|
||||
return Constants.Roles.CREATOR
|
||||
}
|
||||
|
||||
return roles[prodAppId]
|
||||
}
|
||||
|
||||
const getNameLabel = user => {
|
||||
const { firstName, lastName, email } = user || {}
|
||||
if (!firstName && !lastName) {
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
import { StatusLight } from "@budibase/bbui"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { roles } from "stores/backend"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let value
|
||||
|
||||
const getRoleLabel = roleId => {
|
||||
const role = $roles.find(x => x._id === roleId)
|
||||
return role?.name || "Custom role"
|
||||
return roleId === Constants.Roles.CREATOR
|
||||
? capitalise(Constants.Roles.CREATOR.toLowerCase())
|
||||
: role?.name || "Custom role"
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -5,9 +5,22 @@
|
|||
|
||||
export let value
|
||||
export let row
|
||||
|
||||
$: console.log(row)
|
||||
$: priviliged = sdk.users.isAdminOrBuilder(row)
|
||||
$: count = priviliged ? $apps.length : value?.length || 0
|
||||
$: count = getCount(row)
|
||||
|
||||
const getCount = () => {
|
||||
if (priviliged) {
|
||||
return $apps.length
|
||||
} else {
|
||||
return sdk.users.hasAppBuilderPermissions(row)
|
||||
? row.builder.apps.length +
|
||||
Object.keys(row.roles || {}).filter(appId =>
|
||||
row.builder.apps.includes(appId)
|
||||
).length
|
||||
: value?.length || 0
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
export let row
|
||||
|
||||
const TooltipMap = {
|
||||
appUser: "Only has access to published apps",
|
||||
appUser: "Only has access to assigned apps",
|
||||
developer: "Access to the app builder",
|
||||
admin: "Full access",
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export { database } from "./database"
|
||||
export { tables } from "./tables"
|
||||
export { views } from "./views"
|
||||
export { viewsV2 } from "./viewsV2"
|
||||
export { permissions } from "./permissions"
|
||||
export { roles } from "./roles"
|
||||
export { datasources, ImportTableError } from "./datasources"
|
||||
|
|
|
@ -9,7 +9,10 @@ export function createViewsStore() {
|
|||
const derivedStore = derived([store, tables], ([$store, $tables]) => {
|
||||
let list = []
|
||||
$tables.list?.forEach(table => {
|
||||
list = list.concat(Object.values(table?.views || {}))
|
||||
const views = Object.values(table?.views || {}).filter(view => {
|
||||
return view.version !== 2
|
||||
})
|
||||
list = list.concat(views)
|
||||
})
|
||||
return {
|
||||
...$store,
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import { writable, derived, get } from "svelte/store"
|
||||
import { tables } from "./"
|
||||
import { API } from "api"
|
||||
|
||||
export function createViewsV2Store() {
|
||||
const store = writable({
|
||||
selectedViewId: null,
|
||||
})
|
||||
const derivedStore = derived([store, tables], ([$store, $tables]) => {
|
||||
let list = []
|
||||
$tables.list?.forEach(table => {
|
||||
const views = Object.values(table?.views || {}).filter(view => {
|
||||
return view.version === 2
|
||||
})
|
||||
list = list.concat(views)
|
||||
})
|
||||
return {
|
||||
...$store,
|
||||
list,
|
||||
selected: list.find(view => view.id === $store.selectedViewId),
|
||||
}
|
||||
})
|
||||
|
||||
const select = id => {
|
||||
store.update(state => ({
|
||||
...state,
|
||||
selectedViewId: id,
|
||||
}))
|
||||
}
|
||||
|
||||
const deleteView = async view => {
|
||||
await API.viewV2.delete(view.id)
|
||||
replaceView(view.id, null)
|
||||
}
|
||||
|
||||
const create = async view => {
|
||||
const savedViewResponse = await API.viewV2.create(view)
|
||||
const savedView = savedViewResponse.data
|
||||
replaceView(savedView.id, savedView)
|
||||
return savedView
|
||||
}
|
||||
|
||||
const save = async view => {
|
||||
const res = await API.viewV2.update(view)
|
||||
const savedView = res?.data
|
||||
replaceView(view.id, savedView)
|
||||
}
|
||||
|
||||
// Handles external updates of tables
|
||||
const replaceView = (viewId, view) => {
|
||||
if (!viewId) {
|
||||
return
|
||||
}
|
||||
const existingView = get(derivedStore).list.find(view => view.id === viewId)
|
||||
const tableIndex = get(tables).list.findIndex(table => {
|
||||
return table._id === view?.tableId || table._id === existingView?.tableId
|
||||
})
|
||||
if (tableIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
// Handle deletion
|
||||
if (!view) {
|
||||
tables.update(state => {
|
||||
delete state.list[tableIndex].views[existingView.name]
|
||||
return state
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Add new view
|
||||
if (!existingView) {
|
||||
tables.update(state => {
|
||||
state.list[tableIndex].views[view.name] = view
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
// Update existing view
|
||||
else {
|
||||
tables.update(state => {
|
||||
// Remove old view
|
||||
delete state.list[tableIndex].views[existingView.name]
|
||||
|
||||
// Add new view
|
||||
state.list[tableIndex].views[view.name] = view
|
||||
return state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
select,
|
||||
delete: deleteView,
|
||||
create,
|
||||
save,
|
||||
replaceView,
|
||||
}
|
||||
}
|
||||
|
||||
export const viewsV2 = createViewsV2Store()
|
|
@ -78,7 +78,19 @@ export function createGroupsStore() {
|
|||
},
|
||||
|
||||
getGroupAppIds: group => {
|
||||
return Object.keys(group?.roles || {})
|
||||
let groupAppIds = Object.keys(group?.roles || {})
|
||||
if (group?.builder?.apps) {
|
||||
groupAppIds = groupAppIds.concat(group.builder.apps)
|
||||
}
|
||||
return groupAppIds
|
||||
},
|
||||
|
||||
addGroupAppBuilder: async (groupId, appId) => {
|
||||
return await API.addGroupAppBuilder({ groupId, appId })
|
||||
},
|
||||
|
||||
removeGroupAppBuilder: async (groupId, appId) => {
|
||||
return await API.removeGroupAppBuilder({ groupId, appId })
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -125,6 +125,13 @@ export const createLicensingStore = () => {
|
|||
const syncAutomationsEnabled = license.features.includes(
|
||||
Constants.Features.SYNC_AUTOMATIONS
|
||||
)
|
||||
const perAppBuildersEnabled = license.features.includes(
|
||||
Constants.Features.APP_BUILDERS
|
||||
)
|
||||
|
||||
const isViewPermissionsEnabled = license.features.includes(
|
||||
Constants.Features.VIEW_PERMISSIONS
|
||||
)
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
|
@ -140,6 +147,8 @@ export const createLicensingStore = () => {
|
|||
auditLogsEnabled,
|
||||
enforceableSSO,
|
||||
syncAutomationsEnabled,
|
||||
isViewPermissionsEnabled,
|
||||
perAppBuildersEnabled,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -112,12 +112,16 @@ export function createUsersStore() {
|
|||
return await API.saveUser(user)
|
||||
}
|
||||
|
||||
async function addAppBuilder(userId, appId) {
|
||||
return await API.addAppBuilder({ userId, appId })
|
||||
}
|
||||
|
||||
async function removeAppBuilder(userId, appId) {
|
||||
return await API.removeAppBuilder({ userId, appId })
|
||||
}
|
||||
|
||||
const getUserRole = user =>
|
||||
sdk.users.isAdmin(user)
|
||||
? "admin"
|
||||
: sdk.users.isBuilder(user)
|
||||
? "developer"
|
||||
: "appUser"
|
||||
sdk.users.isAdminOrGlobalBuilder(user) ? "admin" : "appUser"
|
||||
|
||||
const refreshUsage =
|
||||
fn =>
|
||||
|
@ -139,6 +143,8 @@ export function createUsersStore() {
|
|||
getInvites,
|
||||
updateInvite,
|
||||
getUserCountByApp,
|
||||
addAppBuilder,
|
||||
removeAppBuilder,
|
||||
// any operation that adds or deletes users
|
||||
acceptInvite,
|
||||
create: refreshUsage(create),
|
||||
|
|
|
@ -4622,14 +4622,15 @@
|
|||
"type": "field/sortable",
|
||||
"label": "Sort by",
|
||||
"key": "sortColumn",
|
||||
"placeholder": "None"
|
||||
"placeholder": "Default"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Sort order",
|
||||
"key": "sortOrder",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Ascending"
|
||||
"defaultValue": "Ascending",
|
||||
"dependsOn": "sortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -5271,7 +5272,7 @@
|
|||
},
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Table",
|
||||
"label": "Data",
|
||||
"key": "dataSource"
|
||||
},
|
||||
{
|
||||
|
@ -5443,7 +5444,7 @@
|
|||
"settings": [
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Table",
|
||||
"label": "Data",
|
||||
"key": "dataSource",
|
||||
"required": true
|
||||
},
|
||||
|
@ -5534,7 +5535,7 @@
|
|||
"settings": [
|
||||
{
|
||||
"type": "table",
|
||||
"label": "Table",
|
||||
"label": "Data",
|
||||
"key": "table",
|
||||
"required": true
|
||||
},
|
||||
|
@ -5560,7 +5561,8 @@
|
|||
"label": "Sort order",
|
||||
"key": "initialSortOrder",
|
||||
"options": ["Ascending", "Descending"],
|
||||
"defaultValue": "Ascending"
|
||||
"defaultValue": "Ascending",
|
||||
"dependsOn": "initialSortColumn"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
|
|
@ -272,12 +272,36 @@
|
|||
return missing
|
||||
})
|
||||
|
||||
// Run any migrations
|
||||
runMigrations(instance, settingsDefinition)
|
||||
|
||||
// Force an initial enrichment of the new settings
|
||||
enrichComponentSettings(get(context), settingsDefinitionMap, {
|
||||
force: true,
|
||||
})
|
||||
}
|
||||
|
||||
const runMigrations = (instance, settingsDefinition) => {
|
||||
settingsDefinition.forEach(setting => {
|
||||
// Migrate "table" settings to ensure they have a type and resource ID
|
||||
if (setting.type === "table") {
|
||||
const val = instance[setting.key]
|
||||
if (val) {
|
||||
if (!val.type) {
|
||||
val.type = "table"
|
||||
}
|
||||
if (!val.resourceId) {
|
||||
if (val.type === "viewV2") {
|
||||
val.resourceId = val.id
|
||||
} else {
|
||||
val.resourceId = val.tableId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSettingsDefinitionMap = settingsDefinition => {
|
||||
let map = {}
|
||||
settingsDefinition?.forEach(setting => {
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
paginate,
|
||||
})
|
||||
|
||||
// Sanitize schema to remove hidden fields
|
||||
$: schema = sanitizeSchema($fetch.schema)
|
||||
|
||||
// Build our action context
|
||||
$: actions = [
|
||||
{
|
||||
|
@ -66,7 +69,7 @@
|
|||
rows: $fetch.rows,
|
||||
info: $fetch.info,
|
||||
datasource: dataSource || {},
|
||||
schema: $fetch.schema,
|
||||
schema,
|
||||
rowsLength: $fetch.rows.length,
|
||||
|
||||
// Undocumented properties. These aren't supposed to be used in builder
|
||||
|
@ -94,6 +97,19 @@
|
|||
})
|
||||
}
|
||||
|
||||
const sanitizeSchema = schema => {
|
||||
if (!schema) {
|
||||
return schema
|
||||
}
|
||||
let cloned = { ...schema }
|
||||
Object.entries(cloned).forEach(([field, fieldSchema]) => {
|
||||
if (fieldSchema.visible === false) {
|
||||
delete cloned[field]
|
||||
}
|
||||
})
|
||||
return cloned
|
||||
}
|
||||
|
||||
const addQueryExtension = (key, extension) => {
|
||||
if (!key || !extension) {
|
||||
return
|
||||
|
|
|
@ -38,11 +38,8 @@
|
|||
class:in-builder={$builderStore.inBuilder}
|
||||
>
|
||||
<Grid
|
||||
tableId={table?.tableId}
|
||||
datasource={table}
|
||||
{API}
|
||||
{allowAddRows}
|
||||
{allowEditRows}
|
||||
{allowDeleteRows}
|
||||
{stripeRows}
|
||||
{initialFilter}
|
||||
{initialSortColumn}
|
||||
|
@ -50,9 +47,13 @@
|
|||
{fixedRowHeight}
|
||||
{columnWhitelist}
|
||||
{schemaOverrides}
|
||||
canAddRows={allowAddRows}
|
||||
canEditRows={allowEditRows}
|
||||
canDeleteRows={allowDeleteRows}
|
||||
canEditColumns={false}
|
||||
canExpandRows={false}
|
||||
canSaveSchema={false}
|
||||
showControls={false}
|
||||
allowExpandRows={false}
|
||||
allowSchemaChanges={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
/>
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
|
||||
// Accommodate old config to ensure delete button does not reappear
|
||||
$: deleteLabel = sidePanelShowDelete === false ? "" : sidePanelDeleteLabel
|
||||
|
||||
$: isDSPlus = dataSource?.type === "table" || dataSource?.type === "viewV2"
|
||||
$: fetchSchema(dataSource)
|
||||
$: enrichSearchColumns(searchColumns, schema).then(
|
||||
val => (enrichedSearchColumns = val)
|
||||
|
@ -56,7 +56,7 @@
|
|||
$: editTitle = getEditTitle(detailsFormBlockId, primaryDisplay)
|
||||
$: normalFields = getNormalFields(schema)
|
||||
$: rowClickActions =
|
||||
clickBehaviour === "actions" || dataSource?.type !== "table"
|
||||
clickBehaviour === "actions" || !isDSPlus
|
||||
? onClick
|
||||
: [
|
||||
{
|
||||
|
@ -78,7 +78,7 @@
|
|||
},
|
||||
]
|
||||
$: buttonClickActions =
|
||||
titleButtonClickBehaviour === "actions" || dataSource?.type !== "table"
|
||||
titleButtonClickBehaviour === "actions" || !isDSPlus
|
||||
? onClickTitleButton
|
||||
: [
|
||||
{
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
"##eventHandlerType": "Save Row",
|
||||
parameters: {
|
||||
providerId: formId,
|
||||
tableId: dataSource?.tableId,
|
||||
tableId: dataSource?.resourceId,
|
||||
notificationOverride,
|
||||
},
|
||||
},
|
||||
|
@ -78,7 +78,7 @@
|
|||
"##eventHandlerType": "Delete Row",
|
||||
parameters: {
|
||||
confirm: true,
|
||||
tableId: dataSource?.tableId,
|
||||
tableId: dataSource?.resourceId,
|
||||
rowId: `{{ ${safe(repeaterId)}.${safe("_id")} }}`,
|
||||
revId: `{{ ${safe(repeaterId)}.${safe("_rev")} }}`,
|
||||
notificationOverride,
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
return
|
||||
}
|
||||
nextIndicators[idx].visible =
|
||||
nextIndicators[idx].isSidePanel || entries[0].isIntersecting
|
||||
nextIndicators[idx].insideSidePanel || entries[0].isIntersecting
|
||||
if (++callbackCount === observers.length) {
|
||||
indicators = nextIndicators
|
||||
updating = false
|
||||
|
@ -125,7 +125,7 @@
|
|||
width: elBounds.width + 4,
|
||||
height: elBounds.height + 4,
|
||||
visible: false,
|
||||
isSidePanel: child.classList.contains("side-panel"),
|
||||
insideSidePanel: !!child.closest(".side-panel"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ export const createDataSourceStore = () => {
|
|||
// Extract table ID
|
||||
if (dataSource.type === "table" || dataSource.type === "view") {
|
||||
dataSourceId = dataSource.tableId
|
||||
} else if (dataSource.type === "viewV2") {
|
||||
dataSourceId = dataSource.id
|
||||
}
|
||||
|
||||
// Only one side of the relationship is required as a trigger, as it will
|
||||
|
@ -79,7 +81,7 @@ export const createDataSourceStore = () => {
|
|||
|
||||
// Fetch related table IDs from table schema
|
||||
let schema
|
||||
if (options.invalidateRelationships) {
|
||||
if (options.invalidateRelationships && !dataSourceId?.includes("view_")) {
|
||||
try {
|
||||
const definition = await API.fetchTableDefinition(dataSourceId)
|
||||
schema = definition?.schema
|
||||
|
|
|
@ -42,7 +42,7 @@ const saveRowHandler = async (action, context) => {
|
|||
}
|
||||
|
||||
// Refresh related datasources
|
||||
await dataSourceStore.actions.invalidateDataSource(row.tableId, {
|
||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||
invalidateRelationships: true,
|
||||
})
|
||||
|
||||
|
@ -75,7 +75,7 @@ const duplicateRowHandler = async (action, context) => {
|
|||
}
|
||||
|
||||
// Refresh related datasources
|
||||
await dataSourceStore.actions.invalidateDataSource(row.tableId, {
|
||||
await dataSourceStore.actions.invalidateDataSource(tableId, {
|
||||
invalidateRelationships: true,
|
||||
})
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import RelationshipFetch from "@budibase/frontend-core/src/fetch/RelationshipFet
|
|||
import NestedProviderFetch from "@budibase/frontend-core/src/fetch/NestedProviderFetch.js"
|
||||
import FieldFetch from "@budibase/frontend-core/src/fetch/FieldFetch.js"
|
||||
import JSONArrayFetch from "@budibase/frontend-core/src/fetch/JSONArrayFetch.js"
|
||||
import ViewV2Fetch from "@budibase/frontend-core/src/fetch/ViewV2Fetch.js"
|
||||
|
||||
/**
|
||||
* Fetches the schema of any kind of datasource.
|
||||
|
@ -21,6 +22,7 @@ export const fetchDatasourceSchema = async (
|
|||
const handler = {
|
||||
table: TableFetch,
|
||||
view: ViewFetch,
|
||||
viewV2: ViewV2Fetch,
|
||||
query: QueryFetch,
|
||||
link: RelationshipFetch,
|
||||
provider: NestedProviderFetch,
|
||||
|
@ -49,6 +51,15 @@ export const fetchDatasourceSchema = async (
|
|||
return null
|
||||
}
|
||||
|
||||
// Strip hidden fields from views
|
||||
if (datasource.type === "viewV2") {
|
||||
Object.keys(schema).forEach(field => {
|
||||
if (!schema[field].visible) {
|
||||
delete schema[field]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Enrich schema with relationships if required
|
||||
if (definition?.sql && options?.enrichRelationships) {
|
||||
const relationshipAdditions = await getRelationshipSchemaAdditions(schema)
|
||||
|
|
|
@ -104,5 +104,27 @@ export const buildGroupsEndpoints = API => {
|
|||
removeAppsFromGroup: async (groupId, appArray) => {
|
||||
return updateGroupResource(groupId, "apps", "remove", appArray)
|
||||
},
|
||||
|
||||
/**
|
||||
* Add app builder to group
|
||||
* @param groupId The group to update
|
||||
* @param appId The app id where the builder will be added
|
||||
*/
|
||||
addGroupAppBuilder: async ({ groupId, appId }) => {
|
||||
return await API.post({
|
||||
url: `/api/global/groups/${groupId}/app/${appId}/builder`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove app builder from group
|
||||
* @param groupId The group to update
|
||||
* @param appId The app id where the builder will be removed
|
||||
*/
|
||||
removeGroupAppBuilder: async ({ groupId, appId }) => {
|
||||
return await API.delete({
|
||||
url: `/api/global/groups/${groupId}/app/${appId}/builder`,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import { buildTemplateEndpoints } from "./templates"
|
|||
import { buildUserEndpoints } from "./user"
|
||||
import { buildSelfEndpoints } from "./self"
|
||||
import { buildViewEndpoints } from "./views"
|
||||
import { buildViewV2Endpoints } from "./viewsV2"
|
||||
import { buildLicensingEndpoints } from "./licensing"
|
||||
import { buildGroupsEndpoints } from "./groups"
|
||||
import { buildPluginEndpoints } from "./plugins"
|
||||
|
@ -279,5 +280,6 @@ export const createAPIClient = config => {
|
|||
...buildEventEndpoints(API),
|
||||
...buildAuditLogsEndpoints(API),
|
||||
...buildLogsEndpoints(API),
|
||||
viewV2: buildViewV2Endpoints(API),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,23 @@ export const buildRowEndpoints = API => ({
|
|||
return
|
||||
}
|
||||
return await API.post({
|
||||
url: `/api/${row.tableId}/rows`,
|
||||
url: `/api/${row._viewId || row.tableId}/rows`,
|
||||
body: row,
|
||||
suppressErrors,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Patches a row in a table.
|
||||
* @param row the row to patch
|
||||
* @param suppressErrors whether or not to suppress error notifications
|
||||
*/
|
||||
patchRow: async (row, suppressErrors = false) => {
|
||||
if (!row?.tableId && !row?._viewId) {
|
||||
return
|
||||
}
|
||||
return await API.patch({
|
||||
url: `/api/${row._viewId || row.tableId}/rows`,
|
||||
body: row,
|
||||
suppressErrors,
|
||||
})
|
||||
|
@ -31,7 +47,7 @@ export const buildRowEndpoints = API => ({
|
|||
|
||||
/**
|
||||
* Deletes a row from a table.
|
||||
* @param tableId the ID of the table to delete from
|
||||
* @param tableId the ID of the table or view to delete from
|
||||
* @param rowId the ID of the row to delete
|
||||
* @param revId the rev of the row to delete
|
||||
*/
|
||||
|
@ -50,10 +66,13 @@ export const buildRowEndpoints = API => ({
|
|||
|
||||
/**
|
||||
* Deletes multiple rows from a table.
|
||||
* @param tableId the table ID to delete the rows from
|
||||
* @param tableId the table or view ID to delete the rows from
|
||||
* @param rows the array of rows to delete
|
||||
*/
|
||||
deleteRows: async ({ tableId, rows }) => {
|
||||
rows?.forEach(row => {
|
||||
delete row?._viewId
|
||||
})
|
||||
return await API.delete({
|
||||
url: `/api/${tableId}/rows`,
|
||||
body: {
|
||||
|
|
|
@ -156,13 +156,14 @@ export const buildUserEndpoints = API => ({
|
|||
return await API.post({
|
||||
url: "/api/global/users/onboard",
|
||||
body: payload.map(invite => {
|
||||
const { email, admin, builder, apps } = invite
|
||||
const { email, admin, builder, apps, appBuilders } = invite
|
||||
return {
|
||||
email,
|
||||
userInfo: {
|
||||
admin: admin ? { global: true } : undefined,
|
||||
builder: builder ? { global: true } : undefined,
|
||||
apps: apps ? apps : undefined,
|
||||
appBuilders,
|
||||
},
|
||||
}
|
||||
}),
|
||||
|
@ -175,10 +176,12 @@ export const buildUserEndpoints = API => ({
|
|||
* @param invite the invite code sent in the email
|
||||
*/
|
||||
updateUserInvite: async invite => {
|
||||
console.log(invite)
|
||||
await API.post({
|
||||
url: `/api/global/users/invite/update/${invite.code}`,
|
||||
body: {
|
||||
apps: invite.apps,
|
||||
appBuilders: invite.appBuilders,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
@ -250,4 +253,26 @@ export const buildUserEndpoints = API => ({
|
|||
url: `/api/global/users/count/${appId}`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a per app builder to the selected app
|
||||
* @param appId the applications id
|
||||
* @param userId The id of the user to add as a builder
|
||||
*/
|
||||
addAppBuilder: async ({ userId, appId }) => {
|
||||
return await API.post({
|
||||
url: `/api/global/users/${userId}/app/${appId}/builder`,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes a per app builder to the selected app
|
||||
* @param appId the applications id
|
||||
* @param userId The id of the user to remove as a builder
|
||||
*/
|
||||
removeAppBuilder: async ({ userId, appId }) => {
|
||||
return await API.delete({
|
||||
url: `/api/global/users/${userId}/app/${appId}/builder`,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
export const buildViewV2Endpoints = API => ({
|
||||
/**
|
||||
* Fetches the definition of a view
|
||||
* @param viewId the ID of the view to fetch
|
||||
*/
|
||||
fetchDefinition: async viewId => {
|
||||
return await API.get({
|
||||
url: `/api/v2/views/${viewId}`,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Create a new view
|
||||
* @param view the view object
|
||||
*/
|
||||
create: async view => {
|
||||
return await API.post({
|
||||
url: `/api/v2/views`,
|
||||
body: view,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Updates a view
|
||||
* @param view the view object
|
||||
*/
|
||||
update: async view => {
|
||||
return await API.put({
|
||||
url: `/api/v2/views/${view.id}`,
|
||||
body: view,
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Fetches all rows in a view
|
||||
* @param viewId the id of the view
|
||||
* @param query the search query
|
||||
* @param paginate whether to paginate or not
|
||||
* @param limit page size
|
||||
* @param bookmark pagination cursor
|
||||
* @param sort sort column
|
||||
* @param sortOrder sort order
|
||||
* @param sortType sort type (text or numeric)
|
||||
*/
|
||||
fetch: async ({
|
||||
viewId,
|
||||
query,
|
||||
paginate,
|
||||
limit,
|
||||
bookmark,
|
||||
sort,
|
||||
sortOrder,
|
||||
sortType,
|
||||
}) => {
|
||||
return await API.post({
|
||||
url: `/api/v2/views/${viewId}/search`,
|
||||
body: {
|
||||
query,
|
||||
paginate,
|
||||
limit,
|
||||
bookmark,
|
||||
sort,
|
||||
sortOrder,
|
||||
sortType,
|
||||
},
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Delete a view
|
||||
* @param viewId the id of the view
|
||||
*/
|
||||
delete: async viewId => {
|
||||
return await API.delete({ url: `/api/v2/views/${viewId}` })
|
||||
},
|
||||
})
|
|
@ -34,7 +34,7 @@
|
|||
column.schema.autocolumn ||
|
||||
column.schema.disabled ||
|
||||
column.schema.type === "formula" ||
|
||||
(!$config.allowEditRows && row._id)
|
||||
(!$config.canEditRows && row._id)
|
||||
|
||||
// Register this cell API if the row is focused
|
||||
$: {
|
||||
|
@ -58,9 +58,14 @@
|
|||
isReadonly: () => readonly,
|
||||
getType: () => column.schema.type,
|
||||
getValue: () => row[column.name],
|
||||
setValue: value => {
|
||||
setValue: (value, options = { save: true }) => {
|
||||
validation.actions.setError(cellId, null)
|
||||
updateValue(row._id, column.name, value)
|
||||
updateValue({
|
||||
rowId: row._id,
|
||||
column: column.name,
|
||||
value,
|
||||
save: options?.save,
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<div
|
||||
on:click={select}
|
||||
class="checkbox"
|
||||
class:visible={$config.allowDeleteRows &&
|
||||
class:visible={$config.canDeleteRows &&
|
||||
(disableNumber || rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
<Checkbox value={rowSelected} {disabled} />
|
||||
|
@ -48,14 +48,14 @@
|
|||
{#if !disableNumber}
|
||||
<div
|
||||
class="number"
|
||||
class:visible={!$config.allowDeleteRows ||
|
||||
class:visible={!$config.canDeleteRows ||
|
||||
!(rowSelected || rowHovered || rowFocused)}
|
||||
>
|
||||
{row.__idx + 1}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if rowSelected && $config.allowDeleteRows}
|
||||
{#if rowSelected && $config.canDeleteRows}
|
||||
<div class="delete" on:click={() => dispatch("request-bulk-delete")}>
|
||||
<Icon
|
||||
name="Delete"
|
||||
|
@ -64,7 +64,7 @@
|
|||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="expand" class:visible={$config.allowExpandRows && expandable}>
|
||||
<div class="expand" class:visible={$config.canExpandRows && expandable}>
|
||||
<Icon
|
||||
size="S"
|
||||
name="Maximize"
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
popover.hide()
|
||||
editIsOpen = false
|
||||
}
|
||||
|
||||
const onMouseDown = e => {
|
||||
if (e.button === 0 && orderable) {
|
||||
timeout = setTimeout(() => {
|
||||
|
@ -116,6 +117,7 @@
|
|||
columns.actions.saveChanges()
|
||||
open = false
|
||||
}
|
||||
|
||||
onMount(() => subscribe("close-edit-column", cancelEdit))
|
||||
</script>
|
||||
|
||||
|
@ -170,7 +172,6 @@
|
|||
align="right"
|
||||
offset={0}
|
||||
popoverTarget={document.getElementById(`grid-${rand}`)}
|
||||
animate={false}
|
||||
customZindex={100}
|
||||
>
|
||||
{#if editIsOpen}
|
||||
|
@ -187,7 +188,7 @@
|
|||
<MenuItem
|
||||
icon="Edit"
|
||||
on:click={editColumn}
|
||||
disabled={!$config.allowSchemaChanges || column.schema.disabled}
|
||||
disabled={!$config.canEditColumns || column.schema.disabled}
|
||||
>
|
||||
Edit column
|
||||
</MenuItem>
|
||||
|
@ -195,7 +196,6 @@
|
|||
icon="Label"
|
||||
on:click={makeDisplayColumn}
|
||||
disabled={idx === "sticky" ||
|
||||
!$config.allowSchemaChanges ||
|
||||
bannedDisplayColumnTypes.includes(column.schema.type)}
|
||||
>
|
||||
Use as display column
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { ActionButton, Popover, Toggle, Icon } from "@budibase/bbui"
|
||||
import { getColumnIcon } from "../lib/utils"
|
||||
|
||||
const { columns, stickyColumn } = getContext("grid")
|
||||
const { columns, stickyColumn, dispatch } = getContext("grid")
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
@ -11,33 +11,36 @@
|
|||
$: anyHidden = $columns.some(col => !col.visible)
|
||||
$: text = getText($columns)
|
||||
|
||||
const toggleVisibility = (column, visible) => {
|
||||
const toggleVisibility = async (column, visible) => {
|
||||
columns.update(state => {
|
||||
const index = state.findIndex(col => col.name === column.name)
|
||||
state[index].visible = visible
|
||||
return state.slice()
|
||||
})
|
||||
columns.actions.saveChanges()
|
||||
await columns.actions.saveChanges()
|
||||
dispatch(visible ? "show-column" : "hide-column")
|
||||
}
|
||||
|
||||
const showAll = () => {
|
||||
const showAll = async () => {
|
||||
columns.update(state => {
|
||||
return state.map(col => ({
|
||||
...col,
|
||||
visible: true,
|
||||
}))
|
||||
})
|
||||
columns.actions.saveChanges()
|
||||
await columns.actions.saveChanges()
|
||||
dispatch("show-column")
|
||||
}
|
||||
|
||||
const hideAll = () => {
|
||||
const hideAll = async () => {
|
||||
columns.update(state => {
|
||||
return state.map(col => ({
|
||||
...col,
|
||||
visible: false,
|
||||
}))
|
||||
})
|
||||
columns.actions.saveChanges()
|
||||
await columns.actions.saveChanges()
|
||||
dispatch("hide-column")
|
||||
}
|
||||
|
||||
const getText = columns => {
|
||||
|
|
|
@ -8,8 +8,14 @@
|
|||
SmallRowHeight,
|
||||
} from "../lib/constants"
|
||||
|
||||
const { stickyColumn, columns, rowHeight, table, fixedRowHeight } =
|
||||
getContext("grid")
|
||||
const {
|
||||
stickyColumn,
|
||||
columns,
|
||||
rowHeight,
|
||||
definition,
|
||||
fixedRowHeight,
|
||||
datasource,
|
||||
} = getContext("grid")
|
||||
|
||||
// Some constants for column width options
|
||||
const smallColSize = 120
|
||||
|
@ -60,8 +66,8 @@
|
|||
]
|
||||
|
||||
const changeRowHeight = height => {
|
||||
columns.actions.saveTable({
|
||||
...$table,
|
||||
datasource.actions.saveDefinition({
|
||||
...$definition,
|
||||
rowHeight: height,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
let anchor
|
||||
|
||||
$: columnOptions = getColumnOptions($stickyColumn, $columns)
|
||||
$: checkValidSortColumn($sort.column, $stickyColumn, $columns)
|
||||
$: orderOptions = getOrderOptions($sort.column, columnOptions)
|
||||
|
||||
const getColumnOptions = (stickyColumn, columns) => {
|
||||
|
@ -46,8 +45,8 @@
|
|||
|
||||
const updateSortColumn = e => {
|
||||
sort.update(state => ({
|
||||
...state,
|
||||
column: e.detail,
|
||||
order: e.detail ? state.order : "ascending",
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -57,29 +56,6 @@
|
|||
order: e.detail,
|
||||
}))
|
||||
}
|
||||
|
||||
// Ensure we never have a sort column selected that is not visible
|
||||
const checkValidSortColumn = (sortColumn, stickyColumn, columns) => {
|
||||
if (!sortColumn) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
sortColumn !== stickyColumn?.name &&
|
||||
!columns.some(col => col.name === sortColumn)
|
||||
) {
|
||||
if (stickyColumn) {
|
||||
sort.update(state => ({
|
||||
...state,
|
||||
column: stickyColumn.name,
|
||||
}))
|
||||
} else {
|
||||
sort.update(state => ({
|
||||
...state,
|
||||
column: columns[0]?.name,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={anchor}>
|
||||
|
@ -98,21 +74,23 @@
|
|||
<Popover bind:open {anchor} align="left">
|
||||
<div class="content">
|
||||
<Select
|
||||
placeholder={null}
|
||||
placeholder="Default"
|
||||
value={$sort.column}
|
||||
options={columnOptions}
|
||||
autoWidth
|
||||
on:change={updateSortColumn}
|
||||
label="Column"
|
||||
/>
|
||||
<Select
|
||||
placeholder={null}
|
||||
value={$sort.order}
|
||||
options={orderOptions}
|
||||
autoWidth
|
||||
on:change={updateSortOrder}
|
||||
label="Order"
|
||||
/>
|
||||
{#if $sort.column}
|
||||
<Select
|
||||
placeholder={null}
|
||||
value={$sort.order || "ascending"}
|
||||
options={orderOptions}
|
||||
autoWidth
|
||||
on:change={updateSortOrder}
|
||||
label="Order"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<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"
|
||||
|
@ -28,14 +29,15 @@
|
|||
} from "../lib/constants"
|
||||
|
||||
export let API = null
|
||||
export let tableId = null
|
||||
export let datasource = null
|
||||
export let schemaOverrides = null
|
||||
export let columnWhitelist = null
|
||||
export let allowAddRows = true
|
||||
export let allowExpandRows = true
|
||||
export let allowEditRows = true
|
||||
export let allowDeleteRows = true
|
||||
export let allowSchemaChanges = true
|
||||
export let canAddRows = true
|
||||
export let canExpandRows = true
|
||||
export let canEditRows = true
|
||||
export let canDeleteRows = true
|
||||
export let canEditColumns = true
|
||||
export let canSaveSchema = true
|
||||
export let stripeRows = false
|
||||
export let collaboration = true
|
||||
export let showAvatars = true
|
||||
|
@ -50,11 +52,14 @@
|
|||
// Unique identifier for DOM nodes inside this instance
|
||||
const rand = Math.random()
|
||||
|
||||
// Store props in a store for reference in other stores
|
||||
const props = writable($$props)
|
||||
|
||||
// Build up context
|
||||
let context = {
|
||||
API: API || createAPIClient(),
|
||||
rand,
|
||||
props: $$props,
|
||||
props,
|
||||
}
|
||||
context = { ...context, ...createEventManagers() }
|
||||
context = attachStores(context)
|
||||
|
@ -71,19 +76,19 @@
|
|||
contentLines,
|
||||
gridFocused,
|
||||
error,
|
||||
canAddRows,
|
||||
} = context
|
||||
|
||||
// Keep config store up to date with props
|
||||
$: config.set({
|
||||
tableId,
|
||||
$: props.set({
|
||||
datasource,
|
||||
schemaOverrides,
|
||||
columnWhitelist,
|
||||
allowAddRows,
|
||||
allowExpandRows,
|
||||
allowEditRows,
|
||||
allowDeleteRows,
|
||||
allowSchemaChanges,
|
||||
canAddRows,
|
||||
canExpandRows,
|
||||
canEditRows,
|
||||
canDeleteRows,
|
||||
canEditColumns,
|
||||
canSaveSchema,
|
||||
stripeRows,
|
||||
collaboration,
|
||||
showAvatars,
|
||||
|
@ -155,7 +160,7 @@
|
|||
</HeaderRow>
|
||||
<GridBody />
|
||||
</div>
|
||||
{#if $canAddRows}
|
||||
{#if $config.canAddRows}
|
||||
<NewRow />
|
||||
{/if}
|
||||
<div class="overlays">
|
||||
|
@ -179,7 +184,7 @@
|
|||
<ProgressCircle />
|
||||
</div>
|
||||
{/if}
|
||||
{#if allowDeleteRows}
|
||||
{#if $config.canDeleteRows}
|
||||
<BulkDeleteHandler />
|
||||
{/if}
|
||||
<KeyboardManager />
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
renderedRows,
|
||||
renderedColumns,
|
||||
rowVerticalInversionIndex,
|
||||
canAddRows,
|
||||
hoveredRowId,
|
||||
dispatch,
|
||||
isDragging,
|
||||
config,
|
||||
} = getContext("grid")
|
||||
|
||||
let body
|
||||
|
@ -43,7 +43,7 @@
|
|||
invertY={idx >= $rowVerticalInversionIndex}
|
||||
/>
|
||||
{/each}
|
||||
{#if $canAddRows}
|
||||
{#if $config.canAddRows}
|
||||
<div
|
||||
class="blank"
|
||||
class:highlighted={$hoveredRowId === BlankRowID}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<script>
|
||||
import NewColumnButton from "./NewColumnButton.svelte"
|
||||
|
||||
import { getContext } from "svelte"
|
||||
import GridScrollWrapper from "./GridScrollWrapper.svelte"
|
||||
import HeaderCell from "../cells/HeaderCell.svelte"
|
||||
import { TempTooltip, TooltipType } from "@budibase/bbui"
|
||||
|
||||
const { renderedColumns, config, hasNonAutoColumn, tableId, loading } =
|
||||
const { renderedColumns, config, hasNonAutoColumn, datasource, loading } =
|
||||
getContext("grid")
|
||||
</script>
|
||||
|
||||
|
@ -20,8 +19,8 @@
|
|||
{/each}
|
||||
</div>
|
||||
</GridScrollWrapper>
|
||||
{#if $config.allowSchemaChanges}
|
||||
{#key $tableId}
|
||||
{#if $config.canEditColumns}
|
||||
{#key $datasource}
|
||||
<TempTooltip
|
||||
text="Click here to create your first column"
|
||||
type={TooltipType.Info}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
let anchor
|
||||
let open = false
|
||||
|
||||
$: columnsWidth = $renderedColumns.reduce(
|
||||
(total, col) => (total += col.width),
|
||||
0
|
||||
|
@ -17,6 +18,7 @@
|
|||
const close = () => {
|
||||
open = false
|
||||
}
|
||||
|
||||
onMount(() => subscribe("close-edit-column", close))
|
||||
</script>
|
||||
|
||||
|
@ -35,7 +37,6 @@
|
|||
align={$renderedColumns.length ? "right" : "left"}
|
||||
offset={0}
|
||||
popoverTarget={document.getElementById(`add-column-button`)}
|
||||
animate={false}
|
||||
customZindex={100}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
dispatch,
|
||||
rows,
|
||||
focusedCellAPI,
|
||||
tableId,
|
||||
datasource,
|
||||
subscribe,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
|
@ -28,7 +28,7 @@
|
|||
columnHorizontalInversionIndex,
|
||||
selectedRows,
|
||||
loading,
|
||||
canAddRows,
|
||||
config,
|
||||
} = getContext("grid")
|
||||
|
||||
let visible = false
|
||||
|
@ -38,7 +38,7 @@
|
|||
|
||||
$: firstColumn = $stickyColumn || $renderedColumns[0]
|
||||
$: width = GutterWidth + ($stickyColumn?.width || 0)
|
||||
$: $tableId, (visible = false)
|
||||
$: $datasource, (visible = false)
|
||||
$: invertY = shouldInvertY(offset, $rowVerticalInversionIndex, $renderedRows)
|
||||
$: selectedRowCount = Object.values($selectedRows).length
|
||||
$: hasNoRows = !$rows.length
|
||||
|
@ -120,8 +120,8 @@
|
|||
document.addEventListener("keydown", handleKeyPress)
|
||||
}
|
||||
|
||||
const updateValue = (rowId, columnName, val) => {
|
||||
newRow[columnName] = val
|
||||
const updateValue = ({ column, value }) => {
|
||||
newRow[column] = value
|
||||
}
|
||||
|
||||
const addViaModal = () => {
|
||||
|
@ -154,7 +154,7 @@
|
|||
condition={hasNoRows && !$loading}
|
||||
type={TooltipType.Info}
|
||||
>
|
||||
{#if !visible && !selectedRowCount && $canAddRows}
|
||||
{#if !visible && !selectedRowCount && $config.canAddRows}
|
||||
<div
|
||||
class="new-row-fab"
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
renderedRows,
|
||||
focusedCellId,
|
||||
hoveredRowId,
|
||||
canAddRows,
|
||||
config,
|
||||
selectedCellMap,
|
||||
focusedRow,
|
||||
scrollLeft,
|
||||
|
@ -94,7 +94,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $canAddRows}
|
||||
{#if $config.canAddRows}
|
||||
<div
|
||||
class="row new"
|
||||
on:mouseenter={$isDragging
|
||||
|
|
|
@ -3,18 +3,21 @@ import { createWebsocket } from "../../../utils"
|
|||
import { SocketEvent, GridSocketEvent } from "@budibase/shared-core"
|
||||
|
||||
export const createGridWebsocket = context => {
|
||||
const { rows, tableId, users, focusedCellId, table, API } = context
|
||||
const { rows, datasource, users, focusedCellId, definition, API } = context
|
||||
const socket = createWebsocket("/socket/grid")
|
||||
|
||||
const connectToTable = tableId => {
|
||||
const connectToDatasource = datasource => {
|
||||
if (!socket.connected) {
|
||||
return
|
||||
}
|
||||
// Identify which table we are editing
|
||||
const appId = API.getAppID()
|
||||
socket.emit(
|
||||
GridSocketEvent.SelectTable,
|
||||
{ tableId, appId },
|
||||
GridSocketEvent.SelectDatasource,
|
||||
{
|
||||
datasource,
|
||||
appId,
|
||||
},
|
||||
({ users: gridUsers }) => {
|
||||
users.set(gridUsers)
|
||||
}
|
||||
|
@ -23,7 +26,7 @@ export const createGridWebsocket = context => {
|
|||
|
||||
// Built-in events
|
||||
socket.on("connect", () => {
|
||||
connectToTable(get(tableId))
|
||||
connectToDatasource(get(datasource))
|
||||
})
|
||||
socket.on("connect_error", err => {
|
||||
console.log("Failed to connect to grid websocket:", err.message)
|
||||
|
@ -48,16 +51,19 @@ export const createGridWebsocket = context => {
|
|||
})
|
||||
|
||||
// Table events
|
||||
socket.onOther(GridSocketEvent.TableChange, ({ table: newTable }) => {
|
||||
// Only update table if one exists. If the table was deleted then we don't
|
||||
// want to know - let the builder navigate away
|
||||
if (newTable) {
|
||||
table.set(newTable)
|
||||
socket.onOther(
|
||||
GridSocketEvent.DatasourceChange,
|
||||
({ datasource: newDatasource }) => {
|
||||
// Only update definition if one exists. If the datasource was deleted
|
||||
// then we don't want to know - let the builder navigate away
|
||||
if (newDatasource) {
|
||||
definition.set(newDatasource)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Change websocket connection when table changes
|
||||
tableId.subscribe(connectToTable)
|
||||
datasource.subscribe(connectToDatasource)
|
||||
|
||||
// Notify selected cell changes
|
||||
focusedCellId.subscribe($focusedCellId => {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import { NewRowID } from "../lib/constants"
|
||||
|
||||
const {
|
||||
enrichedRows,
|
||||
rows,
|
||||
focusedCellId,
|
||||
visibleColumns,
|
||||
focusedRow,
|
||||
|
@ -16,7 +16,6 @@
|
|||
config,
|
||||
menu,
|
||||
gridFocused,
|
||||
canAddRows,
|
||||
} = getContext("grid")
|
||||
|
||||
const ignoredOriginSelectors = [
|
||||
|
@ -46,12 +45,12 @@
|
|||
e.preventDefault()
|
||||
focusFirstCell()
|
||||
} else if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
||||
if ($canAddRows) {
|
||||
if ($config.canAddRows) {
|
||||
e.preventDefault()
|
||||
dispatch("add-row-inline")
|
||||
}
|
||||
} else if (e.key === "Delete" || e.key === "Backspace") {
|
||||
if (Object.keys($selectedRows).length && $config.allowDeleteRows) {
|
||||
if (Object.keys($selectedRows).length && $config.canDeleteRows) {
|
||||
dispatch("request-bulk-delete")
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +99,7 @@
|
|||
}
|
||||
break
|
||||
case "Enter":
|
||||
if ($canAddRows) {
|
||||
if ($config.canAddRows) {
|
||||
dispatch("add-row-inline")
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +119,7 @@
|
|||
break
|
||||
case "Delete":
|
||||
case "Backspace":
|
||||
if (Object.keys($selectedRows).length && $config.allowDeleteRows) {
|
||||
if (Object.keys($selectedRows).length && $config.canDeleteRows) {
|
||||
dispatch("request-bulk-delete")
|
||||
} else {
|
||||
deleteSelectedCell()
|
||||
|
@ -131,7 +130,7 @@
|
|||
break
|
||||
case " ":
|
||||
case "Space":
|
||||
if ($config.allowDeleteRows) {
|
||||
if ($config.canDeleteRows) {
|
||||
toggleSelectRow()
|
||||
}
|
||||
break
|
||||
|
@ -143,7 +142,7 @@
|
|||
|
||||
// Focuses the first cell in the grid
|
||||
const focusFirstCell = () => {
|
||||
const firstRow = $enrichedRows[0]
|
||||
const firstRow = $rows[0]
|
||||
if (!firstRow) {
|
||||
return
|
||||
}
|
||||
|
@ -184,7 +183,7 @@
|
|||
if (!$focusedRow) {
|
||||
return
|
||||
}
|
||||
const newRow = $enrichedRows[$focusedRow.__idx + delta]
|
||||
const newRow = $rows[$focusedRow.__idx + delta]
|
||||
if (newRow) {
|
||||
const split = $focusedCellId.split("-")
|
||||
$focusedCellId = `${newRow._id}-${split[1]}`
|
||||
|
@ -216,13 +215,15 @@
|
|||
if ($focusedCellAPI && !$focusedCellAPI.isReadonly()) {
|
||||
const type = $focusedCellAPI.getType()
|
||||
if (type === "number" && keyCodeIsNumber(keyCode)) {
|
||||
$focusedCellAPI.setValue(parseInt(key))
|
||||
// Update the value locally but don't save it yet
|
||||
$focusedCellAPI.setValue(parseInt(key), { save: false })
|
||||
$focusedCellAPI.focus()
|
||||
} else if (
|
||||
["string", "barcodeqr", "longform"].includes(type) &&
|
||||
(keyCodeIsLetter(keyCode) || keyCodeIsNumber(keyCode))
|
||||
) {
|
||||
$focusedCellAPI.setValue(key)
|
||||
// Update the value locally but don't save it yet
|
||||
$focusedCellAPI.setValue(key, { save: false })
|
||||
$focusedCellAPI.focus()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
focusedCellAPI,
|
||||
focusedRowId,
|
||||
notifications,
|
||||
canAddRows,
|
||||
} = getContext("grid")
|
||||
|
||||
$: style = makeStyle($menu)
|
||||
|
@ -68,9 +67,7 @@
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Maximize"
|
||||
disabled={isNewRow ||
|
||||
!$config.allowEditRows ||
|
||||
!$config.allowExpandRows}
|
||||
disabled={isNewRow || !$config.canEditRows || !$config.canExpandRows}
|
||||
on:click={() => dispatch("edit-row", $focusedRow)}
|
||||
on:click={menu.actions.close}
|
||||
>
|
||||
|
@ -94,14 +91,14 @@
|
|||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Duplicate"
|
||||
disabled={isNewRow || !$canAddRows}
|
||||
disabled={isNewRow || !$config.canAddRows}
|
||||
on:click={duplicate}
|
||||
>
|
||||
Duplicate row
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="Delete"
|
||||
disabled={isNewRow || !$config.allowDeleteRows}
|
||||
disabled={isNewRow || !$config.canDeleteRows}
|
||||
on:click={deleteRow}
|
||||
>
|
||||
Delete row
|
||||
|
|
|
@ -8,7 +8,7 @@ export const createStores = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
export const createActions = context => {
|
||||
const { copiedCell, focusedCellAPI } = context
|
||||
|
||||
const copy = () => {
|
||||
|
|
|
@ -35,20 +35,10 @@ export const createStores = () => {
|
|||
[]
|
||||
)
|
||||
|
||||
// Checks if we have a certain column by name
|
||||
const hasColumn = column => {
|
||||
const $columns = get(columns)
|
||||
const $sticky = get(stickyColumn)
|
||||
return $columns.some(col => col.name === column) || $sticky?.name === column
|
||||
}
|
||||
|
||||
return {
|
||||
columns: {
|
||||
...columns,
|
||||
subscribe: enrichedColumns.subscribe,
|
||||
actions: {
|
||||
hasColumn,
|
||||
},
|
||||
},
|
||||
stickyColumn,
|
||||
visibleColumns,
|
||||
|
@ -56,12 +46,35 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { table, columns, stickyColumn, API, dispatch, config } = context
|
||||
const { columns, stickyColumn } = context
|
||||
|
||||
// Updates the tables primary display column
|
||||
// Derive if we have any normal columns
|
||||
const hasNonAutoColumn = derived(
|
||||
[columns, stickyColumn],
|
||||
([$columns, $stickyColumn]) => {
|
||||
let allCols = $columns || []
|
||||
if ($stickyColumn) {
|
||||
allCols = [...allCols, $stickyColumn]
|
||||
}
|
||||
const normalCols = allCols.filter(column => {
|
||||
return !column.schema?.autocolumn
|
||||
})
|
||||
return normalCols.length > 0
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
hasNonAutoColumn,
|
||||
}
|
||||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { columns, stickyColumn, datasource, definition } = context
|
||||
|
||||
// Updates the datasources primary display column
|
||||
const changePrimaryDisplay = async column => {
|
||||
return await saveTable({
|
||||
...get(table),
|
||||
return await datasource.actions.saveDefinition({
|
||||
...get(definition),
|
||||
primaryDisplay: column,
|
||||
})
|
||||
}
|
||||
|
@ -83,29 +96,14 @@ export const deriveStores = context => {
|
|||
await saveChanges()
|
||||
}
|
||||
|
||||
// Derive if we have any normal columns
|
||||
const hasNonAutoColumn = derived(
|
||||
[columns, stickyColumn],
|
||||
([$columns, $stickyColumn]) => {
|
||||
let allCols = $columns || []
|
||||
if ($stickyColumn) {
|
||||
allCols = [...allCols, $stickyColumn]
|
||||
}
|
||||
const normalCols = allCols.filter(column => {
|
||||
return !column.schema?.autocolumn
|
||||
})
|
||||
return normalCols.length > 0
|
||||
}
|
||||
)
|
||||
|
||||
// Persists column changes by saving metadata against table schema
|
||||
// Persists column changes by saving metadata against datasource schema
|
||||
const saveChanges = async () => {
|
||||
const $columns = get(columns)
|
||||
const $table = get(table)
|
||||
const $definition = get(definition)
|
||||
const $stickyColumn = get(stickyColumn)
|
||||
const newSchema = cloneDeep($table.schema)
|
||||
const newSchema = cloneDeep($definition.schema)
|
||||
|
||||
// Build new updated table schema
|
||||
// Build new updated datasource schema
|
||||
Object.keys(newSchema).forEach(column => {
|
||||
// Respect order specified by columns
|
||||
const index = $columns.findIndex(x => x.name === column)
|
||||
|
@ -125,31 +123,17 @@ export const deriveStores = context => {
|
|||
}
|
||||
})
|
||||
|
||||
await saveTable({ ...$table, schema: newSchema })
|
||||
}
|
||||
|
||||
const saveTable = async newTable => {
|
||||
// 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)
|
||||
await datasource.actions.saveDefinition({
|
||||
...$definition,
|
||||
schema: newSchema,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
hasNonAutoColumn,
|
||||
columns: {
|
||||
...columns,
|
||||
actions: {
|
||||
...columns.actions,
|
||||
saveChanges,
|
||||
saveTable,
|
||||
changePrimaryDisplay,
|
||||
changeAllColumnWidths,
|
||||
},
|
||||
|
@ -158,51 +142,7 @@ export const deriveStores = context => {
|
|||
}
|
||||
|
||||
export const initialise = context => {
|
||||
const { table, columns, stickyColumn, schemaOverrides, columnWhitelist } =
|
||||
context
|
||||
|
||||
const schema = derived(
|
||||
[table, schemaOverrides, columnWhitelist],
|
||||
([$table, $schemaOverrides, $columnWhitelist]) => {
|
||||
if (!$table?.schema) {
|
||||
return null
|
||||
}
|
||||
let newSchema = { ...$table?.schema }
|
||||
|
||||
// Edge case to temporarily allow deletion of duplicated user
|
||||
// fields that were saved with the "disabled" flag set.
|
||||
// By overriding the saved schema we ensure only overrides can
|
||||
// set the disabled flag.
|
||||
// TODO: remove in future
|
||||
Object.keys(newSchema).forEach(field => {
|
||||
newSchema[field] = {
|
||||
...newSchema[field],
|
||||
disabled: false,
|
||||
}
|
||||
})
|
||||
|
||||
// Apply schema overrides
|
||||
Object.keys($schemaOverrides || {}).forEach(field => {
|
||||
if (newSchema[field]) {
|
||||
newSchema[field] = {
|
||||
...newSchema[field],
|
||||
...$schemaOverrides[field],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Apply whitelist if specified
|
||||
if ($columnWhitelist?.length) {
|
||||
Object.keys(newSchema).forEach(key => {
|
||||
if (!$columnWhitelist.includes(key)) {
|
||||
delete newSchema[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newSchema
|
||||
}
|
||||
)
|
||||
const { definition, columns, stickyColumn, schema } = context
|
||||
|
||||
// Merge new schema fields with existing schema in order to preserve widths
|
||||
schema.subscribe($schema => {
|
||||
|
@ -211,12 +151,12 @@ export const initialise = context => {
|
|||
stickyColumn.set(null)
|
||||
return
|
||||
}
|
||||
const $table = get(table)
|
||||
const $definition = get(definition)
|
||||
|
||||
// Find primary display
|
||||
let primaryDisplay
|
||||
if ($table.primaryDisplay && $schema[$table.primaryDisplay]) {
|
||||
primaryDisplay = $table.primaryDisplay
|
||||
if ($definition.primaryDisplay && $schema[$definition.primaryDisplay]) {
|
||||
primaryDisplay = $definition.primaryDisplay
|
||||
}
|
||||
|
||||
// Get field list
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { derivedMemo } from "../../../utils"
|
||||
import { derived } from "svelte/store"
|
||||
|
||||
export const createStores = context => {
|
||||
const config = writable(context.props)
|
||||
const getProp = prop => derivedMemo(config, $config => $config[prop])
|
||||
const { props } = context
|
||||
const getProp = prop => derivedMemo(props, $props => $props[prop])
|
||||
|
||||
// Derive and memoize some props so that we can react to them in isolation
|
||||
const tableId = getProp("tableId")
|
||||
const datasource = getProp("datasource")
|
||||
const initialSortColumn = getProp("initialSortColumn")
|
||||
const initialSortOrder = getProp("initialSortOrder")
|
||||
const initialFilter = getProp("initialFilter")
|
||||
|
@ -17,8 +17,7 @@ export const createStores = context => {
|
|||
const notifyError = getProp("notifyError")
|
||||
|
||||
return {
|
||||
config,
|
||||
tableId,
|
||||
datasource,
|
||||
initialSortColumn,
|
||||
initialSortOrder,
|
||||
initialFilter,
|
||||
|
@ -29,3 +28,31 @@ export const createStores = context => {
|
|||
notifyError,
|
||||
}
|
||||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { props, hasNonAutoColumn } = context
|
||||
|
||||
// Derive features
|
||||
const config = derived(
|
||||
[props, hasNonAutoColumn],
|
||||
([$props, $hasNonAutoColumn]) => {
|
||||
let config = { ...$props }
|
||||
|
||||
// Disable some features if we're editing a view
|
||||
if ($props.datasource?.type === "viewV2") {
|
||||
config.canEditColumns = false
|
||||
}
|
||||
|
||||
// Disable adding rows if we don't have any valid columns
|
||||
if (!$hasNonAutoColumn) {
|
||||
config.canAddRows = false
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
|
||||
export const createStores = () => {
|
||||
const definition = writable(null)
|
||||
|
||||
return {
|
||||
definition,
|
||||
}
|
||||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { definition, schemaOverrides, columnWhitelist } = context
|
||||
|
||||
const schema = derived(
|
||||
[definition, schemaOverrides, columnWhitelist],
|
||||
([$definition, $schemaOverrides, $columnWhitelist]) => {
|
||||
if (!$definition?.schema) {
|
||||
return null
|
||||
}
|
||||
let newSchema = { ...$definition?.schema }
|
||||
|
||||
// Apply schema overrides
|
||||
Object.keys($schemaOverrides || {}).forEach(field => {
|
||||
if (newSchema[field]) {
|
||||
newSchema[field] = {
|
||||
...newSchema[field],
|
||||
...$schemaOverrides[field],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Apply whitelist if specified
|
||||
if ($columnWhitelist?.length) {
|
||||
Object.keys(newSchema).forEach(key => {
|
||||
if (!$columnWhitelist.includes(key)) {
|
||||
delete newSchema[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return newSchema
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
schema,
|
||||
}
|
||||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { datasource, definition, config, dispatch, table, viewV2 } = context
|
||||
|
||||
// Gets the appropriate API for the configured datasource type
|
||||
const getAPI = () => {
|
||||
const $datasource = get(datasource)
|
||||
switch ($datasource?.type) {
|
||||
case "table":
|
||||
return table
|
||||
case "viewV2":
|
||||
return viewV2
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Refreshes the datasource definition
|
||||
const refreshDefinition = async () => {
|
||||
return await getAPI()?.actions.refreshDefinition()
|
||||
}
|
||||
|
||||
// Saves the datasource definition
|
||||
const saveDefinition = async newDefinition => {
|
||||
// Update local state
|
||||
definition.set(newDefinition)
|
||||
|
||||
// Update server
|
||||
if (get(config).canSaveSchema) {
|
||||
await getAPI()?.actions.saveDefinition(newDefinition)
|
||||
}
|
||||
|
||||
// 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("updatedatasource", newDefinition)
|
||||
}
|
||||
|
||||
// Adds a row to the datasource
|
||||
const addRow = async row => {
|
||||
return await getAPI()?.actions.addRow(row)
|
||||
}
|
||||
|
||||
// Updates an existing row in the datasource
|
||||
const updateRow = async row => {
|
||||
return await getAPI()?.actions.updateRow(row)
|
||||
}
|
||||
|
||||
// Deletes rows from the datasource
|
||||
const deleteRows = async rows => {
|
||||
return await getAPI()?.actions.deleteRows(rows)
|
||||
}
|
||||
|
||||
// Gets a single row from a datasource
|
||||
const getRow = async id => {
|
||||
return await getAPI()?.actions.getRow(id)
|
||||
}
|
||||
|
||||
// Checks if a certain datasource config is valid
|
||||
const isDatasourceValid = datasource => {
|
||||
return getAPI()?.actions.isDatasourceValid(datasource)
|
||||
}
|
||||
|
||||
// Checks if this datasource can use a specific column by name
|
||||
const canUseColumn = name => {
|
||||
return getAPI()?.actions.canUseColumn(name)
|
||||
}
|
||||
|
||||
return {
|
||||
datasource: {
|
||||
...datasource,
|
||||
actions: {
|
||||
refreshDefinition,
|
||||
saveDefinition,
|
||||
addRow,
|
||||
updateRow,
|
||||
deleteRows,
|
||||
getRow,
|
||||
isDatasourceValid,
|
||||
canUseColumn,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
|
||||
export const createStores = context => {
|
||||
const { props } = context
|
||||
|
||||
// Initialise to default props
|
||||
const filter = writable(props.initialFilter)
|
||||
const filter = writable(get(props).initialFilter)
|
||||
|
||||
return {
|
||||
filter,
|
||||
|
|
|
@ -15,16 +15,20 @@ import * as Config from "./config"
|
|||
import * as Sort from "./sort"
|
||||
import * as Filter from "./filter"
|
||||
import * as Notifications from "./notifications"
|
||||
import * as Table from "./table"
|
||||
import * as ViewV2 from "./viewV2"
|
||||
import * as Datasource from "./datasource"
|
||||
|
||||
const DependencyOrderedStores = [
|
||||
Config,
|
||||
Notifications,
|
||||
Sort,
|
||||
Filter,
|
||||
Bounds,
|
||||
Scroll,
|
||||
Rows,
|
||||
Table,
|
||||
ViewV2,
|
||||
Datasource,
|
||||
Columns,
|
||||
Rows,
|
||||
UI,
|
||||
Validation,
|
||||
Resize,
|
||||
|
@ -34,6 +38,8 @@ const DependencyOrderedStores = [
|
|||
Menu,
|
||||
Pagination,
|
||||
Clipboard,
|
||||
Config,
|
||||
Notifications,
|
||||
]
|
||||
|
||||
export const attachStores = context => {
|
||||
|
@ -47,6 +53,11 @@ export const attachStores = context => {
|
|||
context = { ...context, ...store.deriveStores?.(context) }
|
||||
}
|
||||
|
||||
// Action creation
|
||||
for (let store of DependencyOrderedStores) {
|
||||
context = { ...context, ...store.createActions?.(context) }
|
||||
}
|
||||
|
||||
// Initialise any store logic
|
||||
for (let store of DependencyOrderedStores) {
|
||||
store.initialise?.(context)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue