Merge remote-tracking branch 'origin/v3-ui' into feature/automation-branching-ux
This commit is contained in:
commit
3bd41db25c
|
@ -277,5 +277,5 @@ export const flags = new FlagSet({
|
|||
AUTOMATION_BRANCHING: Flag.boolean(env.isDev()),
|
||||
SQS: Flag.boolean(env.isDev()),
|
||||
[FeatureFlag.AI_CUSTOM_CONFIGS]: Flag.boolean(env.isDev()),
|
||||
[FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(false),
|
||||
[FeatureFlag.ENRICHED_RELATIONSHIPS]: Flag.boolean(env.isDev()),
|
||||
})
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import RoleCell from "./cells/RoleCell.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
import { canBeSortColumn } from "@budibase/frontend-core"
|
||||
|
||||
export let schema = {}
|
||||
export let data = []
|
||||
|
@ -31,7 +31,7 @@
|
|||
acc[key] =
|
||||
typeof schema[key] === "string" ? { type: schema[key] } : schema[key]
|
||||
|
||||
if (!canBeSortColumn(acc[key].type)) {
|
||||
if (!canBeSortColumn(acc[key])) {
|
||||
acc[key].sortable = false
|
||||
}
|
||||
return acc
|
||||
|
|
|
@ -121,8 +121,10 @@
|
|||
label: name,
|
||||
schema: {
|
||||
type: column.type,
|
||||
subtype: column.subtype,
|
||||
visible: column.visible,
|
||||
readonly: column.readonly,
|
||||
constraints: column.constraints, // This is needed to properly display "users" column
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -13,13 +13,15 @@
|
|||
import { isEnabled } from "helpers/featureFlags"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
const { columns, datasource } = getContext("grid")
|
||||
const { tableColumns, datasource } = getContext("grid")
|
||||
|
||||
let open = false
|
||||
let anchor
|
||||
|
||||
$: anyRestricted = $columns.filter(col => !col.visible || col.readonly).length
|
||||
$: text = anyRestricted ? `Columns: ${anyRestricted} restricted` : "Columns"
|
||||
$: anyRestricted = $tableColumns.filter(
|
||||
col => !col.visible || col.readonly
|
||||
).length
|
||||
$: text = anyRestricted ? `Columns: (${anyRestricted} restricted)` : "Columns"
|
||||
$: permissions =
|
||||
$datasource.type === "viewV2"
|
||||
? [
|
||||
|
@ -37,8 +39,8 @@
|
|||
size="M"
|
||||
on:click={() => (open = !open)}
|
||||
selected={open || anyRestricted}
|
||||
disabled={!$columns.length}
|
||||
accentColor="#674D00"
|
||||
disabled={!$tableColumns.length}
|
||||
>
|
||||
{text}
|
||||
</ActionButton>
|
||||
|
@ -46,7 +48,7 @@
|
|||
|
||||
<Popover bind:open {anchor} align="left">
|
||||
<ColumnsSettingContent
|
||||
columns={$columns}
|
||||
columns={$tableColumns}
|
||||
canSetRelationshipSchemas={isEnabled(FeatureFlag.ENRICHED_RELATIONSHIPS)}
|
||||
{permissions}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
import { canBeSortColumn } from "@budibase/frontend-core"
|
||||
|
||||
const { sort, columns } = getContext("grid")
|
||||
|
||||
|
@ -13,8 +13,9 @@
|
|||
label: col.label || col.name,
|
||||
value: col.name,
|
||||
type: col.schema?.type,
|
||||
related: col.related,
|
||||
}))
|
||||
.filter(col => canBeSortColumn(col.type))
|
||||
.filter(col => canBeSortColumn(col))
|
||||
$: orderOptions = getOrderOptions($sort.column, columnOptions)
|
||||
|
||||
const getOrderOptions = (column, columnOptions) => {
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
helpers,
|
||||
PROTECTED_INTERNAL_COLUMNS,
|
||||
PROTECTED_EXTERNAL_COLUMNS,
|
||||
canBeDisplayColumn,
|
||||
canHaveDefaultColumn,
|
||||
} from "@budibase/shared-core"
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte"
|
||||
|
@ -43,7 +42,7 @@
|
|||
SourceName,
|
||||
} from "@budibase/types"
|
||||
import RelationshipSelector from "components/common/RelationshipSelector.svelte"
|
||||
import { RowUtils } from "@budibase/frontend-core"
|
||||
import { RowUtils, canBeDisplayColumn } from "@budibase/frontend-core"
|
||||
import ServerBindingPanel from "components/common/bindings/ServerBindingPanel.svelte"
|
||||
import OptionsEditor from "./OptionsEditor.svelte"
|
||||
import { isEnabled } from "helpers/featureFlags"
|
||||
|
@ -166,7 +165,7 @@
|
|||
: availableAutoColumns
|
||||
// used to select what different options can be displayed for column type
|
||||
$: canBeDisplay =
|
||||
canBeDisplayColumn(editableColumn.type) && !editableColumn.autocolumn
|
||||
canBeDisplayColumn(editableColumn) && !editableColumn.autocolumn
|
||||
$: canHaveDefault =
|
||||
isEnabled("DEFAULT_VALUES") && canHaveDefaultColumn(editableColumn.type)
|
||||
$: canBeRequired =
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { Select, Icon } from "@budibase/bbui"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { canBeDisplayColumn, utils } from "@budibase/shared-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { canBeDisplayColumn } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { parseFile } from "./utils"
|
||||
|
||||
|
@ -100,10 +101,10 @@
|
|||
let rawRows = []
|
||||
|
||||
$: displayColumnOptions = Object.keys(schema || {}).filter(column => {
|
||||
return validation[column] && canBeDisplayColumn(schema[column].type)
|
||||
return validation[column] && canBeDisplayColumn(schema[column])
|
||||
})
|
||||
|
||||
$: if (displayColumn && !canBeDisplayColumn(schema[displayColumn].type)) {
|
||||
$: if (displayColumn && !canBeDisplayColumn(schema[displayColumn])) {
|
||||
displayColumn = null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { enrichSchemaWithRelColumns } from "@budibase/frontend-core"
|
||||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
import { selectedScreen, componentStore } from "stores/builder"
|
||||
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||
|
@ -28,7 +29,8 @@
|
|||
delete schema._rev
|
||||
}
|
||||
|
||||
return schema
|
||||
const result = enrichSchemaWithRelColumns(schema)
|
||||
return result
|
||||
}
|
||||
|
||||
$: datasource = getDatasourceForProvider($selectedScreen, componentInstance)
|
||||
|
|
|
@ -82,7 +82,7 @@ const toDraggableListFormat = (gridFormatColumns, createComponent, schema) => {
|
|||
active: column.active,
|
||||
field: column.field,
|
||||
label: column.label,
|
||||
columnType: schema[column.field].type,
|
||||
columnType: column.columnType || schema[column.field].type,
|
||||
width: column.width,
|
||||
conditions: column.conditions,
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
|
||||
import { selectedScreen } from "stores/builder"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
import { canBeSortColumn } from "@budibase/frontend-core"
|
||||
|
||||
export let componentInstance = {}
|
||||
export let value = ""
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
const getSortableFields = schema => {
|
||||
return Object.entries(schema || {})
|
||||
.filter(entry => canBeSortColumn(entry[1].type))
|
||||
.filter(entry => canBeSortColumn(entry[1]))
|
||||
.map(entry => entry[0])
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { viewsV2, rowActions } from "stores/builder"
|
||||
import { admin } from "stores/portal"
|
||||
import { admin, themeStore } from "stores/portal"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
|
@ -24,6 +24,9 @@
|
|||
$: buttons = makeRowActionButtons($rowActions[id])
|
||||
$: rowActions.refreshRowActions(id)
|
||||
|
||||
$: currentTheme = $themeStore?.theme
|
||||
$: darkMode = !currentTheme.includes("light")
|
||||
|
||||
const makeRowActionButtons = actions => {
|
||||
return (actions || []).map(action => ({
|
||||
text: action.name,
|
||||
|
@ -40,6 +43,7 @@
|
|||
|
||||
<Grid
|
||||
{API}
|
||||
{darkMode}
|
||||
{datasource}
|
||||
allowAddRows
|
||||
allowDeleteRows
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext, onDestroy } from "svelte"
|
||||
import { Table } from "@budibase/bbui"
|
||||
import SlotRenderer from "./SlotRenderer.svelte"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
import { canBeSortColumn } from "@budibase/frontend-core"
|
||||
import Provider from "components/context/Provider.svelte"
|
||||
|
||||
export let dataProvider
|
||||
|
@ -146,7 +146,7 @@
|
|||
return
|
||||
}
|
||||
newSchema[columnName] = schema[columnName]
|
||||
if (!canBeSortColumn(schema[columnName].type)) {
|
||||
if (!canBeSortColumn(schema[columnName])) {
|
||||
newSchema[columnName].sortable = false
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount, getContext } from "svelte"
|
||||
import { Dropzone } from "@budibase/bbui"
|
||||
import GridPopover from "../overlays/GridPopover.svelte"
|
||||
import { FieldType } from "@budibase/types"
|
||||
|
||||
export let value
|
||||
export let focused = false
|
||||
|
@ -81,7 +82,12 @@
|
|||
>
|
||||
{#each value || [] as attachment}
|
||||
{#if isImage(attachment.extension)}
|
||||
<img src={attachment.url} alt={attachment.extension} />
|
||||
<img
|
||||
class:light={!$props?.darkMode &&
|
||||
schema.type === FieldType.SIGNATURE_SINGLE}
|
||||
src={attachment.url}
|
||||
alt={attachment.extension}
|
||||
/>
|
||||
{:else}
|
||||
<div class="file" title={attachment.name}>
|
||||
{attachment.extension}
|
||||
|
@ -140,4 +146,9 @@
|
|||
width: 320px;
|
||||
padding: var(--cell-padding);
|
||||
}
|
||||
|
||||
.attachment-cell img.light {
|
||||
-webkit-filter: invert(100%);
|
||||
filter: invert(100%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { getContext, onMount, tick } from "svelte"
|
||||
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
|
||||
import { canBeSortColumn, canBeDisplayColumn } from "@budibase/frontend-core"
|
||||
import { Icon, Menu, MenuItem, Modal } from "@budibase/bbui"
|
||||
import GridCell from "./GridCell.svelte"
|
||||
import { getColumnIcon } from "../../../utils/schema"
|
||||
|
@ -165,7 +165,17 @@
|
|||
}
|
||||
|
||||
const hideColumn = () => {
|
||||
datasource.actions.addSchemaMutation(column.name, { visible: false })
|
||||
const { related } = column
|
||||
const mutation = { visible: false }
|
||||
if (!related) {
|
||||
datasource.actions.addSchemaMutation(column.name, mutation)
|
||||
} else {
|
||||
datasource.actions.addSubSchemaMutation(
|
||||
related.subField,
|
||||
related.field,
|
||||
mutation
|
||||
)
|
||||
}
|
||||
datasource.actions.saveSchemaMutations()
|
||||
open = false
|
||||
}
|
||||
|
@ -347,15 +357,14 @@
|
|||
<MenuItem
|
||||
icon="Label"
|
||||
on:click={makeDisplayColumn}
|
||||
disabled={column.primaryDisplay ||
|
||||
!canBeDisplayColumn(column.schema.type)}
|
||||
disabled={column.primaryDisplay || !canBeDisplayColumn(column.schema)}
|
||||
>
|
||||
Use as display column
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="SortOrderUp"
|
||||
on:click={sortAscending}
|
||||
disabled={!canBeSortColumn(column.schema.type) ||
|
||||
disabled={!canBeSortColumn(column.schema) ||
|
||||
(column.name === $sort.column && $sort.order === "ascending")}
|
||||
>
|
||||
Sort {sortingLabels.ascending}
|
||||
|
@ -363,7 +372,7 @@
|
|||
<MenuItem
|
||||
icon="SortOrderDown"
|
||||
on:click={sortDescending}
|
||||
disabled={!canBeSortColumn(column.schema.type) ||
|
||||
disabled={!canBeSortColumn(column.schema) ||
|
||||
(column.name === $sort.column && $sort.order === "descending")}
|
||||
>
|
||||
Sort {sortingLabels.descending}
|
||||
|
|
|
@ -35,5 +35,9 @@ const TypeComponentMap = {
|
|||
[FieldType.BB_REFERENCE_SINGLE]: BBReferenceSingleCell,
|
||||
}
|
||||
export const getCellRenderer = column => {
|
||||
return TypeComponentMap[column?.schema?.type] || TextCell
|
||||
return (
|
||||
TypeComponentMap[column?.schema?.cellRenderType] ||
|
||||
TypeComponentMap[column?.schema?.type] ||
|
||||
TextCell
|
||||
)
|
||||
}
|
||||
|
|
|
@ -42,6 +42,11 @@ export const deriveStores = context => {
|
|||
return map
|
||||
})
|
||||
|
||||
// Derived list of columns which are direct part of the table
|
||||
const tableColumns = derived(columns, $columns => {
|
||||
return $columns.filter(col => !col.related)
|
||||
})
|
||||
|
||||
// Derived list of columns which have not been explicitly hidden
|
||||
const visibleColumns = derived(columns, $columns => {
|
||||
return $columns.filter(col => col.visible)
|
||||
|
@ -64,6 +69,7 @@ export const deriveStores = context => {
|
|||
})
|
||||
|
||||
return {
|
||||
tableColumns,
|
||||
displayColumn,
|
||||
columnLookupMap,
|
||||
visibleColumns,
|
||||
|
@ -73,16 +79,24 @@ export const deriveStores = context => {
|
|||
}
|
||||
|
||||
export const createActions = context => {
|
||||
const { columns, datasource, schema } = context
|
||||
const { columns, datasource } = context
|
||||
|
||||
// Updates the width of all columns
|
||||
const changeAllColumnWidths = async width => {
|
||||
const $schema = get(schema)
|
||||
let mutations = {}
|
||||
Object.keys($schema).forEach(field => {
|
||||
mutations[field] = { width }
|
||||
const $columns = get(columns)
|
||||
$columns.forEach(column => {
|
||||
const { related } = column
|
||||
const mutation = { width }
|
||||
if (!related) {
|
||||
datasource.actions.addSchemaMutation(column.name, mutation)
|
||||
} else {
|
||||
datasource.actions.addSubSchemaMutation(
|
||||
related.subField,
|
||||
related.field,
|
||||
mutation
|
||||
)
|
||||
}
|
||||
})
|
||||
datasource.actions.addSchemaMutations(mutations)
|
||||
await datasource.actions.saveSchemaMutations()
|
||||
}
|
||||
|
||||
|
@ -136,7 +150,7 @@ export const initialise = context => {
|
|||
.map(field => {
|
||||
const fieldSchema = $enrichedSchema[field]
|
||||
const oldColumn = $columns?.find(col => col.name === field)
|
||||
let column = {
|
||||
const column = {
|
||||
name: field,
|
||||
label: fieldSchema.displayName || field,
|
||||
schema: fieldSchema,
|
||||
|
@ -145,6 +159,7 @@ export const initialise = context => {
|
|||
readonly: fieldSchema.readonly,
|
||||
order: fieldSchema.order ?? oldColumn?.order,
|
||||
conditions: fieldSchema.conditions,
|
||||
related: fieldSchema.related,
|
||||
}
|
||||
// Override a few properties for primary display
|
||||
if (field === primaryDisplay) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { derived, get } from "svelte/store"
|
||||
import { getDatasourceDefinition, getDatasourceSchema } from "../../../fetch"
|
||||
import { memo } from "../../../utils"
|
||||
import { enrichSchemaWithRelColumns, memo } from "../../../utils"
|
||||
|
||||
export const createStores = () => {
|
||||
const definition = memo(null)
|
||||
|
@ -53,10 +53,13 @@ export const deriveStores = context => {
|
|||
if (!$schema) {
|
||||
return null
|
||||
}
|
||||
let enrichedSchema = {}
|
||||
Object.keys($schema).forEach(field => {
|
||||
|
||||
const schemaWithRelatedColumns = enrichSchemaWithRelColumns($schema)
|
||||
|
||||
const enrichedSchema = {}
|
||||
Object.keys(schemaWithRelatedColumns).forEach(field => {
|
||||
enrichedSchema[field] = {
|
||||
...$schema[field],
|
||||
...schemaWithRelatedColumns[field],
|
||||
...$schemaOverrides?.[field],
|
||||
...$schemaMutations[field],
|
||||
}
|
||||
|
@ -202,24 +205,6 @@ export const createActions = context => {
|
|||
})
|
||||
}
|
||||
|
||||
// Adds schema mutations for multiple fields at once
|
||||
const addSchemaMutations = mutations => {
|
||||
const fields = Object.keys(mutations || {})
|
||||
if (!fields.length) {
|
||||
return
|
||||
}
|
||||
schemaMutations.update($schemaMutations => {
|
||||
let newSchemaMutations = { ...$schemaMutations }
|
||||
fields.forEach(field => {
|
||||
newSchemaMutations[field] = {
|
||||
...newSchemaMutations[field],
|
||||
...mutations[field],
|
||||
}
|
||||
})
|
||||
return newSchemaMutations
|
||||
})
|
||||
}
|
||||
|
||||
// Saves schema changes to the server, if possible
|
||||
const saveSchemaMutations = async () => {
|
||||
// If we can't save schema changes then we just want to keep this in memory
|
||||
|
@ -309,7 +294,6 @@ export const createActions = context => {
|
|||
changePrimaryDisplay,
|
||||
addSchemaMutation,
|
||||
addSubSchemaMutation,
|
||||
addSchemaMutations,
|
||||
saveSchemaMutations,
|
||||
resetSchemaMutations,
|
||||
},
|
||||
|
|
|
@ -133,16 +133,22 @@ export const initialise = context => {
|
|||
// When sorting changes, ensure view definition is kept up to date
|
||||
unsubscribers.push(
|
||||
sort.subscribe(async $sort => {
|
||||
// If we can mutate schema then update the view definition
|
||||
if (get(config).canSaveSchema) {
|
||||
// Ensure we're updating the correct view
|
||||
const $view = get(definition)
|
||||
if ($view?.id !== $datasource.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip if nothing actually changed
|
||||
if (
|
||||
$sort?.column !== $view.sort?.field ||
|
||||
$sort?.order !== $view.sort?.order
|
||||
$sort?.column === $view.sort?.field &&
|
||||
$sort?.order === $view.sort?.order
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we can mutate schema then update the view definition
|
||||
if (get(config).canSaveSchema) {
|
||||
await datasource.actions.saveDefinition({
|
||||
...$view,
|
||||
sort: {
|
||||
|
@ -151,7 +157,6 @@ export const initialise = context => {
|
|||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the fetch to ensure the new sort is respected.
|
||||
// Ensure we're updating the correct fetch.
|
||||
|
|
|
@ -214,11 +214,20 @@ export const createActions = context => {
|
|||
})
|
||||
|
||||
// Extract new orders as schema mutations
|
||||
let mutations = {}
|
||||
get(columns).forEach((column, idx) => {
|
||||
mutations[column.name] = { order: idx }
|
||||
const { related } = column
|
||||
const mutation = { order: idx }
|
||||
if (!related) {
|
||||
datasource.actions.addSchemaMutation(column.name, mutation)
|
||||
} else {
|
||||
datasource.actions.addSubSchemaMutation(
|
||||
related.subField,
|
||||
related.field,
|
||||
mutation
|
||||
)
|
||||
}
|
||||
})
|
||||
datasource.actions.addSchemaMutations(mutations)
|
||||
|
||||
await datasource.actions.saveSchemaMutations()
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export const createActions = context => {
|
|||
initialWidth: column.width,
|
||||
initialMouseX: x,
|
||||
column: column.name,
|
||||
related: column.related,
|
||||
})
|
||||
|
||||
// Add mouse event listeners to handle resizing
|
||||
|
@ -50,7 +51,7 @@ export const createActions = context => {
|
|||
|
||||
// Handler for moving the mouse to resize columns
|
||||
const onResizeMouseMove = e => {
|
||||
const { initialMouseX, initialWidth, width, column } = get(resize)
|
||||
const { initialMouseX, initialWidth, width, column, related } = get(resize)
|
||||
const { x } = parseEventLocation(e)
|
||||
const dx = x - initialMouseX
|
||||
const newWidth = Math.round(Math.max(MinColumnWidth, initialWidth + dx))
|
||||
|
@ -61,7 +62,13 @@ export const createActions = context => {
|
|||
}
|
||||
|
||||
// Update column state
|
||||
if (!related) {
|
||||
datasource.actions.addSchemaMutation(column, { width })
|
||||
} else {
|
||||
datasource.actions.addSubSchemaMutation(related.subField, related.field, {
|
||||
width,
|
||||
})
|
||||
}
|
||||
|
||||
// Update state
|
||||
resize.update(state => ({
|
||||
|
|
|
@ -6,6 +6,7 @@ import { tick } from "svelte"
|
|||
import { Helpers } from "@budibase/bbui"
|
||||
import { sleep } from "../../../utils/utils"
|
||||
import { FieldType } from "@budibase/types"
|
||||
import { getRelatedTableValues } from "../../../utils"
|
||||
|
||||
export const createStores = () => {
|
||||
const rows = writable([])
|
||||
|
@ -42,15 +43,26 @@ export const createStores = () => {
|
|||
}
|
||||
|
||||
export const deriveStores = context => {
|
||||
const { rows } = context
|
||||
const { rows, enrichedSchema } = context
|
||||
|
||||
// Enrich rows with an index property and any pending changes
|
||||
const enrichedRows = derived(rows, $rows => {
|
||||
const enrichedRows = derived(
|
||||
[rows, enrichedSchema],
|
||||
([$rows, $enrichedSchema]) => {
|
||||
const customColumns = Object.values($enrichedSchema || {}).filter(
|
||||
f => f.related
|
||||
)
|
||||
return $rows.map((row, idx) => ({
|
||||
...row,
|
||||
__idx: idx,
|
||||
...customColumns.reduce((map, column) => {
|
||||
const fromField = $enrichedSchema[column.related.field]
|
||||
map[column.name] = getRelatedTableValues(row, column, fromField)
|
||||
return map
|
||||
}, {}),
|
||||
}))
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Generate a lookup map to quick find a row by ID
|
||||
const rowLookupMap = derived(enrichedRows, $enrichedRows => {
|
||||
|
|
|
@ -11,3 +11,5 @@ export { createWebsocket } from "./websocket"
|
|||
export * from "./download"
|
||||
export * from "./theme"
|
||||
export * from "./settings"
|
||||
export * from "./relatedColumns"
|
||||
export * from "./table"
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import { FieldType, RelationshipType } from "@budibase/types"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
const columnTypeManyTypeOverrides = {
|
||||
[FieldType.DATETIME]: FieldType.STRING,
|
||||
[FieldType.BOOLEAN]: FieldType.STRING,
|
||||
[FieldType.SIGNATURE_SINGLE]: FieldType.ATTACHMENTS,
|
||||
}
|
||||
|
||||
const columnTypeManyParser = {
|
||||
[FieldType.DATETIME]: (value, field) => {
|
||||
function parseDate(value) {
|
||||
const { timeOnly, dateOnly, ignoreTimezones } = field || {}
|
||||
const enableTime = !dateOnly
|
||||
const parsedValue = Helpers.parseDate(value, {
|
||||
timeOnly,
|
||||
enableTime,
|
||||
ignoreTimezones,
|
||||
})
|
||||
const parsed = Helpers.getDateDisplayValue(parsedValue, {
|
||||
enableTime,
|
||||
timeOnly,
|
||||
})
|
||||
return parsed
|
||||
}
|
||||
|
||||
return value?.map(v => parseDate(v))
|
||||
},
|
||||
[FieldType.BOOLEAN]: value => value?.map(v => !!v),
|
||||
[FieldType.BB_REFERENCE_SINGLE]: value => [
|
||||
...new Map(value.map(i => [i._id, i])).values(),
|
||||
],
|
||||
[FieldType.BB_REFERENCE]: value => [
|
||||
...new Map(value.map(i => [i._id, i])).values(),
|
||||
],
|
||||
[FieldType.ARRAY]: value => Array.from(new Set(value)),
|
||||
}
|
||||
|
||||
export function enrichSchemaWithRelColumns(schema) {
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
const result = Object.keys(schema).reduce((result, fieldName) => {
|
||||
const field = schema[fieldName]
|
||||
result[fieldName] = field
|
||||
|
||||
if (field.visible !== false && field.columns) {
|
||||
const fromSingle =
|
||||
field?.relationshipType === RelationshipType.ONE_TO_MANY
|
||||
|
||||
for (const relColumn of Object.keys(field.columns)) {
|
||||
const relField = field.columns[relColumn]
|
||||
if (!relField.visible) {
|
||||
continue
|
||||
}
|
||||
const name = `${field.name}.${relColumn}`
|
||||
result[name] = {
|
||||
...relField,
|
||||
name,
|
||||
related: { field: fieldName, subField: relColumn },
|
||||
cellRenderType:
|
||||
(!fromSingle && columnTypeManyTypeOverrides[relField.type]) ||
|
||||
relField.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getRelatedTableValues(row, field, fromField) {
|
||||
const fromSingle =
|
||||
fromField?.relationshipType === RelationshipType.ONE_TO_MANY
|
||||
|
||||
let result = ""
|
||||
|
||||
if (fromSingle) {
|
||||
result = row[field.related.field]?.[0]?.[field.related.subField]
|
||||
} else {
|
||||
const parser = columnTypeManyParser[field.type] || (value => value)
|
||||
|
||||
result = parser(
|
||||
row[field.related.field]
|
||||
?.flatMap(r => r[field.related.subField])
|
||||
?.filter(i => i !== undefined && i !== null),
|
||||
field
|
||||
)
|
||||
|
||||
if (
|
||||
[
|
||||
FieldType.STRING,
|
||||
FieldType.NUMBER,
|
||||
FieldType.BIGINT,
|
||||
FieldType.BOOLEAN,
|
||||
FieldType.DATETIME,
|
||||
FieldType.LONGFORM,
|
||||
FieldType.BARCODEQR,
|
||||
].includes(field.type)
|
||||
) {
|
||||
result = result?.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import * as sharedCore from "@budibase/shared-core"
|
||||
|
||||
export function canBeDisplayColumn(column) {
|
||||
if (!sharedCore.canBeDisplayColumn(column.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (column.related) {
|
||||
// If it's a related column (only available in the frontend), don't allow using it as display column
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function canBeSortColumn(column) {
|
||||
if (!sharedCore.canBeSortColumn(column.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (column.related) {
|
||||
// If it's a related column (only available in the frontend), don't allow using it as display column
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
import { permissions, roles, context } from "@budibase/backend-core"
|
||||
import {
|
||||
UserCtx,
|
||||
Database,
|
||||
Role,
|
||||
PermissionLevel,
|
||||
GetResourcePermsResponse,
|
||||
ResourcePermissionInfo,
|
||||
GetDependantResourcesResponse,
|
||||
|
@ -12,107 +10,15 @@ import {
|
|||
RemovePermissionRequest,
|
||||
RemovePermissionResponse,
|
||||
} from "@budibase/types"
|
||||
import { getRoleParams } from "../../db/utils"
|
||||
import {
|
||||
CURRENTLY_SUPPORTED_LEVELS,
|
||||
getBasePermissions,
|
||||
} from "../../utilities/security"
|
||||
import { removeFromArray } from "../../utilities"
|
||||
import sdk from "../../sdk"
|
||||
|
||||
const enum PermissionUpdateType {
|
||||
REMOVE = "remove",
|
||||
ADD = "add",
|
||||
}
|
||||
import { PermissionUpdateType } from "../../sdk/app/permissions"
|
||||
|
||||
const SUPPORTED_LEVELS = CURRENTLY_SUPPORTED_LEVELS
|
||||
|
||||
// utility function to stop this repetition - permissions always stored under roles
|
||||
async function getAllDBRoles(db: Database) {
|
||||
const body = await db.allDocs<Role>(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
return body.rows.map(row => row.doc!)
|
||||
}
|
||||
|
||||
async function updatePermissionOnRole(
|
||||
{
|
||||
roleId,
|
||||
resourceId,
|
||||
level,
|
||||
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
||||
updateType: PermissionUpdateType
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const remove = updateType === PermissionUpdateType.REMOVE
|
||||
const isABuiltin = roles.isBuiltin(roleId)
|
||||
const dbRoleId = roles.getDBRoleID(roleId)
|
||||
const dbRoles = await getAllDBRoles(db)
|
||||
const docUpdates: Role[] = []
|
||||
|
||||
// the permission is for a built in, make sure it exists
|
||||
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
|
||||
const builtin = roles.getBuiltinRoles()[roleId]
|
||||
builtin._id = roles.getDBRoleID(builtin._id!)
|
||||
dbRoles.push(builtin)
|
||||
}
|
||||
|
||||
// now try to find any roles which need updated, e.g. removing the
|
||||
// resource from another role and then adding to the new role
|
||||
for (let role of dbRoles) {
|
||||
let updated = false
|
||||
const rolePermissions: Record<string, PermissionLevel[]> = role.permissions
|
||||
? role.permissions
|
||||
: {}
|
||||
// make sure its an array, also handle migrating
|
||||
if (
|
||||
!rolePermissions[resourceId] ||
|
||||
!Array.isArray(rolePermissions[resourceId])
|
||||
) {
|
||||
rolePermissions[resourceId] =
|
||||
typeof rolePermissions[resourceId] === "string"
|
||||
? [rolePermissions[resourceId] as unknown as PermissionLevel]
|
||||
: []
|
||||
}
|
||||
// handle the removal/updating the role which has this permission first
|
||||
// the updating (role._id !== dbRoleId) is required because a resource/level can
|
||||
// only be permitted in a single role (this reduces hierarchy confusion and simplifies
|
||||
// the general UI for this, rather than needing to show everywhere it is used)
|
||||
if (
|
||||
(role._id !== dbRoleId || remove) &&
|
||||
rolePermissions[resourceId].indexOf(level) !== -1
|
||||
) {
|
||||
removeFromArray(rolePermissions[resourceId], level)
|
||||
updated = true
|
||||
}
|
||||
// handle the adding, we're on the correct role, at it to this
|
||||
if (!remove && role._id === dbRoleId) {
|
||||
const set = new Set(rolePermissions[resourceId])
|
||||
rolePermissions[resourceId] = [...set.add(level)]
|
||||
updated = true
|
||||
}
|
||||
// handle the update, add it to bulk docs to perform at end
|
||||
if (updated) {
|
||||
role.permissions = rolePermissions
|
||||
docUpdates.push(role)
|
||||
}
|
||||
}
|
||||
|
||||
const response = await db.bulkDocs(docUpdates)
|
||||
return response.map(resp => {
|
||||
const version = docUpdates.find(role => role._id === resp.id)?.version
|
||||
const _id = roles.getExternalRoleID(resp.id, version)
|
||||
return {
|
||||
_id,
|
||||
rev: resp.rev,
|
||||
error: resp.error,
|
||||
reason: resp.reason,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchBuiltin(ctx: UserCtx) {
|
||||
ctx.body = Object.values(permissions.getBuiltinPermissions())
|
||||
}
|
||||
|
@ -124,7 +30,7 @@ export function fetchLevels(ctx: UserCtx) {
|
|||
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
const dbRoles: Role[] = await getAllDBRoles(db)
|
||||
const dbRoles: Role[] = await sdk.permissions.getAllDBRoles(db)
|
||||
let permissions: any = {}
|
||||
// create an object with structure role ID -> resource ID -> level
|
||||
for (let role of dbRoles) {
|
||||
|
@ -186,12 +92,18 @@ export async function getDependantResources(
|
|||
|
||||
export async function addPermission(ctx: UserCtx<void, AddPermissionResponse>) {
|
||||
const params: AddPermissionRequest = ctx.params
|
||||
ctx.body = await updatePermissionOnRole(params, PermissionUpdateType.ADD)
|
||||
ctx.body = await sdk.permissions.updatePermissionOnRole(
|
||||
params,
|
||||
PermissionUpdateType.ADD
|
||||
)
|
||||
}
|
||||
|
||||
export async function removePermission(
|
||||
ctx: UserCtx<void, RemovePermissionResponse>
|
||||
) {
|
||||
const params: RemovePermissionRequest = ctx.params
|
||||
ctx.body = await updatePermissionOnRole(params, PermissionUpdateType.REMOVE)
|
||||
ctx.body = await sdk.permissions.updatePermissionOnRole(
|
||||
params,
|
||||
PermissionUpdateType.REMOVE
|
||||
)
|
||||
}
|
||||
|
|
|
@ -125,6 +125,12 @@ describe("/permission", () => {
|
|||
})
|
||||
|
||||
it("should be able to access the view data when the table is set to public and with no view permissions overrides", async () => {
|
||||
// Make view inherit table permissions. Needed for backwards compatibility with existing views.
|
||||
await config.api.permission.revoke({
|
||||
roleId: STD_ROLE_ID,
|
||||
resourceId: view.id,
|
||||
level: PermissionLevel.READ,
|
||||
})
|
||||
// replicate changes before checking permissions
|
||||
await config.publish()
|
||||
|
||||
|
@ -138,6 +144,12 @@ describe("/permission", () => {
|
|||
resourceId: table._id,
|
||||
level: PermissionLevel.READ,
|
||||
})
|
||||
// Make view inherit table permissions. Needed for backwards compatibility with existing views.
|
||||
await config.api.permission.revoke({
|
||||
roleId: STD_ROLE_ID,
|
||||
resourceId: view.id,
|
||||
level: PermissionLevel.READ,
|
||||
})
|
||||
// replicate changes before checking permissions
|
||||
await config.publish()
|
||||
|
||||
|
|
|
@ -2684,7 +2684,7 @@ describe.each([
|
|||
async (__, retrieveDelegate) => {
|
||||
await withCoreEnv(
|
||||
{
|
||||
TENANT_FEATURE_FLAGS: ``,
|
||||
TENANT_FEATURE_FLAGS: `*:!${FeatureFlag.ENRICHED_RELATIONSHIPS}`,
|
||||
},
|
||||
async () => {
|
||||
const otherRows = _.sampleSize(auxData, 5)
|
||||
|
|
|
@ -826,11 +826,20 @@ describe("/rowsActions", () => {
|
|||
)
|
||||
).id
|
||||
|
||||
// Allow row action on view
|
||||
await config.api.rowAction.setViewPermission(
|
||||
tableId,
|
||||
viewId,
|
||||
rowAction.id
|
||||
)
|
||||
|
||||
// Delete explicit view permissions so they inherit table permissions
|
||||
await config.api.permission.revoke({
|
||||
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, // Don't think this matters since we are revoking the permission
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: viewId,
|
||||
})
|
||||
|
||||
return { permissionResource: tableId, triggerResouce: viewId }
|
||||
},
|
||||
],
|
||||
|
|
|
@ -2417,6 +2417,11 @@ describe.each([
|
|||
level: PermissionLevel.READ,
|
||||
resourceId: table._id!,
|
||||
})
|
||||
await config.api.permission.revoke({
|
||||
roleId: roles.BUILTIN_ROLE_IDS.PUBLIC, // Don't think this matters since we are revoking the permission
|
||||
level: PermissionLevel.READ,
|
||||
resourceId: view.id,
|
||||
})
|
||||
await config.publish()
|
||||
|
||||
const response = await config.api.viewV2.publicSearch(view.id)
|
||||
|
|
|
@ -10,7 +10,10 @@ import flatten from "lodash/flatten"
|
|||
import { USER_METDATA_PREFIX } from "../utils"
|
||||
import partition from "lodash/partition"
|
||||
import { getGlobalUsersFromMetadata } from "../../utilities/global"
|
||||
import { outputProcessing, processFormulas } from "../../utilities/rowProcessor"
|
||||
import {
|
||||
coreOutputProcessing,
|
||||
processFormulas,
|
||||
} from "../../utilities/rowProcessor"
|
||||
import { context, features } from "@budibase/backend-core"
|
||||
import {
|
||||
ContextUser,
|
||||
|
@ -156,9 +159,6 @@ export async function updateLinks(args: {
|
|||
/**
|
||||
* Given a table and a list of rows this will retrieve all of the attached docs and enrich them into the row.
|
||||
* This is required for formula fields, this may only be utilised internally (for now).
|
||||
* @param table The table from which the rows originated.
|
||||
* @param rows The rows which are to be enriched.
|
||||
* @param opts optional - options like passing in a base row to use for enrichment.
|
||||
* @return returns the rows with all of the enriched relationships on it.
|
||||
*/
|
||||
export async function attachFullLinkedDocs(
|
||||
|
@ -248,9 +248,6 @@ export type SquashTableFields = Record<string, { visibleFieldNames: string[] }>
|
|||
|
||||
/**
|
||||
* This function will take the given enriched rows and squash the links to only contain the primary display field.
|
||||
* @param table The table from which the rows originated.
|
||||
* @param enriched The pre-enriched rows (full docs) which are to be squashed.
|
||||
* @param squashFields Per link column (key) define which columns are allowed while squashing.
|
||||
* @returns The rows after having their links squashed to only contain the ID and primary display.
|
||||
*/
|
||||
export async function squashLinks<T = Row[] | Row>(
|
||||
|
@ -283,21 +280,18 @@ export async function squashLinks<T = Row[] | Row>(
|
|||
if (schema.type !== FieldType.LINK || !Array.isArray(row[column])) {
|
||||
continue
|
||||
}
|
||||
const newLinks = []
|
||||
for (const link of row[column]) {
|
||||
const linkTblId =
|
||||
link.tableId || getRelatedTableForField(table.schema, column)
|
||||
const linkedTable = await getLinkedTable(linkTblId!, linkedTables)
|
||||
const relatedTable = await getLinkedTable(schema.tableId, linkedTables)
|
||||
if (viewSchema[column]?.columns) {
|
||||
row[column] = await coreOutputProcessing(relatedTable, row[column])
|
||||
}
|
||||
row[column] = row[column].map((link: Row) => {
|
||||
const obj: any = { _id: link._id }
|
||||
obj.primaryDisplay = getPrimaryDisplayValue(link, linkedTable)
|
||||
obj.primaryDisplay = getPrimaryDisplayValue(link, relatedTable)
|
||||
|
||||
if (viewSchema[column]?.columns) {
|
||||
const enrichedLink = await outputProcessing(linkedTable, link, {
|
||||
squash: false,
|
||||
})
|
||||
const squashFields = Object.entries(viewSchema[column].columns)
|
||||
const squashFields = Object.entries(viewSchema[column].columns || {})
|
||||
.filter(([columnName, viewColumnConfig]) => {
|
||||
const tableColumn = linkedTable.schema[columnName]
|
||||
const tableColumn = relatedTable.schema[columnName]
|
||||
if (!tableColumn) {
|
||||
return false
|
||||
}
|
||||
|
@ -315,13 +309,14 @@ export async function squashLinks<T = Row[] | Row>(
|
|||
.map(([columnName]) => columnName)
|
||||
|
||||
for (const relField of squashFields) {
|
||||
obj[relField] = enrichedLink[relField]
|
||||
if (link[relField] != null) {
|
||||
obj[relField] = link[relField]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newLinks.push(obj)
|
||||
}
|
||||
row[column] = newLinks
|
||||
return obj
|
||||
})
|
||||
}
|
||||
}
|
||||
return (isArray ? enrichedArray : enrichedArray[0]) as T
|
||||
|
|
|
@ -1,22 +1,34 @@
|
|||
import { db, roles } from "@budibase/backend-core"
|
||||
import { db, roles, context } from "@budibase/backend-core"
|
||||
import {
|
||||
PermissionLevel,
|
||||
PermissionSource,
|
||||
VirtualDocumentType,
|
||||
Role,
|
||||
Database,
|
||||
} from "@budibase/types"
|
||||
import { extractViewInfoFromID, isViewID } from "../../../db/utils"
|
||||
import {
|
||||
extractViewInfoFromID,
|
||||
isViewID,
|
||||
getRoleParams,
|
||||
} from "../../../db/utils"
|
||||
import {
|
||||
CURRENTLY_SUPPORTED_LEVELS,
|
||||
getBasePermissions,
|
||||
} from "../../../utilities/security"
|
||||
import sdk from "../../../sdk"
|
||||
import { isV2 } from "../views"
|
||||
import { removeFromArray } from "../../../utilities"
|
||||
|
||||
type ResourcePermissions = Record<
|
||||
string,
|
||||
{ role: string; type: PermissionSource }
|
||||
>
|
||||
|
||||
export const enum PermissionUpdateType {
|
||||
REMOVE = "remove",
|
||||
ADD = "add",
|
||||
}
|
||||
|
||||
export async function getInheritablePermissions(
|
||||
resourceId: string
|
||||
): Promise<ResourcePermissions | undefined> {
|
||||
|
@ -100,3 +112,89 @@ export async function getDependantResources(
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
export async function updatePermissionOnRole(
|
||||
{
|
||||
roleId,
|
||||
resourceId,
|
||||
level,
|
||||
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
||||
updateType: PermissionUpdateType
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const remove = updateType === PermissionUpdateType.REMOVE
|
||||
const isABuiltin = roles.isBuiltin(roleId)
|
||||
const dbRoleId = roles.getDBRoleID(roleId)
|
||||
const dbRoles = await getAllDBRoles(db)
|
||||
const docUpdates: Role[] = []
|
||||
|
||||
// the permission is for a built in, make sure it exists
|
||||
if (isABuiltin && !dbRoles.some(role => role._id === dbRoleId)) {
|
||||
const builtin = roles.getBuiltinRoles()[roleId]
|
||||
builtin._id = roles.getDBRoleID(builtin._id!)
|
||||
dbRoles.push(builtin)
|
||||
}
|
||||
|
||||
// now try to find any roles which need updated, e.g. removing the
|
||||
// resource from another role and then adding to the new role
|
||||
for (let role of dbRoles) {
|
||||
let updated = false
|
||||
const rolePermissions: Record<string, PermissionLevel[]> = role.permissions
|
||||
? role.permissions
|
||||
: {}
|
||||
// make sure its an array, also handle migrating
|
||||
if (
|
||||
!rolePermissions[resourceId] ||
|
||||
!Array.isArray(rolePermissions[resourceId])
|
||||
) {
|
||||
rolePermissions[resourceId] =
|
||||
typeof rolePermissions[resourceId] === "string"
|
||||
? [rolePermissions[resourceId] as unknown as PermissionLevel]
|
||||
: []
|
||||
}
|
||||
// handle the removal/updating the role which has this permission first
|
||||
// the updating (role._id !== dbRoleId) is required because a resource/level can
|
||||
// only be permitted in a single role (this reduces hierarchy confusion and simplifies
|
||||
// the general UI for this, rather than needing to show everywhere it is used)
|
||||
if (
|
||||
(role._id !== dbRoleId || remove) &&
|
||||
rolePermissions[resourceId].indexOf(level) !== -1
|
||||
) {
|
||||
removeFromArray(rolePermissions[resourceId], level)
|
||||
updated = true
|
||||
}
|
||||
// handle the adding, we're on the correct role, at it to this
|
||||
if (!remove && role._id === dbRoleId) {
|
||||
const set = new Set(rolePermissions[resourceId])
|
||||
rolePermissions[resourceId] = [...set.add(level)]
|
||||
updated = true
|
||||
}
|
||||
// handle the update, add it to bulk docs to perform at end
|
||||
if (updated) {
|
||||
role.permissions = rolePermissions
|
||||
docUpdates.push(role)
|
||||
}
|
||||
}
|
||||
|
||||
const response = await db.bulkDocs(docUpdates)
|
||||
return response.map(resp => {
|
||||
const version = docUpdates.find(role => role._id === resp.id)?.version
|
||||
const _id = roles.getExternalRoleID(resp.id, version)
|
||||
return {
|
||||
_id,
|
||||
rev: resp.rev,
|
||||
error: resp.error,
|
||||
reason: resp.reason,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// utility function to stop this repetition - permissions always stored under roles
|
||||
export async function getAllDBRoles(db: Database) {
|
||||
const body = await db.allDocs<Role>(
|
||||
getRoleParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
return body.rows.map(row => row.doc!)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
FieldType,
|
||||
PermissionLevel,
|
||||
RelationSchemaField,
|
||||
RenameColumn,
|
||||
Table,
|
||||
|
@ -9,19 +10,18 @@ import {
|
|||
ViewV2ColumnEnriched,
|
||||
ViewV2Enriched,
|
||||
} from "@budibase/types"
|
||||
import { HTTPError } from "@budibase/backend-core"
|
||||
import { HTTPError, roles } from "@budibase/backend-core"
|
||||
import {
|
||||
helpers,
|
||||
PROTECTED_EXTERNAL_COLUMNS,
|
||||
PROTECTED_INTERNAL_COLUMNS,
|
||||
} from "@budibase/shared-core"
|
||||
|
||||
import * as utils from "../../../db/utils"
|
||||
import { isExternalTableID } from "../../../integrations/utils"
|
||||
|
||||
import * as internal from "./internal"
|
||||
import * as external from "./external"
|
||||
import sdk from "../../../sdk"
|
||||
import { updatePermissionOnRole, PermissionUpdateType } from "../permissions"
|
||||
|
||||
function pickApi(tableId: any) {
|
||||
if (isExternalTableID(tableId)) {
|
||||
|
@ -114,8 +114,30 @@ export async function create(
|
|||
viewRequest: Omit<ViewV2, "id" | "version">
|
||||
): Promise<ViewV2> {
|
||||
await guardViewSchema(tableId, viewRequest)
|
||||
const view = await pickApi(tableId).create(tableId, viewRequest)
|
||||
|
||||
return pickApi(tableId).create(tableId, viewRequest)
|
||||
// Set permissions to be the same as the table
|
||||
const tablePerms = await sdk.permissions.getResourcePerms(tableId)
|
||||
const readRole = tablePerms[PermissionLevel.READ]?.role
|
||||
const writeRole = tablePerms[PermissionLevel.WRITE]?.role
|
||||
await updatePermissionOnRole(
|
||||
{
|
||||
roleId: readRole || roles.BUILTIN_ROLE_IDS.BASIC,
|
||||
resourceId: view.id,
|
||||
level: PermissionLevel.READ,
|
||||
},
|
||||
PermissionUpdateType.ADD
|
||||
)
|
||||
await updatePermissionOnRole(
|
||||
{
|
||||
roleId: writeRole || roles.BUILTIN_ROLE_IDS.BASIC,
|
||||
resourceId: view.id,
|
||||
level: PermissionLevel.WRITE,
|
||||
},
|
||||
PermissionUpdateType.ADD
|
||||
)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
export async function update(tableId: string, view: ViewV2): Promise<ViewV2> {
|
||||
|
|
|
@ -264,6 +264,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
} else {
|
||||
safeRows = rows
|
||||
}
|
||||
// SQS returns the rows with full relationship contents
|
||||
// attach any linked row information
|
||||
let enriched = !opts.preserveLinks
|
||||
? await linkRows.attachFullLinkedDocs(table.schema, safeRows, {
|
||||
|
@ -271,11 +272,39 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
})
|
||||
: safeRows
|
||||
|
||||
// make sure squash is enabled if needed
|
||||
if (!opts.squash && utils.hasCircularStructure(rows)) {
|
||||
opts.squash = true
|
||||
}
|
||||
|
||||
enriched = await coreOutputProcessing(table, enriched, opts)
|
||||
|
||||
if (opts.squash) {
|
||||
enriched = await linkRows.squashLinks(table, enriched, {
|
||||
fromViewId: opts?.fromViewId,
|
||||
})
|
||||
}
|
||||
|
||||
return (wasArray ? enriched : enriched[0]) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is similar to the outputProcessing function above, it makes sure that all the provided
|
||||
* rows are ready for output, but does not have enrichment for squash capabilities which can cause performance issues.
|
||||
* outputProcessing should be used when responding from the API, while this should be used when internally processing
|
||||
* rows for any reason (like part of view operations).
|
||||
*/
|
||||
export async function coreOutputProcessing(
|
||||
table: Table,
|
||||
rows: Row[],
|
||||
opts: {
|
||||
preserveLinks?: boolean
|
||||
skipBBReferences?: boolean
|
||||
fromViewId?: string
|
||||
} = {
|
||||
preserveLinks: false,
|
||||
skipBBReferences: false,
|
||||
}
|
||||
): Promise<Row[]> {
|
||||
// process complex types: attachments, bb references...
|
||||
for (const [property, column] of Object.entries(table.schema)) {
|
||||
if (
|
||||
|
@ -283,7 +312,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
column.type === FieldType.ATTACHMENT_SINGLE ||
|
||||
column.type === FieldType.SIGNATURE_SINGLE
|
||||
) {
|
||||
for (const row of enriched) {
|
||||
for (const row of rows) {
|
||||
if (row[property] == null) {
|
||||
continue
|
||||
}
|
||||
|
@ -308,7 +337,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
!opts.skipBBReferences &&
|
||||
column.type == FieldType.BB_REFERENCE
|
||||
) {
|
||||
for (const row of enriched) {
|
||||
for (const row of rows) {
|
||||
row[property] = await processOutputBBReferences(
|
||||
row[property],
|
||||
column.subtype
|
||||
|
@ -318,14 +347,14 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
!opts.skipBBReferences &&
|
||||
column.type == FieldType.BB_REFERENCE_SINGLE
|
||||
) {
|
||||
for (const row of enriched) {
|
||||
for (const row of rows) {
|
||||
row[property] = await processOutputBBReference(
|
||||
row[property],
|
||||
column.subtype
|
||||
)
|
||||
}
|
||||
} else if (column.type === FieldType.DATETIME && column.timeOnly) {
|
||||
for (const row of enriched) {
|
||||
for (const row of rows) {
|
||||
if (row[property] instanceof Date) {
|
||||
const hours = row[property].getUTCHours().toString().padStart(2, "0")
|
||||
const minutes = row[property]
|
||||
|
@ -340,7 +369,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
}
|
||||
}
|
||||
} else if (column.type === FieldType.LINK) {
|
||||
for (let row of enriched) {
|
||||
for (let row of rows) {
|
||||
// if relationship is empty - remove the array, this has been part of the API for some time
|
||||
if (Array.isArray(row[property]) && row[property].length === 0) {
|
||||
delete row[property]
|
||||
|
@ -350,17 +379,12 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
}
|
||||
|
||||
// process formulas after the complex types had been processed
|
||||
enriched = await processFormulas(table, enriched, { dynamic: true })
|
||||
rows = await processFormulas(table, rows, { dynamic: true })
|
||||
|
||||
if (opts.squash) {
|
||||
enriched = await linkRows.squashLinks(table, enriched, {
|
||||
fromViewId: opts?.fromViewId,
|
||||
})
|
||||
}
|
||||
// remove null properties to match internal API
|
||||
const isExternal = isExternalTableID(table._id!)
|
||||
if (isExternal || (await features.flags.isEnabled("SQS"))) {
|
||||
for (const row of enriched) {
|
||||
for (const row of rows) {
|
||||
for (const key of Object.keys(row)) {
|
||||
if (row[key] === null) {
|
||||
delete row[key]
|
||||
|
@ -388,7 +412,7 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
const fields = [...tableFields, ...protectedColumns].map(f =>
|
||||
f.toLowerCase()
|
||||
)
|
||||
for (const row of enriched) {
|
||||
for (const row of rows) {
|
||||
for (const key of Object.keys(row)) {
|
||||
if (!fields.includes(key.toLowerCase())) {
|
||||
delete row[key]
|
||||
|
@ -397,5 +421,5 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
}
|
||||
}
|
||||
|
||||
return (wasArray ? enriched : enriched[0]) as T
|
||||
return rows
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue