mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-03-25 17:05:45 +00:00
Improve data explorer perfs
This commit is contained in:
parent
cffd1e0117
commit
57ace0b584
16 changed files with 418 additions and 409 deletions
changelog/entries/unreleased/feature
web-frontend/modules
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "feature",
|
||||||
|
"message": "[Builder] Improve data explorer performances and allow to select any repetition from a source",
|
||||||
|
"issue_number": 2762,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2024-07-24"
|
||||||
|
}
|
|
@ -515,24 +515,10 @@ export class FormDataProviderType extends DataProviderType {
|
||||||
)
|
)
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
accessibleFormElements.map((element) => {
|
accessibleFormElements.map((element) => {
|
||||||
let formEntry = {}
|
|
||||||
if (recordIndexPath !== undefined) {
|
|
||||||
const uniqueElementId = this.app.$registry
|
const uniqueElementId = this.app.$registry
|
||||||
.get('element', element.type)
|
.get('element', element.type)
|
||||||
.uniqueElementId(element, recordIndexPath)
|
.uniqueElementId(element, recordIndexPath)
|
||||||
formEntry = getValueAtPath(formData, uniqueElementId)
|
const formEntry = getValueAtPath(formData, uniqueElementId)
|
||||||
} else {
|
|
||||||
// When `getDataContent` is called by `getNodes`, we won't have
|
|
||||||
// access to `recordIndexPath`, so we need to find the first *array*
|
|
||||||
// in the form data that corresponds to the element.
|
|
||||||
function _findActualValue(currentValue) {
|
|
||||||
if (Array.isArray(currentValue)) {
|
|
||||||
return _findActualValue(currentValue[0])
|
|
||||||
}
|
|
||||||
return currentValue
|
|
||||||
}
|
|
||||||
formEntry = _findActualValue(formData[element.id])
|
|
||||||
}
|
|
||||||
return [element.id, formEntry?.value]
|
return [element.id, formEntry?.value]
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -80,15 +80,27 @@ const mutations = {
|
||||||
updateCachedValues(page)
|
updateCachedValues(page)
|
||||||
},
|
},
|
||||||
UPDATE_ITEM(state, { page, element: elementToUpdate, values }) {
|
UPDATE_ITEM(state, { page, element: elementToUpdate, values }) {
|
||||||
|
let updateCached = false
|
||||||
page.elements.forEach((element) => {
|
page.elements.forEach((element) => {
|
||||||
if (element.id === elementToUpdate.id) {
|
if (element.id === elementToUpdate.id) {
|
||||||
|
if (
|
||||||
|
(values.order !== undefined && values.order !== element.order) ||
|
||||||
|
(values.place_in_container !== undefined &&
|
||||||
|
values.place_in_container !== element.place_in_container)
|
||||||
|
) {
|
||||||
|
updateCached = true
|
||||||
|
}
|
||||||
Object.assign(element, values)
|
Object.assign(element, values)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (state.selected?.id === elementToUpdate.id) {
|
if (state.selected?.id === elementToUpdate.id) {
|
||||||
Object.assign(state.selected, values)
|
Object.assign(state.selected, values)
|
||||||
}
|
}
|
||||||
|
if (updateCached) {
|
||||||
|
// We need to update cached values only if order or place of an element has
|
||||||
|
// changed or if an element has been added or removed.
|
||||||
updateCachedValues(page)
|
updateCachedValues(page)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
DELETE_ITEM(state, { page, elementId }) {
|
DELETE_ITEM(state, { page, elementId }) {
|
||||||
const index = page.elements.findIndex((element) => element.id === elementId)
|
const index = page.elements.findIndex((element) => element.id === elementId)
|
||||||
|
|
|
@ -158,9 +158,8 @@
|
||||||
@import 'get_formula_component';
|
@import 'get_formula_component';
|
||||||
@import 'color_input';
|
@import 'color_input';
|
||||||
@import 'group_bys';
|
@import 'group_bys';
|
||||||
@import 'data_explorer/node';
|
|
||||||
@import 'data_explorer/root_node';
|
|
||||||
@import 'data_explorer/data_explorer';
|
@import 'data_explorer/data_explorer';
|
||||||
|
@import 'data_explorer/data_explorer_node';
|
||||||
@import 'anchor';
|
@import 'anchor';
|
||||||
@import 'call_to_action';
|
@import 'call_to_action';
|
||||||
@import 'toast_button';
|
@import 'toast_button';
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
.data-explorer-node__content-icon {
|
||||||
|
color: $color-neutral-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node__children {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node__content {
|
||||||
|
@extend %ellipsis;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin: 0 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
.data-explorer-node--selected & {
|
||||||
|
background-color: $color-primary-100;
|
||||||
|
|
||||||
|
.data-explorer-node__content-selected-icon {
|
||||||
|
color: $color-success-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node--level-0 > & {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $color-neutral-500;
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node--level-0 .data-explorer-node--level-1 & {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $color-neutral-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node--level-0 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node__content-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-explorer-node__array-node-more {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding-top: 3px;
|
||||||
|
color: $color-neutral-600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -1,44 +0,0 @@
|
||||||
.node {
|
|
||||||
margin: 3px 0 3px 10px;
|
|
||||||
|
|
||||||
&.node--first-indentation {
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.node__content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0 5px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 3px;
|
|
||||||
line-height: 24px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $color-neutral-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
background-color: $color-primary-100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.node__content-name {
|
|
||||||
@extend %ellipsis;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node__selected {
|
|
||||||
margin: auto 0 auto 4px;
|
|
||||||
color: $color-success-500;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.node__icon {
|
|
||||||
color: $color-neutral-600;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
.root-node {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-node__name {
|
|
||||||
font-size: 12px;
|
|
||||||
color: $color-neutral-500;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
|
@ -6,35 +6,35 @@
|
||||||
class="data-explorer"
|
class="data-explorer"
|
||||||
@shown="onShow"
|
@shown="onShow"
|
||||||
>
|
>
|
||||||
<div
|
<div ref="wrapper">
|
||||||
ref="wrapper"
|
|
||||||
tabindex="0"
|
|
||||||
@focusin="$emit('focusin')"
|
|
||||||
@focusout="$emit('focusout')"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="context--loading">
|
<div v-if="loading" class="context--loading">
|
||||||
<div class="loading"></div>
|
<div class="loading" />
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<SelectSearch
|
<SelectSearch
|
||||||
v-model="search"
|
v-model="search"
|
||||||
:placeholder="$t('action.search')"
|
:placeholder="$t('action.search')"
|
||||||
class="margin-bottom-1"
|
class="margin-bottom-1"
|
||||||
></SelectSearch>
|
/>
|
||||||
<RootNode
|
<DataExplorerNode
|
||||||
v-for="node in matchingNodes"
|
v-for="node in nodes"
|
||||||
:key="node.identifier"
|
:key="node.identifier"
|
||||||
:node="node"
|
:node="node"
|
||||||
:node-selected="nodeSelected"
|
|
||||||
:open-nodes="openNodes"
|
:open-nodes="openNodes"
|
||||||
@node-selected="$emit('node-selected', $event)"
|
:path="node.identifier"
|
||||||
|
:search-path="node.identifier"
|
||||||
|
:node-selected="nodeSelected"
|
||||||
|
:search="debouncedSearch"
|
||||||
|
@click="$emit('node-selected', $event)"
|
||||||
@toggle="toggleNode"
|
@toggle="toggleNode"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="nodes.length === 0 || emptyResults"
|
||||||
|
class="context__description"
|
||||||
>
|
>
|
||||||
</RootNode>
|
<span v-if="emptyResults">
|
||||||
<div v-if="matchingNodes.length === 0" class="context__description">
|
{{ $t('dataExplorer.noMatchingNodesText') }}
|
||||||
<span v-if="isSearching">{{
|
</span>
|
||||||
$t('dataExplorer.noMatchingNodesText')
|
|
||||||
}}</span>
|
|
||||||
<span v-else>{{ $t('dataExplorer.noProvidersText') }}</span>
|
<span v-else>{{ $t('dataExplorer.noProvidersText') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -45,12 +45,13 @@
|
||||||
<script>
|
<script>
|
||||||
import context from '@baserow/modules/core/mixins/context'
|
import context from '@baserow/modules/core/mixins/context'
|
||||||
import SelectSearch from '@baserow/modules/core/components/SelectSearch'
|
import SelectSearch from '@baserow/modules/core/components/SelectSearch'
|
||||||
import RootNode from '@baserow/modules/core/components/dataExplorer/RootNode'
|
import DataExplorerNode from '@baserow/modules/core/components/dataExplorer/DataExplorerNode'
|
||||||
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DataExplorer',
|
name: 'DataExplorer',
|
||||||
components: { SelectSearch, RootNode },
|
components: { SelectSearch, DataExplorerNode },
|
||||||
mixins: [context],
|
mixins: [context],
|
||||||
props: {
|
props: {
|
||||||
nodes: {
|
nodes: {
|
||||||
|
@ -82,6 +83,9 @@ export default {
|
||||||
isSearching() {
|
isSearching() {
|
||||||
return Boolean(this.debouncedSearch)
|
return Boolean(this.debouncedSearch)
|
||||||
},
|
},
|
||||||
|
emptyResults() {
|
||||||
|
return this.isSearching && this.openNodes.size === 0
|
||||||
|
},
|
||||||
matchingPaths() {
|
matchingPaths() {
|
||||||
if (!this.isSearching) {
|
if (!this.isSearching) {
|
||||||
return new Set()
|
return new Set()
|
||||||
|
@ -89,34 +93,26 @@ export default {
|
||||||
return this.matchesSearch(this.nodes, this.debouncedSearch)
|
return this.matchesSearch(this.nodes, this.debouncedSearch)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
matchingNodes() {
|
|
||||||
if (!this.isSearching) {
|
|
||||||
return this.nodes
|
|
||||||
} else {
|
|
||||||
return this.filterNodes(
|
|
||||||
this.nodes,
|
|
||||||
(node, path) => path === '' || this.matchingPaths.has(path)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
/**
|
/**
|
||||||
* Debounces the actual search to prevent perf issues
|
* Debounces the actual search to prevent perf issues
|
||||||
*/
|
*/
|
||||||
search(value) {
|
search(newSearch) {
|
||||||
|
this.$emit('node-unselected')
|
||||||
clearTimeout(this.debounceSearch)
|
clearTimeout(this.debounceSearch)
|
||||||
this.debounceSearch = setTimeout(() => {
|
this.debounceSearch = setTimeout(() => {
|
||||||
this.debouncedSearch = value
|
this.debouncedSearch = newSearch.trim().toLowerCase() || null
|
||||||
}, 300)
|
}, 300)
|
||||||
},
|
},
|
||||||
matchingPaths(value) {
|
matchingPaths(value) {
|
||||||
this.openNodes = value
|
this.openNodes = value
|
||||||
},
|
},
|
||||||
nodeSelected: {
|
nodeSelected: {
|
||||||
handler(value) {
|
handler(path) {
|
||||||
if (value !== null) {
|
if (path) {
|
||||||
this.toggleNode(value, true)
|
this.debouncedSearch = null
|
||||||
|
this.toggleNode(path, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
@ -146,13 +142,16 @@ export default {
|
||||||
* @returns A Set of path of nodes that match the search term
|
* @returns A Set of path of nodes that match the search term
|
||||||
*/
|
*/
|
||||||
matchesSearch(nodes, search, parentPath = []) {
|
matchesSearch(nodes, search, parentPath = []) {
|
||||||
const searchSanitised = search.trim().toLowerCase()
|
|
||||||
|
|
||||||
return (nodes || []).reduce((acc, subNode) => {
|
return (nodes || []).reduce((acc, subNode) => {
|
||||||
const subNodePath = [...parentPath, subNode.identifier]
|
let subNodePath = [...parentPath, subNode.identifier]
|
||||||
|
|
||||||
if (subNode.nodes) {
|
if (subNode.nodes) {
|
||||||
// It's not a leaf
|
// It's not a leaf
|
||||||
|
if (subNode.type === 'array') {
|
||||||
|
// For array we have a special case. We need to match any intermediate value
|
||||||
|
// Can be either `*` or an integer. We use the `__any__` placeholder to
|
||||||
|
// achieve that.
|
||||||
|
subNodePath = [...parentPath, subNode.identifier, '__any__']
|
||||||
|
}
|
||||||
const subSubNodes = this.matchesSearch(
|
const subSubNodes = this.matchesSearch(
|
||||||
subNode.nodes,
|
subNode.nodes,
|
||||||
search,
|
search,
|
||||||
|
@ -160,10 +159,10 @@ export default {
|
||||||
)
|
)
|
||||||
acc = new Set([...acc, ...subSubNodes])
|
acc = new Set([...acc, ...subSubNodes])
|
||||||
} else {
|
} else {
|
||||||
// It's a leaf we check if the name match the search
|
// It's a leaf we check if the name matches the search
|
||||||
const nodeNameSanitised = subNode.name.trim().toLowerCase()
|
const nodeNameSanitised = subNode.name.trim().toLowerCase()
|
||||||
|
|
||||||
if (nodeNameSanitised.includes(searchSanitised)) {
|
if (nodeNameSanitised.includes(search)) {
|
||||||
// We also add the parents of the node
|
// We also add the parents of the node
|
||||||
acc = new Set([...acc, ...this.getPathAndParents(subNodePath)])
|
acc = new Set([...acc, ...this.getPathAndParents(subNodePath)])
|
||||||
}
|
}
|
||||||
|
@ -171,25 +170,6 @@ export default {
|
||||||
return acc
|
return acc
|
||||||
}, new Set())
|
}, new Set())
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Filters the nodes according to the given predicate. The predicate receives the
|
|
||||||
* node itself and the path of the node.
|
|
||||||
* @param {Array} nodes Node tree to filter.
|
|
||||||
* @param {Function} predicate Should return true if the node should be kept.
|
|
||||||
* @param {Array<String>} path Current nodes path part list.
|
|
||||||
*/
|
|
||||||
filterNodes(nodes, predicate, path = []) {
|
|
||||||
const result = (nodes || [])
|
|
||||||
.filter((node) => predicate(node, [...path, node.identifier].join('.')))
|
|
||||||
.map((node) => ({
|
|
||||||
...node,
|
|
||||||
nodes: this.filterNodes(node.nodes, predicate, [
|
|
||||||
...path,
|
|
||||||
node.identifier,
|
|
||||||
]),
|
|
||||||
}))
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Toggles a node state
|
* Toggles a node state
|
||||||
* @param {string} path to open/close.
|
* @param {string} path to open/close.
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="showNode"
|
||||||
|
class="data-explorer-node"
|
||||||
|
:class="{
|
||||||
|
[`data-explorer-node--level-${depth}`]: true,
|
||||||
|
'data-explorer-node--selected': isSelected,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="data-explorer-node__content" @click="handleClick(node)">
|
||||||
|
<i
|
||||||
|
v-if="depth > 0"
|
||||||
|
class="data-explorer-node__content-icon"
|
||||||
|
:class="getIcon(node)"
|
||||||
|
/>
|
||||||
|
<span class="data-explorer-node__content-name">{{ node.name }}</span>
|
||||||
|
<i
|
||||||
|
v-if="isSelected"
|
||||||
|
class="data-explorer-node__content-selected-icon iconoir-check-circle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="isNodeOpen" ref="nodes" class="data-explorer-node__children">
|
||||||
|
<template v-if="node.type !== 'array'">
|
||||||
|
<DataExplorerNode
|
||||||
|
v-for="subNode in sortedNodes"
|
||||||
|
:key="subNode.identifier"
|
||||||
|
:node="subNode"
|
||||||
|
:depth="depth + 1"
|
||||||
|
:open-nodes="openNodes"
|
||||||
|
:node-selected="nodeSelected"
|
||||||
|
:path="`${path}.${subNode.identifier}`"
|
||||||
|
:search-path="`${searchPath}.${subNode.identifier}`"
|
||||||
|
:search="search"
|
||||||
|
@click="$emit('click', $event)"
|
||||||
|
@toggle="$emit('toggle', $event)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div v-else>
|
||||||
|
<DataExplorerNode
|
||||||
|
v-for="subNode in arrayNodes"
|
||||||
|
:key="subNode.identifier"
|
||||||
|
:node="subNode"
|
||||||
|
:depth="depth + 1"
|
||||||
|
:open-nodes="openNodes"
|
||||||
|
:node-selected="nodeSelected"
|
||||||
|
:search="search"
|
||||||
|
:path="`${path}.${subNode.identifier}`"
|
||||||
|
:search-path="`${searchPath}.__any__`"
|
||||||
|
@click="$emit('click', $event)"
|
||||||
|
@toggle="$emit('toggle', $event)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-tooltip="$t('dataExplorerNode.showMore')"
|
||||||
|
class="data-explorer-node__array-node-more"
|
||||||
|
@click="count += nextIncrement"
|
||||||
|
>
|
||||||
|
{{ `[ ${count}...${nextCount - 1} ]` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DataExplorerNode',
|
||||||
|
props: {
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
depth: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
openNodes: {
|
||||||
|
type: Set,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
nodeSelected: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return { count: 3 }
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isSelected() {
|
||||||
|
return this.nodeSelected === this.path
|
||||||
|
},
|
||||||
|
showNode() {
|
||||||
|
// we show the node if...
|
||||||
|
return (
|
||||||
|
// We are not searching
|
||||||
|
this.search === null ||
|
||||||
|
// It is selected
|
||||||
|
this.isSelected ||
|
||||||
|
// It has children and at least one children matches
|
||||||
|
(this.hasChildren && this.openNodes.has(this.searchPath)) ||
|
||||||
|
// Or it's a leaf and it matches the search term
|
||||||
|
this.node.name.trim().toLowerCase().includes(this.search)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hasChildren() {
|
||||||
|
return this.node.nodes?.length > 0
|
||||||
|
},
|
||||||
|
sortedNodes() {
|
||||||
|
if (this.hasChildren) {
|
||||||
|
return [...this.node.nodes].sort((a, b) => a.order - b.order)
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isNodeOpen() {
|
||||||
|
return (
|
||||||
|
// It's open if we are the first level
|
||||||
|
this.depth === 0 ||
|
||||||
|
// if it's in open node
|
||||||
|
this.openNodes.has(this.path) ||
|
||||||
|
// or if the search path is in openNodes
|
||||||
|
// The search path is the version with `__any__` instead of array indexes
|
||||||
|
this.openNodes.has(this.searchPath)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
nextCount() {
|
||||||
|
return this.count + 10 - ((this.count + 10) % 10)
|
||||||
|
},
|
||||||
|
nextIncrement() {
|
||||||
|
return this.nextCount - this.count
|
||||||
|
},
|
||||||
|
arrayNodes() {
|
||||||
|
if (this.node.type === 'array') {
|
||||||
|
// In case of array node, we generate the nodes on demand
|
||||||
|
const head = {
|
||||||
|
nodes: this.node.nodes,
|
||||||
|
identifier: '*',
|
||||||
|
name: `[${this.$t('common.all')}]`,
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
head,
|
||||||
|
...[...Array(this.count).keys()].map((index) => ({
|
||||||
|
nodes: this.node.nodes,
|
||||||
|
identifier: `${index}`,
|
||||||
|
name: `${index}`,
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
nodeSelected: {
|
||||||
|
handler(newValue) {
|
||||||
|
// Generate enough array nodes to display arbitrary selected data
|
||||||
|
if (this.node.type === 'array' && newValue?.startsWith(this.path)) {
|
||||||
|
const nodeSelectedPath = _.toPath(newValue)
|
||||||
|
const pathParts = _.toPath(this.path)
|
||||||
|
const indexStr = nodeSelectedPath[pathParts.length]
|
||||||
|
|
||||||
|
if (indexStr !== '*') {
|
||||||
|
const index = parseInt(indexStr)
|
||||||
|
if (this.count <= index) {
|
||||||
|
this.count = index + 10 - ((index + 10) % 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
isSelected: {
|
||||||
|
async handler(newValue) {
|
||||||
|
if (newValue) {
|
||||||
|
await this.$nextTick()
|
||||||
|
// We scroll it into view when it becomes selected.
|
||||||
|
this.$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick(node) {
|
||||||
|
if (this.depth < 1) {
|
||||||
|
// We don't want to click on first level
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.hasChildren) {
|
||||||
|
if (this.search === null) {
|
||||||
|
this.$emit('toggle', this.path)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$emit('click', { path: this.path, node })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getIcon(node) {
|
||||||
|
if (this.hasChildren) {
|
||||||
|
return this.isNodeOpen
|
||||||
|
? 'iconoir-nav-arrow-down'
|
||||||
|
: 'iconoir-nav-arrow-right'
|
||||||
|
}
|
||||||
|
return node.icon
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,96 +0,0 @@
|
||||||
<template functional>
|
|
||||||
<div
|
|
||||||
:data-identifier="props.node.identifier"
|
|
||||||
class="node"
|
|
||||||
:class="{ 'node--first-indentation': props.indentation === 0 }"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="node__content"
|
|
||||||
:class="{
|
|
||||||
'node__content--selected': props.nodeSelected === props.path,
|
|
||||||
}"
|
|
||||||
@click="$options.methods.click(props.node, props.path, listeners)"
|
|
||||||
>
|
|
||||||
<div class="node__content-name">
|
|
||||||
<i
|
|
||||||
class="node__icon"
|
|
||||||
:class="`${$options.methods.getIcon(
|
|
||||||
props.node,
|
|
||||||
props.openNodes.has(props.path)
|
|
||||||
)}`"
|
|
||||||
></i>
|
|
||||||
{{ props.node.name }}
|
|
||||||
</div>
|
|
||||||
<i
|
|
||||||
v-if="props.nodeSelected === props.path"
|
|
||||||
class="node__selected iconoir-check-circle"
|
|
||||||
></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="props.openNodes.has(props.path)">
|
|
||||||
<Node
|
|
||||||
v-for="subNode in $options.methods.sortNodes(props.node.nodes || [])"
|
|
||||||
:key="subNode.identifier"
|
|
||||||
:node="subNode"
|
|
||||||
:open-nodes="props.openNodes"
|
|
||||||
:node-selected="props.nodeSelected"
|
|
||||||
:indentation="props.indentation + 1"
|
|
||||||
:path="`${props.path}.${subNode.identifier}`"
|
|
||||||
@click="listeners.click && listeners.click($event)"
|
|
||||||
@toggle="listeners.toggle && listeners.toggle($event)"
|
|
||||||
></Node>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'Node',
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
openNodes: {
|
|
||||||
type: Set,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
indentation: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
nodeSelected: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
click(node, path, listeners) {
|
|
||||||
if (node.nodes?.length > 0 && listeners.toggle) {
|
|
||||||
listeners.toggle(path)
|
|
||||||
} else if (listeners.click) {
|
|
||||||
listeners.click({
|
|
||||||
path,
|
|
||||||
node,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getIcon(node, isOpen) {
|
|
||||||
if (!node.nodes?.length || node.nodes?.length < 1) {
|
|
||||||
return node.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOpen ? 'iconoir-nav-arrow-down' : 'iconoir-nav-arrow-right'
|
|
||||||
},
|
|
||||||
sortNodes(nodes) {
|
|
||||||
return nodes.sort((a, b) => a.order - b.order)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,99 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="root-node">
|
|
||||||
<div class="root-node__name">
|
|
||||||
{{ node.name }}
|
|
||||||
</div>
|
|
||||||
<div ref="nodes">
|
|
||||||
<Node
|
|
||||||
v-for="subNode in sortNodes(node.nodes)"
|
|
||||||
:key="subNode.identifier"
|
|
||||||
:node="subNode"
|
|
||||||
:node-selected="nodeSelected"
|
|
||||||
:open-nodes="openNodes"
|
|
||||||
:path="`${node.identifier}.${subNode.identifier}`"
|
|
||||||
@click="$emit('node-selected', $event)"
|
|
||||||
@toggle="$emit('toggle', $event)"
|
|
||||||
></Node>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Node from '@baserow/modules/core/components/dataExplorer/Node'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RootNode',
|
|
||||||
components: { Node },
|
|
||||||
props: {
|
|
||||||
node: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
openNodes: {
|
|
||||||
type: Set,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
nodeSelected: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
async nodeSelected(path) {
|
|
||||||
if (path === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the nodes to be opened
|
|
||||||
await this.$nextTick()
|
|
||||||
|
|
||||||
const element = this.getNodeElementByPath(
|
|
||||||
this.$refs.nodes,
|
|
||||||
// Remove the first part since it is the root node, and we don't need that
|
|
||||||
_.toPath(path).slice(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getNodeElementByPath(element, path) {
|
|
||||||
const [identifier, ...rest] = path
|
|
||||||
|
|
||||||
const childMatching = [...element.children].find(
|
|
||||||
(child) => child.dataset.identifier === identifier
|
|
||||||
)
|
|
||||||
|
|
||||||
// We found the final element!
|
|
||||||
if (childMatching && rest.length === 0) {
|
|
||||||
return childMatching
|
|
||||||
}
|
|
||||||
|
|
||||||
// That means we still haven't gone through the whole path
|
|
||||||
if (childMatching && rest.length > 0) {
|
|
||||||
return this.getNodeElementByPath(childMatching, rest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// That means we have gone into a dead end
|
|
||||||
if (!childMatching && !element.children.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// That means we have to keep searching the children to find the next piece of the
|
|
||||||
// path
|
|
||||||
return (
|
|
||||||
[...element.children]
|
|
||||||
.map((child) => this.getNodeElementByPath(child, path))
|
|
||||||
.find((e) => e !== null) || null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
sortNodes(nodes) {
|
|
||||||
return nodes.sort((a, b) => a.order - b.order)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -18,16 +18,15 @@
|
||||||
@data-component-clicked="dataComponentClicked"
|
@data-component-clicked="dataComponentClicked"
|
||||||
/>
|
/>
|
||||||
<DataExplorer
|
<DataExplorer
|
||||||
|
v-if="isFocused"
|
||||||
ref="dataExplorer"
|
ref="dataExplorer"
|
||||||
:nodes="nodes"
|
:nodes="nodes"
|
||||||
:node-selected="nodeSelected"
|
:node-selected="nodeSelected"
|
||||||
:loading="dataExplorerLoading"
|
:loading="dataExplorerLoading"
|
||||||
:application-context="applicationContext"
|
:application-context="applicationContext"
|
||||||
@node-selected="dataExplorerItemSelected"
|
@node-selected="dataExplorerItemSelected"
|
||||||
@node-toggled="editor.commands.focus()"
|
@node-unselected="unSelectNode()"
|
||||||
@focusin="dataExplorerFocused = true"
|
/>
|
||||||
@focusout="dataExplorerFocused = false"
|
|
||||||
></DataExplorer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -44,6 +43,7 @@ import { FromTipTapVisitor } from '@baserow/modules/core/formula/tiptap/fromTipT
|
||||||
import { mergeAttributes } from '@tiptap/core'
|
import { mergeAttributes } from '@tiptap/core'
|
||||||
import DataExplorer from '@baserow/modules/core/components/dataExplorer/DataExplorer'
|
import DataExplorer from '@baserow/modules/core/components/dataExplorer/DataExplorer'
|
||||||
import { RuntimeGet } from '@baserow/modules/core/runtimeFormulaTypes'
|
import { RuntimeGet } from '@baserow/modules/core/runtimeFormulaTypes'
|
||||||
|
import { isElement, onClickOutside } from '@baserow/modules/core/utils/dom'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormulaInputField',
|
name: 'FormulaInputField',
|
||||||
|
@ -98,15 +98,11 @@ export default {
|
||||||
content: null,
|
content: null,
|
||||||
isFormulaInvalid: false,
|
isFormulaInvalid: false,
|
||||||
dataNodeSelected: null,
|
dataNodeSelected: null,
|
||||||
dataExplorerFocused: false,
|
|
||||||
formulaInputFocused: false,
|
|
||||||
valueUpdateTimeout: null,
|
valueUpdateTimeout: null,
|
||||||
|
isFocused: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isFocused() {
|
|
||||||
return this.dataExplorerFocused || this.formulaInputFocused
|
|
||||||
},
|
|
||||||
classes() {
|
classes() {
|
||||||
return {
|
return {
|
||||||
'form-input--disabled': this.disabled,
|
'form-input--disabled': this.disabled,
|
||||||
|
@ -157,9 +153,10 @@ export default {
|
||||||
return this.editor.getJSON()
|
return this.editor.getJSON()
|
||||||
},
|
},
|
||||||
nodes() {
|
nodes() {
|
||||||
return this.dataProviders
|
const nodes = this.dataProviders
|
||||||
.map((dataProvider) => dataProvider.getNodes(this.applicationContext))
|
.map((dataProvider) => dataProvider.getNodes(this.applicationContext))
|
||||||
.filter((dataProviderNodes) => dataProviderNodes.nodes?.length > 0)
|
.filter((dataProviderNodes) => dataProviderNodes.nodes?.length > 0)
|
||||||
|
return nodes
|
||||||
},
|
},
|
||||||
nodeSelected() {
|
nodeSelected() {
|
||||||
return this.dataNodeSelected?.attrs?.path || null
|
return this.dataNodeSelected?.attrs?.path || null
|
||||||
|
@ -169,11 +166,14 @@ export default {
|
||||||
disabled(newValue) {
|
disabled(newValue) {
|
||||||
this.editor.setOptions({ editable: !newValue })
|
this.editor.setOptions({ editable: !newValue })
|
||||||
},
|
},
|
||||||
isFocused(value) {
|
async isFocused(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
this.$refs.dataExplorer.hide()
|
this.$refs.dataExplorer.hide()
|
||||||
this.unSelectNode()
|
this.unSelectNode()
|
||||||
} else {
|
} else {
|
||||||
|
// Wait for the data explorer to appear in the DOM.
|
||||||
|
await this.$nextTick()
|
||||||
|
|
||||||
this.unSelectNode()
|
this.unSelectNode()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -233,7 +233,6 @@ export default {
|
||||||
editable: !this.disabled,
|
editable: !this.disabled,
|
||||||
onUpdate: this.onUpdate,
|
onUpdate: this.onUpdate,
|
||||||
onFocus: this.onFocus,
|
onFocus: this.onFocus,
|
||||||
onBlur: this.onBlur,
|
|
||||||
extensions: this.extensions,
|
extensions: this.extensions,
|
||||||
parseOptions: {
|
parseOptions: {
|
||||||
preserveWhitespace: 'full',
|
preserveWhitespace: 'full',
|
||||||
|
@ -260,20 +259,28 @@ export default {
|
||||||
this.unSelectNode()
|
this.unSelectNode()
|
||||||
this.emitChange()
|
this.emitChange()
|
||||||
},
|
},
|
||||||
onFocus() {
|
onFocus(event) {
|
||||||
// If the input is disabled, we don't want users to be
|
// If the input is disabled, we don't want users to be
|
||||||
// able to open the data explorer and select nodes.
|
// able to open the data explorer and select nodes.
|
||||||
this.formulaInputFocused = !this.disabled
|
if (this.disabled) {
|
||||||
},
|
return
|
||||||
onBlur() {
|
}
|
||||||
// We have to delay the browser here by just a bit, running the below will make
|
this.isFocused = true
|
||||||
// sure the browser will execute all other events first, and then trigger this
|
|
||||||
// function. If we don't do this, the data explorer will be closed before the
|
this.$el.clickOutsideEventCancel = onClickOutside(
|
||||||
// focus event can be fired which results in a closed data explorer once you lose
|
this.$el,
|
||||||
// focus on the input.
|
(target, event) => {
|
||||||
setTimeout(() => {
|
if (
|
||||||
this.formulaInputFocused = false
|
this.$refs.dataExplorer &&
|
||||||
}, 0)
|
// We ignore clicks inside data explorer
|
||||||
|
!isElement(this.$refs.dataExplorer.$el, target)
|
||||||
|
) {
|
||||||
|
this.isFocused = false
|
||||||
|
this.editor.commands.blur()
|
||||||
|
this.$el.clickOutsideEventCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
toContent(formula) {
|
toContent(formula) {
|
||||||
if (!formula) {
|
if (!formula) {
|
||||||
|
|
|
@ -41,9 +41,6 @@ export default {
|
||||||
},
|
},
|
||||||
mixins: [formulaComponent],
|
mixins: [formulaComponent],
|
||||||
inject: ['applicationContext', 'dataProviders'],
|
inject: ['applicationContext', 'dataProviders'],
|
||||||
data() {
|
|
||||||
return { nodes: [], pathParts: [] }
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
availableData() {
|
availableData() {
|
||||||
return Object.values(this.dataProviders).map((dataProvider) =>
|
return Object.values(this.dataProviders).map((dataProvider) =>
|
||||||
|
@ -68,10 +65,10 @@ export default {
|
||||||
(dataProvider) => dataProvider.type === pathParts[0]
|
(dataProvider) => dataProvider.type === pathParts[0]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
nodes() {
|
||||||
|
return [this.dataProviderType.getNodes(this.applicationContext)]
|
||||||
},
|
},
|
||||||
mounted() {
|
pathParts() {
|
||||||
if (this.dataProviderType) {
|
|
||||||
this.nodes = [this.dataProviderType.getNodes(this.applicationContext)]
|
|
||||||
const translatedPathPart = this.rawPathParts.map((_, index) =>
|
const translatedPathPart = this.rawPathParts.map((_, index) =>
|
||||||
this.dataProviderType.getPathTitle(
|
this.dataProviderType.getPathTitle(
|
||||||
this.applicationContext,
|
this.applicationContext,
|
||||||
|
@ -80,8 +77,8 @@ export default {
|
||||||
)
|
)
|
||||||
|
|
||||||
translatedPathPart[0] = this.dataProviderType.name
|
translatedPathPart[0] = this.dataProviderType.name
|
||||||
this.pathParts = translatedPathPart
|
return translatedPathPart
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
findNode(nodes, path) {
|
findNode(nodes, path) {
|
||||||
|
@ -98,8 +95,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rest.length > 0) {
|
if (rest.length > 0) {
|
||||||
|
if (nodeFound.type === 'array') {
|
||||||
|
const [index, ...afterIndex] = rest
|
||||||
|
// Check that the index is what is expected
|
||||||
|
if (!(index === '*' || /^\d+$/.test(index))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.findNode(nodeFound.nodes, afterIndex)
|
||||||
|
} else {
|
||||||
return this.findNode(nodeFound.nodes, rest)
|
return this.findNode(nodeFound.nodes, rest)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nodeFound
|
return nodeFound
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Registerable } from '@baserow/modules/core/registry'
|
import { Registerable } from '@baserow/modules/core/registry'
|
||||||
import { getIconForType } from '@baserow/modules/core/utils/icon'
|
import { getIconForType } from '@baserow/modules/core/utils/icon'
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data provider gets data from the application context and populate the context for
|
* A data provider gets data from the application context and populate the context for
|
||||||
|
@ -130,19 +129,13 @@ export class DataProviderType extends Registerable {
|
||||||
* @returns {{identifier: string, name: string, nodes: []}}
|
* @returns {{identifier: string, name: string, nodes: []}}
|
||||||
*/
|
*/
|
||||||
getNodes(applicationContext) {
|
getNodes(applicationContext) {
|
||||||
const content = this.getDataContent(applicationContext)
|
|
||||||
const schema = this.getDataSchema(applicationContext)
|
const schema = this.getDataSchema(applicationContext)
|
||||||
|
|
||||||
if (schema === null) {
|
if (schema === null) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = this._toNode(
|
const result = this._toNode(applicationContext, [this.type], schema)
|
||||||
applicationContext,
|
|
||||||
[this.type],
|
|
||||||
content,
|
|
||||||
schema
|
|
||||||
)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,11 +143,10 @@ export class DataProviderType extends Registerable {
|
||||||
* Recursive method to deeply compute the node tree for this data providers.
|
* Recursive method to deeply compute the node tree for this data providers.
|
||||||
* @param {Object} applicationContext the application context.
|
* @param {Object} applicationContext the application context.
|
||||||
* @param {Array<String>} pathParts the path to get to the current node.
|
* @param {Array<String>} pathParts the path to get to the current node.
|
||||||
* @param {*} content the current node content.
|
|
||||||
* @param {$schema: string} schema the current node schema.
|
* @param {$schema: string} schema the current node schema.
|
||||||
* @returns {{identifier: string, name: string, nodes: []}}
|
* @returns {{identifier: string, name: string, nodes: []}}
|
||||||
*/
|
*/
|
||||||
_toNode(applicationContext, pathParts, content, schema) {
|
_toNode(applicationContext, pathParts, schema) {
|
||||||
const identifier = pathParts.at(-1)
|
const identifier = pathParts.at(-1)
|
||||||
const name = this.getPathTitle(applicationContext, pathParts)
|
const name = this.getPathTitle(applicationContext, pathParts)
|
||||||
const order = schema?.order || null
|
const order = schema?.order || null
|
||||||
|
@ -170,44 +162,16 @@ export class DataProviderType extends Registerable {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schema.type === 'array') {
|
if (schema.type === 'array') {
|
||||||
// When the current node is an array we append a new node to its children
|
|
||||||
// that represents the "whole" array.
|
|
||||||
// This is translated to '*' in the resulting formula and '[All]' in the
|
|
||||||
// data explorer name.
|
|
||||||
// The 'head' object contains all keys of the schema assigned to null
|
|
||||||
let fakeContent = null
|
|
||||||
if (schema.items.type === 'object') {
|
|
||||||
fakeContent = _.mapValues(schema.items.properties, () => null)
|
|
||||||
fakeContent = {}
|
|
||||||
}
|
|
||||||
if (schema.items.type === 'array') {
|
|
||||||
fakeContent = []
|
|
||||||
}
|
|
||||||
|
|
||||||
const head = {
|
|
||||||
...this._toNode(
|
|
||||||
applicationContext,
|
|
||||||
[...pathParts, '*'],
|
|
||||||
fakeContent,
|
|
||||||
schema.items
|
|
||||||
),
|
|
||||||
name: `[${this.app.i18n.t('common.all')}]`,
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
identifier,
|
identifier,
|
||||||
icon: this.getIconForNode(schema),
|
icon: this.getIconForNode(schema),
|
||||||
nodes: [
|
type: 'array',
|
||||||
head,
|
nodes: this._toNode(
|
||||||
...(content || []).map((item, index) =>
|
|
||||||
this._toNode(
|
|
||||||
applicationContext,
|
applicationContext,
|
||||||
[...pathParts, `${index}`],
|
[...pathParts, null],
|
||||||
item,
|
|
||||||
schema.items
|
schema.items
|
||||||
)
|
).nodes,
|
||||||
),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +186,6 @@ export class DataProviderType extends Registerable {
|
||||||
this._toNode(
|
this._toNode(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
[...pathParts, identifier],
|
[...pathParts, identifier],
|
||||||
(content || {})[identifier],
|
|
||||||
subSchema
|
subSchema
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -234,7 +197,6 @@ export class DataProviderType extends Registerable {
|
||||||
order,
|
order,
|
||||||
type: schema.type,
|
type: schema.type,
|
||||||
icon: this.getIconForNode(schema),
|
icon: this.getIconForNode(schema),
|
||||||
value: content,
|
|
||||||
identifier,
|
identifier,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const DATA_TYPE_TO_ICON_MAP = {
|
||||||
string: 'iconoir-text',
|
string: 'iconoir-text',
|
||||||
number: 'baserow-icon-hashtag',
|
number: 'baserow-icon-hashtag',
|
||||||
boolean: 'baserow-icon-circle-checked',
|
boolean: 'baserow-icon-circle-checked',
|
||||||
|
array: 'iconoir-list',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UNKNOWN_DATA_TYPE_ICON = 'iconoir-question-mark'
|
export const UNKNOWN_DATA_TYPE_ICON = 'iconoir-question-mark'
|
||||||
|
|
|
@ -647,7 +647,7 @@
|
||||||
"errorInvalidFormula": "The formula is invalid."
|
"errorInvalidFormula": "The formula is invalid."
|
||||||
},
|
},
|
||||||
"dataExplorer": {
|
"dataExplorer": {
|
||||||
"noMatchingNodesText": "No matching data providers were found.",
|
"noMatchingNodesText": "No matching results were found.",
|
||||||
"noProvidersText": "No data providers were found. To get started you can, for example, add a data source or page parameter."
|
"noProvidersText": "No data providers were found. To get started you can, for example, add a data source or page parameter."
|
||||||
},
|
},
|
||||||
"richTextEditorBubbleMenu": {
|
"richTextEditorBubbleMenu": {
|
||||||
|
@ -718,5 +718,15 @@
|
||||||
"workspaceStep": {
|
"workspaceStep": {
|
||||||
"title": "Create your workspace",
|
"title": "Create your workspace",
|
||||||
"workspaceLabel": "Workspace name"
|
"workspaceLabel": "Workspace name"
|
||||||
|
},
|
||||||
|
"colorInput": {
|
||||||
|
"default": "Default"
|
||||||
|
},
|
||||||
|
"imageInput": {
|
||||||
|
"labelDescription": "Select an image to upload...",
|
||||||
|
"labelButton": "Upload"
|
||||||
|
},
|
||||||
|
"dataExplorerNode": {
|
||||||
|
"showMore": "Show more repetitions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue