mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-17 18:32:35 +00:00
Resolve "Data Explorer"
This commit is contained in:
parent
95208e4f8b
commit
c2ce168564
39 changed files with 1196 additions and 241 deletions
tests/cases
web-frontend
modules
builder
components
dataProviderTypes.jsenums.jslocales
mixins
pages
pathParamTypes.jsstore
core
assets/scss/components
components
dataProviderTypes.jsdirectives
locales
runtimeFormulaContext.jsruntimeFormulaTypes.jsintegrations/components/services
|
@ -23,7 +23,7 @@
|
|||
"content": [
|
||||
{
|
||||
"type": "get-formula-component",
|
||||
"attrs": { "path": "data_source.hello.there" }
|
||||
"attrs": { "path": "data_source.hello.there", "isSelected": false }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -32,7 +32,7 @@
|
|||
"content": [
|
||||
{
|
||||
"type": "get-formula-component",
|
||||
"attrs": { "path": "data_source.hello.there" }
|
||||
"attrs": { "path": "data_source.hello.there", "isSelected": false }
|
||||
},
|
||||
{ "type": "text", "text": "friend :)" }
|
||||
]
|
||||
|
@ -66,7 +66,7 @@
|
|||
},
|
||||
{
|
||||
"type": "get-formula-component",
|
||||
"attrs": { "path": "data_source.hello.there" }
|
||||
"attrs": { "path": "data_source.hello.there", "isSelected": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<FormulaInputGroup
|
||||
v-bind="$attrs"
|
||||
:data-explorer-loading="dataExplorerLoading"
|
||||
:data-providers="dataProviders"
|
||||
v-on="$listeners"
|
||||
></FormulaInputGroup>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormulaInputGroup from '@baserow/modules/core/components/formula/FormulaInputGroup'
|
||||
import { DataSourceDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
|
||||
export default {
|
||||
name: 'ApplicationBuilderFormulaInputGroup',
|
||||
components: { FormulaInputGroup },
|
||||
inject: ['page'],
|
||||
props: {
|
||||
dataProvidersAllowed: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
dataSourceLoading() {
|
||||
return this.$store.getters['dataSource/getLoading'](this.page)
|
||||
},
|
||||
dataSourceContentLoading() {
|
||||
return this.$store.getters['dataSourceContent/getLoading'](this.page)
|
||||
},
|
||||
dataProviders() {
|
||||
return Object.values(this.$registry.getAll('builderDataProvider')).filter(
|
||||
(dataProvider) =>
|
||||
this.dataProvidersAllowed.includes(dataProvider.getType())
|
||||
)
|
||||
},
|
||||
dataExplorerLoading() {
|
||||
return this.dataProvidersAllowed.some(
|
||||
(dataProviderName) => this.dataProviderLoadingMap[dataProviderName]
|
||||
)
|
||||
},
|
||||
/**
|
||||
* This mapping defines which data providers are affected by what loading states.
|
||||
* Since not all data providers are always used in every data explorer we
|
||||
* shouldn't put the data explorer in a loading state whenever some inaccessible
|
||||
* data is loading.
|
||||
*/
|
||||
dataProviderLoadingMap() {
|
||||
return {
|
||||
[new DataSourceDataProviderType().getType()]:
|
||||
this.dataSourceLoading || this.dataSourceContentLoading,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -7,9 +7,6 @@
|
|||
class="data-source-form__name-input"
|
||||
:placeholder="$t('dataSourceForm.namePlaceholder')"
|
||||
/>
|
||||
<!-- TODO This and it's corresponding prop will be removed in the data
|
||||
explorer MR -->
|
||||
({{ id }})
|
||||
<Dropdown
|
||||
v-model="values.type"
|
||||
class="data-source-form__type-dropdown"
|
||||
|
|
|
@ -17,25 +17,23 @@
|
|||
</Dropdown>
|
||||
</div>
|
||||
</FormElement>
|
||||
<FormulaInputGroup
|
||||
<ApplicationBuilderFormulaInputGroup
|
||||
v-model="values.value"
|
||||
:label="$t('headingElementForm.textTitle')"
|
||||
:placeholder="$t('elementForms.textInputPlaceholder')"
|
||||
:error="
|
||||
!$v.values.value.validFormula ? $t('elementForms.invalidFormula') : ''
|
||||
"
|
||||
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_SIDEBAR"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
import FormulaInputGroup from '@baserow/modules/core/components/formula/FormulaInputGroup'
|
||||
import { isValidFormula } from '@baserow/modules/core/formula'
|
||||
import { DATA_PROVIDERS_ALLOWED_SIDEBAR } from '@baserow/modules/builder/enums'
|
||||
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
|
||||
|
||||
export default {
|
||||
name: 'HeaderElementForm',
|
||||
components: { FormulaInputGroup },
|
||||
components: { ApplicationBuilderFormulaInputGroup },
|
||||
mixins: [form],
|
||||
props: {},
|
||||
data() {
|
||||
|
@ -50,12 +48,8 @@ export default {
|
|||
})),
|
||||
}
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
values: {
|
||||
value: { validFormula: isValidFormula },
|
||||
},
|
||||
}
|
||||
computed: {
|
||||
DATA_PROVIDERS_ALLOWED_SIDEBAR: () => DATA_PROVIDERS_ALLOWED_SIDEBAR,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<template>
|
||||
<form @submit.prevent @keydown.enter.prevent>
|
||||
<FormulaInputGroup
|
||||
<ApplicationBuilderFormulaInputGroup
|
||||
v-model="values.value"
|
||||
:label="$t('linkElementForm.text')"
|
||||
:placeholder="$t('linkElementForm.textPlaceholder')"
|
||||
:error="
|
||||
!$v.values.value.validFormula ? $t('elementForms.invalidFormula') : ''
|
||||
"
|
||||
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_SIDEBAR"
|
||||
/>
|
||||
<FormElement class="control">
|
||||
<label class="control__label">
|
||||
|
@ -42,17 +40,12 @@
|
|||
</div>
|
||||
</FormElement>
|
||||
<FormElement v-if="navigateTo === 'custom'" class="control">
|
||||
<FormulaInputGroup
|
||||
<ApplicationBuilderFormulaInputGroup
|
||||
v-model="values.navigate_to_url"
|
||||
:page="page"
|
||||
:label="$t('linkElementForm.url')"
|
||||
:placeholder="$t('linkElementForm.urlPlaceholder')"
|
||||
:error="
|
||||
!$v.values.navigate_to_url.validFormula
|
||||
? $t('elementForms.invalidFormula')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.values.navigate_to_url.$touch()"
|
||||
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_SIDEBAR"
|
||||
/>
|
||||
</FormElement>
|
||||
<FormElement v-if="destinationPage" class="control">
|
||||
|
@ -73,23 +66,17 @@
|
|||
</template>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="(param, index) in values.page_parameters"
|
||||
v-for="param in values.page_parameters"
|
||||
:key="param.name"
|
||||
class="link-element-form__param"
|
||||
>
|
||||
<FormulaInputGroup
|
||||
<ApplicationBuilderFormulaInputGroup
|
||||
v-model="param.value"
|
||||
:page="page"
|
||||
:label="param.name"
|
||||
horizontal
|
||||
:placeholder="$t('linkElementForm.paramPlaceholder')"
|
||||
:error="
|
||||
$v.values.page_parameters.$each[index].$dirty &&
|
||||
$v.values.page_parameters.$each[index].$error
|
||||
? $t('elementForms.invalidFormula')
|
||||
: ''
|
||||
"
|
||||
@blur="$v.values.page_parameters.$each[index].$touch()"
|
||||
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_PAGE_PARAMETERS"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -133,14 +120,24 @@
|
|||
import form from '@baserow/modules/core/mixins/form'
|
||||
import { LinkElementType } from '@baserow/modules/builder/elementTypes'
|
||||
import HorizontalAlignmentSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/HorizontalAlignmentsSelector'
|
||||
import { HORIZONTAL_ALIGNMENTS, WIDTHS } from '@baserow/modules/builder/enums'
|
||||
import {
|
||||
DATA_PROVIDERS_ALLOWED_SIDEBAR,
|
||||
HORIZONTAL_ALIGNMENTS,
|
||||
WIDTHS,
|
||||
} from '@baserow/modules/builder/enums'
|
||||
import FormulaInputGroup from '@baserow/modules/core/components/formula/FormulaInputGroup'
|
||||
import { isValidFormula } from '@baserow/modules/core/formula'
|
||||
import WidthSelector from '@baserow/modules/builder/components/elements/components/forms/general/settings/WidthSelector'
|
||||
import { PageParameterDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
|
||||
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
|
||||
|
||||
export default {
|
||||
name: 'LinkElementForm',
|
||||
components: { WidthSelector, HorizontalAlignmentSelector, FormulaInputGroup },
|
||||
components: {
|
||||
WidthSelector,
|
||||
FormulaInputGroup,
|
||||
ApplicationBuilderFormulaInputGroup,
|
||||
HorizontalAlignmentSelector,
|
||||
},
|
||||
mixins: [form],
|
||||
props: {
|
||||
builder: { type: Object, required: true },
|
||||
|
@ -172,6 +169,15 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
DATA_PROVIDERS_ALLOWED_SIDEBAR: () => DATA_PROVIDERS_ALLOWED_SIDEBAR,
|
||||
DATA_PROVIDERS_ALLOWED_PAGE_PARAMETERS() {
|
||||
const PROVIDERS_TO_REMOVE = [
|
||||
new PageParameterDataProviderType().getType(),
|
||||
]
|
||||
return this.DATA_PROVIDERS_ALLOWED_SIDEBAR.filter(
|
||||
(dataProvider) => !PROVIDERS_TO_REMOVE.includes(dataProvider)
|
||||
)
|
||||
},
|
||||
pages() {
|
||||
return this.builder.pages
|
||||
},
|
||||
|
@ -218,11 +224,6 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
getPageParameterType(parameterName) {
|
||||
return (this.destinationPage?.path_params || []).find(
|
||||
(pathParam) => pathParam.name === parameterName
|
||||
)?.type
|
||||
},
|
||||
updatePageParameters() {
|
||||
this.values.page_parameters = (
|
||||
this.destinationPage?.path_params || []
|
||||
|
@ -231,22 +232,7 @@ export default {
|
|||
return { name, value: previousValue }
|
||||
})
|
||||
this.parametersInError = false
|
||||
this.$v.values.page_parameters.$touch()
|
||||
},
|
||||
validatePageParameter(pageParameter) {
|
||||
return isValidFormula(pageParameter.value)
|
||||
},
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
values: {
|
||||
value: { validFormula: isValidFormula },
|
||||
page_parameters: {
|
||||
$each: { $validator: this.validatePageParameter },
|
||||
},
|
||||
navigate_to_url: { validFormula: isValidFormula },
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
<template>
|
||||
<form @submit.prevent @keydown.enter.prevent>
|
||||
<FormulaInputGroup
|
||||
<ApplicationBuilderFormulaInputGroup
|
||||
v-model="values.value"
|
||||
:label="$t('paragraphElementForm.textTitle')"
|
||||
:placeholder="$t('elementForms.textInputPlaceholder')"
|
||||
:error="
|
||||
!$v.values.value.validFormula ? $t('elementForms.invalidFormula') : ''
|
||||
"
|
||||
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_SIDEBAR"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
@ -14,12 +12,12 @@
|
|||
<script>
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
import FormulaInputGroup from '@baserow/modules/core/components/formula/FormulaInputGroup'
|
||||
import { isValidFormula } from '@baserow/modules/core/formula'
|
||||
import { DATA_PROVIDERS_ALLOWED_SIDEBAR } from '@baserow/modules/builder/enums'
|
||||
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
|
||||
|
||||
export default {
|
||||
name: 'ParagraphElementForm',
|
||||
components: { FormulaInputGroup },
|
||||
components: { ApplicationBuilderFormulaInputGroup },
|
||||
mixins: [form],
|
||||
props: {},
|
||||
data() {
|
||||
|
@ -29,12 +27,8 @@ export default {
|
|||
},
|
||||
}
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
values: {
|
||||
value: { validFormula: isValidFormula },
|
||||
},
|
||||
}
|
||||
computed: {
|
||||
DATA_PROVIDERS_ALLOWED_SIDEBAR: () => DATA_PROVIDERS_ALLOWED_SIDEBAR,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DataProviderType } from '@baserow/modules/core/dataProviderTypes'
|
||||
import GenerateSchema from 'generate-schema'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
|
@ -20,29 +21,90 @@ export class DataSourceDataProviderType extends DataProviderType {
|
|||
return this.app.i18n.t('dataProviderType.dataSource')
|
||||
}
|
||||
|
||||
async init(runtimeFormulaContext) {
|
||||
async init(applicationContext) {
|
||||
const dataSources = this.app.store.getters['dataSource/getPageDataSources'](
|
||||
runtimeFormulaContext.applicationContext.page
|
||||
applicationContext.page
|
||||
)
|
||||
|
||||
// Dispatch the data sources
|
||||
await this.app.store.dispatch(
|
||||
'dataSourceContent/fetchPageDataSourceContent',
|
||||
{
|
||||
page: runtimeFormulaContext.applicationContext.page,
|
||||
data: runtimeFormulaContext.getAllBackendContext(),
|
||||
page: applicationContext.page,
|
||||
data: DataProviderType.getAllBackendContext(
|
||||
this.app.$registry.getAll('builderDataProvider'),
|
||||
applicationContext
|
||||
),
|
||||
dataSources,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getDataChunk(runtimeFormulaContext, [dataSourceId, ...rest]) {
|
||||
const dataSourceContents = this.app.store.getters[
|
||||
'dataSourceContent/getDataSourceContents'
|
||||
](runtimeFormulaContext.applicationContext.page)
|
||||
getDataChunk(applicationContext, [dataSourceId, ...rest]) {
|
||||
const content = this.getDataSourceContent(applicationContext, dataSourceId)
|
||||
|
||||
const content = dataSourceContents[dataSourceId]
|
||||
return content ? _.get(content, rest.join('.')) : null
|
||||
}
|
||||
|
||||
getDataSourceContent(applicationContext, dataSourceId) {
|
||||
const page = applicationContext.page
|
||||
const dataSourceContents =
|
||||
this.app.store.getters['dataSourceContent/getDataSourceContents'](page)
|
||||
|
||||
return dataSourceContents[dataSourceId]
|
||||
}
|
||||
|
||||
getDataSourceSchema(applicationContext, dataSourceId) {
|
||||
return GenerateSchema.json(
|
||||
this.getDataSourceContent(applicationContext, dataSourceId)
|
||||
)
|
||||
}
|
||||
|
||||
getDataContent(applicationContext) {
|
||||
const page = applicationContext.page
|
||||
const dataSources =
|
||||
this.app.store.getters['dataSource/getPageDataSources'](page)
|
||||
|
||||
return Object.fromEntries(
|
||||
dataSources.map((dataSource) => {
|
||||
return [
|
||||
dataSource.id,
|
||||
this.getDataSourceContent(applicationContext, dataSource.id),
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
getDataSchema(applicationContext) {
|
||||
const page = applicationContext.page
|
||||
const dataSources =
|
||||
this.app.store.getters['dataSource/getPageDataSources'](page)
|
||||
|
||||
const dataSourcesSchema = Object.fromEntries(
|
||||
dataSources.map((dataSource) => {
|
||||
const dsSchema = this.getDataSourceSchema(
|
||||
applicationContext,
|
||||
dataSource.id
|
||||
)
|
||||
delete dsSchema.$schema
|
||||
return [dataSource.id, dsSchema]
|
||||
})
|
||||
)
|
||||
|
||||
return { type: 'object', properties: dataSourcesSchema }
|
||||
}
|
||||
|
||||
pathPartToDisplay(applicationContext, part, position) {
|
||||
if (position === 1) {
|
||||
const page = applicationContext?.page
|
||||
return this.app.store.getters['dataSource/getPageDataSourceById'](
|
||||
page,
|
||||
parseInt(part)
|
||||
)?.name
|
||||
}
|
||||
|
||||
return super.pathPartToDisplay(applicationContext, part, position)
|
||||
}
|
||||
}
|
||||
|
||||
export class PageParameterDataProviderType extends DataProviderType {
|
||||
|
@ -54,9 +116,8 @@ export class PageParameterDataProviderType extends DataProviderType {
|
|||
return this.app.i18n.t('dataProviderType.pageParameter')
|
||||
}
|
||||
|
||||
async init(runtimeFormulaContext) {
|
||||
const { page, mode, pageParamsValue } =
|
||||
runtimeFormulaContext.applicationContext
|
||||
async init(applicationContext) {
|
||||
const { page, mode, pageParamsValue } = applicationContext
|
||||
if (mode === 'editing') {
|
||||
// Generate fake values for the parameters
|
||||
await Promise.all(
|
||||
|
@ -82,14 +143,14 @@ export class PageParameterDataProviderType extends DataProviderType {
|
|||
}
|
||||
}
|
||||
|
||||
getDataChunk(runtimeFormulaContext, path) {
|
||||
getDataChunk(applicationContext, path) {
|
||||
if (path.length !== 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [prop] = path
|
||||
const parameters = this.app.store.getters['pageParameter/getParameters'](
|
||||
runtimeFormulaContext.applicationContext.page
|
||||
applicationContext.page
|
||||
)
|
||||
|
||||
if (parameters[prop] === undefined) {
|
||||
|
@ -99,9 +160,31 @@ export class PageParameterDataProviderType extends DataProviderType {
|
|||
return parameters[prop]
|
||||
}
|
||||
|
||||
getBackendContext(runtimeFormulaContext) {
|
||||
getBackendContext(applicationContext) {
|
||||
return this.getDataContent(applicationContext)
|
||||
}
|
||||
|
||||
getDataContent(applicationContext) {
|
||||
return this.app.store.getters['pageParameter/getParameters'](
|
||||
runtimeFormulaContext.applicationContext.page
|
||||
applicationContext.page
|
||||
)
|
||||
}
|
||||
|
||||
getDataSchema(applicationContext) {
|
||||
const page = applicationContext.page
|
||||
const toJSONType = { text: 'string', numeric: 'number' }
|
||||
|
||||
return {
|
||||
type: 'object',
|
||||
properties: Object.fromEntries(
|
||||
(page?.path_params || []).map(({ name, type }) => [
|
||||
name,
|
||||
{
|
||||
name,
|
||||
type: toJSONType[type],
|
||||
},
|
||||
])
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,10 @@ import {
|
|||
ensureInteger,
|
||||
ensureString,
|
||||
} from '@baserow/modules/core/utils/validator'
|
||||
import {
|
||||
DataSourceDataProviderType,
|
||||
PageParameterDataProviderType,
|
||||
} from '@baserow/modules/builder/dataProviderTypes'
|
||||
|
||||
export const PLACEMENTS = {
|
||||
BEFORE: 'before',
|
||||
|
@ -56,3 +60,23 @@ export const WIDTHS = {
|
|||
AUTO: { value: 'auto', name: 'widthSelector.widthAuto' },
|
||||
FULL: { value: 'full', name: 'widthSelector.widthFull' },
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all the data providers that can be used in the formula field on the right
|
||||
* sidebar in the application builder.
|
||||
*
|
||||
* @type {String[]}
|
||||
*/
|
||||
export const DATA_PROVIDERS_ALLOWED_SIDEBAR = [
|
||||
new DataSourceDataProviderType().getType(),
|
||||
new PageParameterDataProviderType().getType(),
|
||||
]
|
||||
|
||||
/**
|
||||
* A list of all the data provider that can be used to configure data sources.
|
||||
*
|
||||
* @type {String[]}
|
||||
*/
|
||||
export const DATA_PROVIDERS_ALLOWED_DATA_SOURCES = [
|
||||
new PageParameterDataProviderType().getType(),
|
||||
]
|
||||
|
|
|
@ -288,5 +288,8 @@
|
|||
},
|
||||
"eventTypes": {
|
||||
"clickLabel": "On click"
|
||||
},
|
||||
"getFormulaComponent": {
|
||||
"errorTooltip": "Invalid reference"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import _ from 'lodash'
|
|||
|
||||
import { clone } from '@baserow/modules/core/utils/object'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import BigNumber from 'bignumber.js'
|
||||
|
||||
export default {
|
||||
inject: ['builder', 'page'],
|
||||
|
@ -36,6 +37,8 @@ export default {
|
|||
async onChange(newValues) {
|
||||
const oldValues = this.element
|
||||
|
||||
newValues.order = new BigNumber(newValues.order)
|
||||
|
||||
if (!this.$refs.panelForm.isFormValid()) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { StoreItemLookupError } from '@baserow/modules/core/errors'
|
|||
import PageHeader from '@baserow/modules/builder/components/page/header/PageHeader'
|
||||
import PagePreview from '@baserow/modules/builder/components/page/PagePreview'
|
||||
import PageSidePanels from '@baserow/modules/builder/components/page/PageSidePanels'
|
||||
import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext'
|
||||
import { DataProviderType } from '@baserow/modules/core/dataProviderTypes'
|
||||
|
||||
export default {
|
||||
name: 'PageEditor',
|
||||
|
@ -57,17 +57,11 @@ export default {
|
|||
store.dispatch('element/fetch', { page }),
|
||||
])
|
||||
|
||||
const runtimeFormulaContext = new RuntimeFormulaContext(
|
||||
$registry.getAll('builderDataProvider'),
|
||||
{
|
||||
builder,
|
||||
page,
|
||||
mode: 'editing',
|
||||
}
|
||||
)
|
||||
|
||||
// Initialize all data provider contents
|
||||
await runtimeFormulaContext.initAll()
|
||||
await DataProviderType.initAll($registry.getAll('builderDataProvider'), {
|
||||
builder,
|
||||
page,
|
||||
mode: 'editing',
|
||||
})
|
||||
|
||||
// And finally select the page to display it
|
||||
await store.dispatch('page/selectById', {
|
||||
|
@ -89,21 +83,21 @@ export default {
|
|||
return data
|
||||
},
|
||||
computed: {
|
||||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
mode: 'editing',
|
||||
}
|
||||
},
|
||||
dataSources() {
|
||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
||||
},
|
||||
runtimeFormulaContext() {
|
||||
return new RuntimeFormulaContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
{
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
mode: 'editing',
|
||||
}
|
||||
)
|
||||
},
|
||||
backendContext() {
|
||||
return this.runtimeFormulaContext.getAllBackendContext()
|
||||
return DataProviderType.getAllBackendContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
<script>
|
||||
import PageContent from '@baserow/modules/builder/components/page/PageContent'
|
||||
import { resolveApplicationRoute } from '@baserow/modules/builder/utils/routing'
|
||||
import RuntimeFormulaContext from '@baserow/modules/core/runtimeFormulaContext'
|
||||
|
||||
import { DataProviderType } from '@baserow/modules/core/dataProviderTypes'
|
||||
|
||||
export default {
|
||||
components: { PageContent },
|
||||
|
@ -83,18 +84,12 @@ export default {
|
|||
store.dispatch('element/fetchPublished', { page }),
|
||||
])
|
||||
|
||||
const runtimeFormulaContext = new RuntimeFormulaContext(
|
||||
$registry.getAll('builderDataProvider'),
|
||||
{
|
||||
builder,
|
||||
page,
|
||||
pageParamsValue,
|
||||
mode,
|
||||
}
|
||||
)
|
||||
|
||||
// Initialize all data provider contents
|
||||
await runtimeFormulaContext.initAll()
|
||||
await DataProviderType.initAll($registry.getAll('builderDataProvider'), {
|
||||
builder,
|
||||
page,
|
||||
pageParamsValue,
|
||||
mode,
|
||||
})
|
||||
|
||||
// And finally select the page to display it
|
||||
await store.dispatch('page/selectById', {
|
||||
|
@ -123,19 +118,19 @@ export default {
|
|||
elements() {
|
||||
return this.$store.getters['element/getRootElements'](this.page)
|
||||
},
|
||||
runtimeFormulaContext() {
|
||||
return new RuntimeFormulaContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
{
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
pageParamsValue: this.params,
|
||||
mode: this.mode,
|
||||
}
|
||||
)
|
||||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
pageParamsValue: this.params,
|
||||
mode: this.mode,
|
||||
}
|
||||
},
|
||||
backendContext() {
|
||||
return this.runtimeFormulaContext.getAllBackendContext()
|
||||
return DataProviderType.getAllBackendContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -4,6 +4,10 @@ export class PathParamType extends Registerable {
|
|||
get name() {
|
||||
return null
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class TextPathParamType extends PathParamType {
|
||||
|
@ -18,6 +22,10 @@ export class TextPathParamType extends PathParamType {
|
|||
get name() {
|
||||
return this.app.i18n.t('pathParamTypes.textName')
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'font'
|
||||
}
|
||||
}
|
||||
|
||||
export class NumericPathParamType extends PathParamType {
|
||||
|
@ -32,4 +40,8 @@ export class NumericPathParamType extends PathParamType {
|
|||
get name() {
|
||||
return this.app.i18n.t('pathParamTypes.numericName')
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return 'hashtag'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,9 @@ const mutations = {
|
|||
CLEAR_ITEMS(state, { page }) {
|
||||
page.dataSources = []
|
||||
},
|
||||
SET_LOADING(state, { page, value }) {
|
||||
page._.dataSourceLoading = value
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
@ -82,7 +85,8 @@ const actions = {
|
|||
commit('MOVE_ITEM', { page, index, oldIndex })
|
||||
}
|
||||
},
|
||||
async create({ dispatch }, { page, values, beforeId }) {
|
||||
async create({ commit, dispatch }, { page, values, beforeId }) {
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
const { data: dataSource } = await DataSourceService(this.$client).create(
|
||||
page.id,
|
||||
values,
|
||||
|
@ -90,8 +94,9 @@ const actions = {
|
|||
)
|
||||
|
||||
await dispatch('forceCreate', { page, dataSource, beforeId })
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
async update({ dispatch }, { page, dataSourceId, values }) {
|
||||
async update({ commit, dispatch }, { page, dataSourceId, values }) {
|
||||
const dataSourcesOfPage = getters.getPageDataSources(page)
|
||||
const dataSource = dataSourcesOfPage.find(
|
||||
(dataSource) => dataSource.id === dataSourceId
|
||||
|
@ -107,12 +112,14 @@ const actions = {
|
|||
|
||||
await dispatch('forceUpdate', { page, dataSource, values: newValues })
|
||||
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
try {
|
||||
await DataSourceService(this.$client).update(dataSource.id, values)
|
||||
} catch (error) {
|
||||
await dispatch('forceUpdate', { page, dataSource, values: oldValues })
|
||||
throw error
|
||||
}
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
|
||||
async debouncedUpdate(
|
||||
|
@ -143,6 +150,7 @@ const actions = {
|
|||
const fire = async () => {
|
||||
const toUpdate = updateContext.valuesToUpdate
|
||||
updateContext.valuesToUpdate = {}
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
try {
|
||||
const { data } = await DataSourceService(this.$client).update(
|
||||
dataSource.id,
|
||||
|
@ -161,6 +169,7 @@ const actions = {
|
|||
updateContext.lastUpdatedValues = null
|
||||
reject(error)
|
||||
}
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
}
|
||||
|
||||
if (updateContext.promiseResolve) {
|
||||
|
@ -178,7 +187,7 @@ const actions = {
|
|||
updateContext.promiseResolve = resolve
|
||||
})
|
||||
},
|
||||
async delete({ dispatch, getters }, { page, dataSourceId }) {
|
||||
async delete({ commit, dispatch, getters }, { page, dataSourceId }) {
|
||||
const dataSourcesOfPage = getters.getPageDataSources(page)
|
||||
const dataSourceIndex = dataSourcesOfPage.findIndex(
|
||||
(dataSource) => dataSource.id === dataSourceId
|
||||
|
@ -191,6 +200,7 @@ const actions = {
|
|||
|
||||
await dispatch('forceDelete', { page, dataSourceId })
|
||||
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
try {
|
||||
await DataSourceService(this.$client).delete(dataSourceId)
|
||||
} catch (error) {
|
||||
|
@ -201,8 +211,10 @@ const actions = {
|
|||
})
|
||||
throw error
|
||||
}
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
async fetch({ dispatch, commit }, { page }) {
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
dispatch(
|
||||
'dataSourceContent/clearDataSourceContents',
|
||||
{ page },
|
||||
|
@ -218,10 +230,12 @@ const actions = {
|
|||
dispatch('forceCreate', { page, dataSource })
|
||||
)
|
||||
)
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
|
||||
return dataSources
|
||||
},
|
||||
async fetchPublished({ dispatch, commit }, { page }) {
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
dispatch(
|
||||
'dataSourceContent/clearDataSourceContents',
|
||||
{ page },
|
||||
|
@ -238,6 +252,7 @@ const actions = {
|
|||
dispatch('forceCreate', { page, dataSource })
|
||||
)
|
||||
)
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
|
||||
return dataSources
|
||||
},
|
||||
|
@ -258,14 +273,16 @@ const actions = {
|
|||
throw error
|
||||
}
|
||||
},
|
||||
async duplicate({ getters, dispatch }, { page, dataSourceId }) {
|
||||
async duplicate({ commit, getters, dispatch }, { page, dataSourceId }) {
|
||||
const dataSourcesOfPage = getters.getPageDataSources(page)
|
||||
const dataSource = dataSourcesOfPage.find((e) => e.id === dataSourceId)
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
await dispatch('create', {
|
||||
page,
|
||||
dataSourceType: dataSource.type,
|
||||
beforeId: dataSource.id,
|
||||
})
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -273,6 +290,12 @@ const getters = {
|
|||
getPageDataSources: (state) => (page) => {
|
||||
return page.dataSources
|
||||
},
|
||||
getPageDataSourceById: (state) => (page, id) => {
|
||||
return page.dataSources.find((dataSource) => dataSource.id === id)
|
||||
},
|
||||
getLoading: (state) => (page) => {
|
||||
return page._.dataSourceLoading
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -27,6 +27,9 @@ const mutations = {
|
|||
CLEAR_CONTENTS(state, { page }) {
|
||||
page.contents = {}
|
||||
},
|
||||
SET_LOADING(state, { page, value }) {
|
||||
page._.dataSourceContentLoading = value
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
@ -45,6 +48,7 @@ const actions = {
|
|||
|
||||
const serviceType = this.app.$registry.get('service', dataSource.type)
|
||||
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
try {
|
||||
if (serviceType.isValid(dataSource)) {
|
||||
const { data } = await DataSourceService(this.app.$client).dispatch(
|
||||
|
@ -66,12 +70,14 @@ const actions = {
|
|||
} catch (e) {
|
||||
commit('SET_CONTENT', { page, dataSourceId: dataSource.id, value: null })
|
||||
}
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch the content for every data sources of the given page.
|
||||
*/
|
||||
async fetchPageDataSourceContent({ commit }, { page, data: queryData }) {
|
||||
commit('SET_LOADING', { page, value: true })
|
||||
try {
|
||||
const { data } = await DataSourceService(this.app.$client).dispatchAll(
|
||||
page.id,
|
||||
|
@ -90,6 +96,7 @@ const actions = {
|
|||
commit('CLEAR_CONTENTS', { page })
|
||||
throw e
|
||||
}
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
|
||||
debouncedFetchPageDataSourceContent({ dispatch }, { page, data: queryData }) {
|
||||
|
@ -111,6 +118,9 @@ const getters = {
|
|||
getDataSourceContents: (state) => (page) => {
|
||||
return page.contents || {}
|
||||
},
|
||||
getLoading: (state) => (page) => {
|
||||
return page._.dataSourceContentLoading
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
|
@ -6,6 +6,8 @@ import { generateHash } from '@baserow/modules/core/utils/hashing'
|
|||
export function populatePage(page) {
|
||||
page._ = {
|
||||
selected: false,
|
||||
dataSourceContentLoading: false,
|
||||
dataSourceLoading: false,
|
||||
}
|
||||
|
||||
page.dataSources = []
|
||||
|
|
|
@ -133,3 +133,6 @@
|
|||
@import 'theme_settings';
|
||||
@import 'color_input';
|
||||
@import 'group_bys';
|
||||
@import 'data_explorer/node';
|
||||
@import 'data_explorer/root_node';
|
||||
@import 'data_explorer/data_explorer';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.data-source-context {
|
||||
padding: 12px;
|
||||
width: 560px;
|
||||
}
|
||||
|
||||
.data-source-context__none {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.data-explorer {
|
||||
width: 323px;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
.node {
|
||||
margin: 4px 0 4px 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;
|
||||
}
|
||||
|
||||
.node__selected {
|
||||
margin: auto 0 auto 4px;
|
||||
color: $color-success-500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.node__icon {
|
||||
color: $color-neutral-500;
|
||||
width: 15px;
|
||||
|
||||
&::before {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.root-node__name {
|
||||
font-size: 12px;
|
||||
color: $color-neutral-500;
|
||||
margin-left: 10px;
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
.formula-input-field {
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
padding: 6px 12px;
|
||||
padding: 4px 12px;
|
||||
min-height: 34px;
|
||||
}
|
||||
|
||||
.formula-input-field--focused {
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
.get-formula-component {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background-color: $color-neutral-100;
|
||||
font-size: 12px;
|
||||
margin: 1px 0;
|
||||
user-select: all;
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
line-height: 18px;
|
||||
@include rounded($rounded);
|
||||
}
|
||||
|
||||
.get-formula-component--error {
|
||||
background-color: $color-error-100;
|
||||
}
|
||||
|
||||
.get-formula-component--selected {
|
||||
background-color: $color-primary-100;
|
||||
}
|
||||
|
||||
.get-formula-component__caret {
|
||||
color: $color-neutral-300;
|
||||
padding-left: 3px;
|
||||
|
|
|
@ -453,7 +453,7 @@ export default {
|
|||
targetRect.right - contextRect.width - horizontalOffset > 0
|
||||
const canLeft =
|
||||
window.innerWidth -
|
||||
targetRect.right -
|
||||
targetRect.left -
|
||||
contextRect.width -
|
||||
horizontalOffset >
|
||||
0
|
||||
|
|
25
web-frontend/modules/core/components/SelectSearch.vue
Normal file
25
web-frontend/modules/core/components/SelectSearch.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="select__search">
|
||||
<i class="select__search-icon fas fa-search"></i>
|
||||
<input
|
||||
:value="value"
|
||||
type="text"
|
||||
class="select__search-input"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectSearch',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<Context
|
||||
overflow-scroll
|
||||
max-height-if-outside-viewport
|
||||
:hide-on-click-outside="false"
|
||||
class="data-explorer"
|
||||
@shown="onShow"
|
||||
>
|
||||
<div v-if="loading" class="context--loading">
|
||||
<div class="loading"></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<SelectSearch
|
||||
v-model="search"
|
||||
:placeholder="$t('action.search')"
|
||||
class="margin-bottom-1"
|
||||
></SelectSearch>
|
||||
<RootNode
|
||||
v-for="node in matchingNodes"
|
||||
:key="node.identifier"
|
||||
:node="node"
|
||||
:node-selected="nodeSelected"
|
||||
:open-nodes="openNodes"
|
||||
@node-selected="$emit('node-selected', $event)"
|
||||
@toggle="toggleNode"
|
||||
>
|
||||
</RootNode>
|
||||
<div v-if="matchingNodes.length === 0" class="context__description">
|
||||
{{ $t('dataExplorer.emptyText') }}
|
||||
</div>
|
||||
</template>
|
||||
</Context>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from '@baserow/modules/core/mixins/context'
|
||||
import SelectSearch from '@baserow/modules/core/components/SelectSearch'
|
||||
import RootNode from '@baserow/modules/core/components/dataExplorer/RootNode'
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'DataExplorer',
|
||||
components: { SelectSearch, RootNode },
|
||||
mixins: [context],
|
||||
props: {
|
||||
nodes: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
nodeSelected: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
search: null,
|
||||
debounceSearch: null,
|
||||
debouncedSearch: null,
|
||||
// A map of open node paths
|
||||
openNodes: new Set(),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSearching() {
|
||||
return Boolean(this.debouncedSearch)
|
||||
},
|
||||
matchingPaths() {
|
||||
if (!this.isSearching) {
|
||||
return new Set()
|
||||
} else {
|
||||
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: {
|
||||
/**
|
||||
* Debounces the actual search to prevent perf issues
|
||||
*/
|
||||
search(value) {
|
||||
clearTimeout(this.debounceSearch)
|
||||
this.debounceSearch = setTimeout(() => {
|
||||
this.debouncedSearch = value
|
||||
}, 300)
|
||||
},
|
||||
matchingPaths(value) {
|
||||
this.openNodes = value
|
||||
},
|
||||
nodeSelected: {
|
||||
handler(value) {
|
||||
if (value !== null) {
|
||||
this.toggleNode(value, true)
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Resets state on show context
|
||||
*/
|
||||
onShow() {
|
||||
this.search = null
|
||||
this.openNodes = new Set()
|
||||
},
|
||||
/**
|
||||
* Given a dotted path, returns a list of prefixes and the given path.
|
||||
* @param {String} path the path we want the ancestors for.
|
||||
*/
|
||||
getPathAndParents(path) {
|
||||
return _.toPath(path).map((item, index, pathParts) =>
|
||||
pathParts.slice(0, index + 1).join('.')
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Returns a Set of leaf nodes path that match the search term (and their parents).
|
||||
* @param {Array} nodes Nodes tree.
|
||||
* @param {String} parentPath Path of the current nodes.
|
||||
* @returns A Set of path of nodes that match the search term
|
||||
*/
|
||||
matchesSearch(nodes, search, parentPath = []) {
|
||||
const searchSanitised = search.trim().toLowerCase()
|
||||
|
||||
return (nodes || []).reduce((acc, subNode) => {
|
||||
const subNodePath = [...parentPath, subNode.identifier]
|
||||
|
||||
if (subNode.nodes) {
|
||||
// It's not a leaf
|
||||
const subSubNodes = this.matchesSearch(
|
||||
subNode.nodes,
|
||||
search,
|
||||
subNodePath
|
||||
)
|
||||
acc = new Set([...acc, ...subSubNodes])
|
||||
} else {
|
||||
// It's a leaf we check if the name match the search
|
||||
const nodeNameSanitised = subNode.name.trim().toLowerCase()
|
||||
|
||||
if (nodeNameSanitised.includes(searchSanitised)) {
|
||||
// We also add the parents of the node
|
||||
acc = new Set([...acc, ...this.getPathAndParents(subNodePath)])
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, 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
|
||||
* @param {string} path to open/close.
|
||||
* @param {Boolean} forceOpen if we want to open the node anyway.
|
||||
*/
|
||||
toggleNode(path, forceOpen = false) {
|
||||
const shouldOpenNode = forceOpen || !this.openNodes.has(path)
|
||||
|
||||
if (shouldOpenNode) {
|
||||
// Open all parents as well
|
||||
this.openNodes = new Set([
|
||||
...this.openNodes,
|
||||
...this.getPathAndParents(path),
|
||||
])
|
||||
} else {
|
||||
const newOpenNodes = new Set(this.openNodes)
|
||||
newOpenNodes.delete(path)
|
||||
this.openNodes = newOpenNodes
|
||||
}
|
||||
|
||||
this.$emit('node-toggled')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
94
web-frontend/modules/core/components/dataExplorer/Node.vue
Normal file
94
web-frontend/modules/core/components/dataExplorer/Node.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<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="`fas fa-${$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 fas fa-check-circle"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<div v-if="props.openNodes.has(props.path)">
|
||||
<Node
|
||||
v-for="subNode in 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 ? 'caret-down' : 'caret-right'
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,96 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="root-node__name">
|
||||
{{ node.name }}
|
||||
</div>
|
||||
<div ref="nodes">
|
||||
<Node
|
||||
v-for="subNode in 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
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -12,12 +12,23 @@
|
|||
{{ $t('action.reset') }}
|
||||
</Button>
|
||||
</Alert>
|
||||
<EditorContent
|
||||
v-else
|
||||
class="input formula-input-field"
|
||||
:class="classes"
|
||||
:editor="editor"
|
||||
/>
|
||||
<div v-else>
|
||||
<EditorContent
|
||||
ref="editor"
|
||||
class="input formula-input-field"
|
||||
:class="classes"
|
||||
:editor="editor"
|
||||
@data-component-clicked="dataComponentClicked"
|
||||
/>
|
||||
<DataExplorer
|
||||
ref="dataExplorer"
|
||||
:nodes="nodes"
|
||||
:node-selected="nodeSelected"
|
||||
:loading="dataExplorerLoading"
|
||||
@node-selected="dataExplorerItemSelected"
|
||||
@node-toggled="editor.commands.focus()"
|
||||
></DataExplorer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -32,12 +43,20 @@ import { RuntimeFunctionCollection } from '@baserow/modules/core/functionCollect
|
|||
import { FromTipTapVisitor } from '@baserow/modules/core/formula/tiptap/fromTipTapVisitor'
|
||||
import { mergeAttributes } from '@tiptap/core'
|
||||
import { HardBreak } from '@tiptap/extension-hard-break'
|
||||
import DataExplorer from '@baserow/modules/core/components/dataExplorer/DataExplorer'
|
||||
import { onClickOutside } from '@baserow/modules/core/utils/dom'
|
||||
import { RuntimeGet } from '@baserow/modules/core/runtimeFormulaTypes'
|
||||
|
||||
export default {
|
||||
name: 'FormulaInputField',
|
||||
components: {
|
||||
DataExplorer,
|
||||
EditorContent,
|
||||
},
|
||||
provide() {
|
||||
// Provide the application context to all formula components
|
||||
return { applicationContext: this.applicationContext }
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
@ -47,6 +66,20 @@ export default {
|
|||
type: String,
|
||||
default: null,
|
||||
},
|
||||
dataProviders: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
dataExplorerLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
applicationContext: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -54,6 +87,7 @@ export default {
|
|||
content: null,
|
||||
isFocused: false,
|
||||
isFormulaInvalid: false,
|
||||
dataNodeSelected: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -113,6 +147,14 @@ export default {
|
|||
wrapperContent() {
|
||||
return this.editor.getJSON().content[0].content
|
||||
},
|
||||
nodes() {
|
||||
return this.dataProviders
|
||||
.map((dataProvider) => dataProvider.getNodes(this.applicationContext))
|
||||
.filter((dataProviderNodes) => dataProviderNodes.nodes?.length > 0)
|
||||
},
|
||||
nodeSelected() {
|
||||
return this.dataNodeSelected?.attrs?.path || null
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(value) {
|
||||
|
@ -142,12 +184,17 @@ export default {
|
|||
editable: true,
|
||||
onUpdate: this.onUpdate,
|
||||
onFocus: this.onFocus,
|
||||
onBlur: this.onBlur,
|
||||
extensions: this.extensions,
|
||||
parseOptions: {
|
||||
preserveWhitespace: 'full',
|
||||
},
|
||||
editorProps: {
|
||||
handleClick: this.unSelectNode,
|
||||
},
|
||||
})
|
||||
|
||||
const clickOutsideEventCancel = onClickOutside(this.$el, this.onBlur)
|
||||
this.$once('hook:beforeDestroy', clickOutsideEventCancel)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.editor?.destroy()
|
||||
|
@ -157,18 +204,38 @@ export default {
|
|||
this.isFormulaInvalid = false
|
||||
this.$emit('input', '')
|
||||
},
|
||||
onUpdate() {
|
||||
this.fixMultipleWrappers()
|
||||
|
||||
emitChange() {
|
||||
if (!this.isFormulaInvalid) {
|
||||
this.$emit('input', this.toFormula(this.wrapperContent))
|
||||
}
|
||||
},
|
||||
onFocus() {
|
||||
this.isFocused = true
|
||||
onUpdate() {
|
||||
this.fixMultipleWrappers()
|
||||
this.unSelectNode()
|
||||
this.emitChange()
|
||||
},
|
||||
onBlur() {
|
||||
this.isFocused = false
|
||||
onFocus() {
|
||||
if (!this.isFocused) {
|
||||
this.isFocused = true
|
||||
this.unSelectNode()
|
||||
this.$refs.dataExplorer.show(
|
||||
this.$refs.editor.$el,
|
||||
'bottom',
|
||||
'left',
|
||||
-100,
|
||||
-330
|
||||
)
|
||||
}
|
||||
},
|
||||
onBlur(target) {
|
||||
if (
|
||||
!this.$refs.dataExplorer.$el.contains(target) &&
|
||||
!this.$refs.editor.$el.contains(target)
|
||||
) {
|
||||
this.isFocused = false
|
||||
this.$refs.dataExplorer.hide()
|
||||
this.unSelectNode()
|
||||
}
|
||||
},
|
||||
toContent(formula) {
|
||||
if (_.isEmpty(formula)) {
|
||||
|
@ -215,6 +282,34 @@ export default {
|
|||
this.editor.commands.joinForward()
|
||||
}
|
||||
},
|
||||
dataComponentClicked(node) {
|
||||
this.selectNode(node)
|
||||
this.editor.commands.blur()
|
||||
},
|
||||
dataExplorerItemSelected({ path }) {
|
||||
const isInEditingMode = this.dataNodeSelected !== null
|
||||
if (isInEditingMode) {
|
||||
this.dataNodeSelected.attrs.path = path
|
||||
this.emitChange()
|
||||
} else {
|
||||
const getNode = new RuntimeGet().toNode([{ text: path }])
|
||||
this.editor.commands.insertContent(getNode)
|
||||
}
|
||||
this.editor.commands.focus()
|
||||
},
|
||||
selectNode(node) {
|
||||
if (node) {
|
||||
this.unSelectNode()
|
||||
this.dataNodeSelected = node
|
||||
this.dataNodeSelected.attrs.isSelected = true
|
||||
}
|
||||
},
|
||||
unSelectNode() {
|
||||
if (this.dataNodeSelected) {
|
||||
this.dataNodeSelected.attrs.isSelected = false
|
||||
this.dataNodeSelected = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -8,27 +8,23 @@
|
|||
{{ label }}
|
||||
</label>
|
||||
<div class="control__elements">
|
||||
<textarea
|
||||
<FormulaInputField
|
||||
:value="value"
|
||||
:rows="nbRows"
|
||||
:placeholder="placeholder"
|
||||
class="input paragraph-element-form__value"
|
||||
:class="{
|
||||
'input--error': hasError,
|
||||
}"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
@keydown.enter.stop
|
||||
@blur="$emit('blur', $event)"
|
||||
:data-providers="dataProviders"
|
||||
:data-explorer-loading="dataExplorerLoading"
|
||||
:application-context="{ page, builder, mode }"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasError" class="error">
|
||||
{{ error }}
|
||||
</div>
|
||||
</FormElement>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormulaInputField from '@baserow/modules/core/components/formula/FormulaInputField'
|
||||
export default {
|
||||
components: { FormulaInputField },
|
||||
inject: ['page', 'builder', 'mode'],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
|
@ -54,18 +50,15 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
dataProviders: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: '',
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasError() {
|
||||
return Boolean(this.error)
|
||||
},
|
||||
nbRows() {
|
||||
return this.value.split(/\n/).length > 1 ? 12 : 1
|
||||
dataExplorerLoading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
<template>
|
||||
<NodeViewWrapper as="span" class="get-formula-component">
|
||||
{{ pathParts.dataProvider }}
|
||||
<template v-for="(part, index) in pathParts.parts">
|
||||
<i :key="index" class="get-formula-component__caret fas fa-angle-right">
|
||||
</i>
|
||||
{{ part }}
|
||||
</template>
|
||||
<a class="get-formula-component__remove" @click="deleteNode">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
<NodeViewWrapper
|
||||
as="span"
|
||||
class="get-formula-component"
|
||||
:class="{
|
||||
'get-formula-component--error': isInvalid,
|
||||
'get-formula-component--selected': isSelected,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-tooltip="$t('getFormulaComponent.errorTooltip')"
|
||||
tooltip-position="top"
|
||||
:hide-tooltip="!isInvalid"
|
||||
@click="emitToEditor('data-component-clicked', node)"
|
||||
>
|
||||
{{ pathParts.dataProvider }}
|
||||
<template v-for="(part, index) in pathParts.parts">
|
||||
<i :key="index" class="get-formula-component__caret fas fa-angle-right">
|
||||
</i>
|
||||
{{ part }}
|
||||
</template>
|
||||
<a class="get-formula-component__remove" @click="deleteNode">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
</template>
|
||||
|
||||
|
@ -23,10 +37,22 @@ export default {
|
|||
NodeViewWrapper,
|
||||
},
|
||||
mixins: [formulaComponent],
|
||||
inject: ['applicationContext'],
|
||||
computed: {
|
||||
availableData() {
|
||||
return Object.values(this.$registry.getAll('builderDataProvider')).map(
|
||||
(dataProvider) => dataProvider.getNodes(this.applicationContext)
|
||||
)
|
||||
},
|
||||
isInvalid() {
|
||||
return this.findNode(this.availableData, _.toPath(this.path)) === null
|
||||
},
|
||||
path() {
|
||||
return this.node.attrs.path
|
||||
},
|
||||
isSelected() {
|
||||
return this.node.attrs.isSelected
|
||||
},
|
||||
pathParts() {
|
||||
const [dataProvider, ...parts] = _.toPath(this.path)
|
||||
const dataProviderType = this.$registry.get(
|
||||
|
@ -36,9 +62,36 @@ export default {
|
|||
|
||||
return {
|
||||
dataProvider: dataProviderType.name,
|
||||
parts,
|
||||
parts: parts.map((part, index) =>
|
||||
dataProviderType.pathPartToDisplay(
|
||||
this.applicationContext,
|
||||
part,
|
||||
index + 1
|
||||
)
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
findNode(nodes, path) {
|
||||
const [identifier, ...rest] = path
|
||||
|
||||
if (!nodes) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nodeFound = nodes.find((node) => node.identifier === identifier)
|
||||
|
||||
if (!nodeFound) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (rest.length > 0) {
|
||||
return this.findNode(nodeFound.nodes, rest)
|
||||
}
|
||||
|
||||
return nodeFound
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -5,6 +5,14 @@ import { Registerable } from '@baserow/modules/core/registry'
|
|||
* the formula resolver.
|
||||
*/
|
||||
export class DataProviderType extends Registerable {
|
||||
DATA_TYPE_TO_ICON_MAP = {
|
||||
string: 'font',
|
||||
number: 'hashtag',
|
||||
boolean: 'check-square',
|
||||
}
|
||||
|
||||
UNKNOWN_DATA_TYPE_ICON = 'question'
|
||||
|
||||
get name() {
|
||||
throw new Error('`name` must be set on the dataProviderType.')
|
||||
}
|
||||
|
@ -23,24 +31,172 @@ export class DataProviderType extends Registerable {
|
|||
*/
|
||||
async init(r) {}
|
||||
|
||||
/**
|
||||
* Call init step of all given data providers according to the application context
|
||||
* @param {Array} dataProviders
|
||||
* @param {Object} applicationContext the application context.
|
||||
*/
|
||||
static async initAll(dataProviders, applicationContext) {
|
||||
// First we initialize providers that doesn't need a backend context
|
||||
await Promise.all(
|
||||
Object.values(dataProviders)
|
||||
.filter((provider) => !provider.needBackendContext)
|
||||
.map((dataProvider) => dataProvider.init(applicationContext))
|
||||
)
|
||||
// Then we initialize those that need the backend context
|
||||
await Promise.all(
|
||||
Object.values(dataProviders)
|
||||
.filter((provider) => provider.needBackendContext)
|
||||
.map((dataProvider) => dataProvider.init(applicationContext))
|
||||
)
|
||||
}
|
||||
|
||||
static getAllBackendContext(dataProviders, applicationContext) {
|
||||
return Object.fromEntries(
|
||||
Object.values(dataProviders).map((dataProvider) => {
|
||||
return [
|
||||
dataProvider.type,
|
||||
dataProvider.getBackendContext(applicationContext),
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the context needed to be send to the backend for each dataProvider
|
||||
* to be able to solve the formulas on the backend.
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @returns An object if the dataProvider wants to send something to the backend.
|
||||
*/
|
||||
getBackendContext(runtimeFormulaContext) {
|
||||
getBackendContext(applicationContext) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual data.
|
||||
* @param {object} runtimeFormulaContext the formula context instance.
|
||||
* Returns the actual data for the given path.
|
||||
* @param {object} applicationContext the application context.
|
||||
* @param {Array<str>} path the path of the data we want to get
|
||||
*/
|
||||
getDataChunk(runtimeFormulaContext, path) {
|
||||
getDataChunk(applicationContext, path) {
|
||||
throw new Error('.getDataChunk() must be set on the dataProviderType.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data content for this data provider.
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @returns {{$schema: string}}
|
||||
*/
|
||||
getDataContent(applicationContext) {
|
||||
throw new Error('.getDataContent() must be set on the dataProviderType.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the schema of the data provided by this data provider
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @returns {{$schema: string}}
|
||||
*/
|
||||
getDataSchema(applicationContext) {
|
||||
throw new Error('.getDataSchema() must be set on the dataProviderType.')
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns an object that can be read by the data explorer to display
|
||||
* the data available from each data provider.
|
||||
*
|
||||
* Make sure to implement `getDataContent` and `getDataSchema` for every data provider
|
||||
* if they should show data in the data explorer.
|
||||
*
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @returns {{identifier: string, name: string, nodes: []}}
|
||||
*/
|
||||
getNodes(applicationContext) {
|
||||
const content = this.getDataContent(applicationContext)
|
||||
const schema = this.getDataSchema(applicationContext)
|
||||
|
||||
return this._toNode(applicationContext, this.type, content, schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive method to deeply compute the node tree for this data providers.
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @param {string} identifier the identifier for the current node.
|
||||
* @param {*} content the current node content.
|
||||
* @param {$schema: string} schema the current node schema.
|
||||
* @param {int} level the level of the current node in the data hierarchy.
|
||||
* @returns {{identifier: string, name: string, nodes: []}}
|
||||
*/
|
||||
_toNode(applicationContext, identifier, content, schema, level = 0) {
|
||||
const name = this.pathPartToDisplay(applicationContext, identifier, level)
|
||||
if (schema.type === 'array') {
|
||||
return {
|
||||
name,
|
||||
identifier,
|
||||
icon: this.getIconForType(schema.type),
|
||||
nodes: content.map((item, index) =>
|
||||
this._toNode(
|
||||
applicationContext,
|
||||
`${index}`,
|
||||
item,
|
||||
schema.items,
|
||||
level + 1
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.type === 'object') {
|
||||
return {
|
||||
name,
|
||||
identifier,
|
||||
icon: this.getIconForType(schema.type),
|
||||
nodes: Object.entries(schema.properties).map(
|
||||
([identifier, subSchema]) =>
|
||||
this._toNode(
|
||||
applicationContext,
|
||||
identifier,
|
||||
content[identifier],
|
||||
subSchema,
|
||||
level + 1
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
type: schema.type,
|
||||
icon: this.getIconForType(schema.type),
|
||||
value: content,
|
||||
identifier,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function gives you the icon that can be used by the data explorer given
|
||||
* the type of the data.
|
||||
* @param type - The type of the data
|
||||
* @returns {*|string} - The corresponding icon
|
||||
*/
|
||||
getIconForType(type) {
|
||||
return this.DATA_TYPE_TO_ICON_MAP[type] || this.UNKNOWN_DATA_TYPE_ICON
|
||||
}
|
||||
|
||||
/**
|
||||
* This function lets you hook into the path translation. Sometimes the path uses an
|
||||
* ID to reference an item, but we want to show the name of the item to the user
|
||||
* instead.
|
||||
* @param {Object} applicationContext the application context.
|
||||
* @param {String} part - raw path part as used by the formula
|
||||
* @param {String} position - index of the part in the path
|
||||
* @returns {Array<String>} - modified path part as it should be displayed to the user
|
||||
*/
|
||||
pathPartToDisplay(applicationContext, part, position) {
|
||||
if (position === 0) {
|
||||
return this.name
|
||||
}
|
||||
return part
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 0
|
||||
}
|
||||
|
|
|
@ -24,6 +24,11 @@ export default {
|
|||
}
|
||||
el.tooltipMouseEnterEvent = () => {
|
||||
const position = el.getAttribute('tooltip-position') || 'bottom'
|
||||
const hide = el.getAttribute('hide-tooltip')
|
||||
|
||||
if (hide) {
|
||||
return
|
||||
}
|
||||
|
||||
if (el.tooltipElement) {
|
||||
this.terminate(el)
|
||||
|
|
|
@ -567,5 +567,8 @@
|
|||
},
|
||||
"formulaInputField": {
|
||||
"errorInvalidFormula": "The formula is invalid."
|
||||
},
|
||||
"dataExplorer": {
|
||||
"emptyText": "No data found"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,29 +23,6 @@ export class RuntimeFormulaContext {
|
|||
this.applicationContext = applicationContext
|
||||
}
|
||||
|
||||
async initAll() {
|
||||
// First we initialize providers that doesn't need a backend context
|
||||
await Promise.all(
|
||||
Object.values(this.dataProviders)
|
||||
.filter((provider) => !provider.needBackendContext)
|
||||
.map((dataProvider) => dataProvider.init(this))
|
||||
)
|
||||
// Then we initialize those that need the backend context
|
||||
await Promise.all(
|
||||
Object.values(this.dataProviders)
|
||||
.filter((provider) => provider.needBackendContext)
|
||||
.map((dataProvider) => dataProvider.init(this))
|
||||
)
|
||||
}
|
||||
|
||||
getAllBackendContext() {
|
||||
return Object.fromEntries(
|
||||
Object.values(this.dataProviders).map((dataProvider) => {
|
||||
return [dataProvider.type, dataProvider.getBackendContext(this)]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for the given path. The first part of the path is
|
||||
* the data provider type, then the remaining parts are given to the data provider.
|
||||
|
@ -62,7 +39,7 @@ export class RuntimeFormulaContext {
|
|||
}
|
||||
|
||||
try {
|
||||
return dataProviderType.getDataChunk(this, rest)
|
||||
return dataProviderType.getDataChunk(this.applicationContext, rest)
|
||||
} catch (e) {
|
||||
throw new UnresolvablePathError(dataProviderType.type, rest.join('.'))
|
||||
}
|
||||
|
|
|
@ -180,6 +180,9 @@ export class RuntimeGet extends RuntimeFormulaFunction {
|
|||
path: {
|
||||
default: '',
|
||||
},
|
||||
isSelected: {
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
parseHTML() {
|
||||
|
@ -205,6 +208,7 @@ export class RuntimeGet extends RuntimeFormulaFunction {
|
|||
const specificConfiguration = {
|
||||
attrs: {
|
||||
path: textNode.text,
|
||||
isSelected: false,
|
||||
},
|
||||
}
|
||||
return _.merge(specificConfiguration, defaultConfiguration)
|
||||
|
|
|
@ -24,16 +24,12 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="col col-4">
|
||||
<FormulaInputGroup
|
||||
<ApplicationBuilderFormulaInputGroup
|
||||
v-model="values.row_id"
|
||||
small-label
|
||||
:label="$t('localBaserowGetRowForm.rowFieldLabel')"
|
||||
:placeholder="$t('localBaserowGetRowForm.rowFieldPlaceHolder')"
|
||||
:error="
|
||||
!$v.values.row_id.validFormula
|
||||
? $t('localBaserowGetRowForm.invalidFormula')
|
||||
: ''
|
||||
"
|
||||
:data-providers-allowed="DATA_PROVIDERS_ALLOWED_DATA_SOURCES"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,11 +54,11 @@
|
|||
|
||||
<script>
|
||||
import form from '@baserow/modules/core/mixins/form'
|
||||
import FormulaInputGroup from '@baserow/modules/core/components/formula/FormulaInputGroup'
|
||||
import { isValidFormula } from '@baserow/modules/core/formula'
|
||||
import { DATA_PROVIDERS_ALLOWED_DATA_SOURCES } from '@baserow/modules/builder/enums'
|
||||
import ApplicationBuilderFormulaInputGroup from '@baserow/modules/builder/components/ApplicationBuilderFormulaInputGroup'
|
||||
|
||||
export default {
|
||||
components: { FormulaInputGroup },
|
||||
components: { ApplicationBuilderFormulaInputGroup },
|
||||
mixins: [form],
|
||||
props: {
|
||||
builder: {
|
||||
|
@ -81,12 +77,9 @@ export default {
|
|||
},
|
||||
}
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
values: {
|
||||
row_id: { validFormula: isValidFormula },
|
||||
},
|
||||
}
|
||||
computed: {
|
||||
DATA_PROVIDERS_ALLOWED_DATA_SOURCES: () =>
|
||||
DATA_PROVIDERS_ALLOWED_DATA_SOURCES,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"cookie-universal-nuxt": "2.2.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"flush-promises": "^1.0.2",
|
||||
"generate-schema": "^2.6.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "13.0.1",
|
||||
|
|
|
@ -4096,7 +4096,7 @@ combined-stream@^1.0.8:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@^2.19.0, commander@^2.20.0:
|
||||
commander@^2.19.0, commander@^2.20.0, commander@^2.9.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
@ -5958,6 +5958,14 @@ gaze@^1.0.0:
|
|||
dependencies:
|
||||
globule "^1.0.0"
|
||||
|
||||
generate-schema@^2.6.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/generate-schema/-/generate-schema-2.6.0.tgz#9ac037550fd4243783a9f7681d39bee8870bcec2"
|
||||
integrity sha512-EUBKfJNzT8f91xUk5X5gKtnbdejZeE065UAJ3BCzE8VEbvwKI9Pm5jaWmqVeK1MYc1g5weAVFDTSJzN7ymtTqA==
|
||||
dependencies:
|
||||
commander "^2.9.0"
|
||||
type-of-is "^3.4.0"
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
|
@ -8758,11 +8766,6 @@ num2fraction@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
|
||||
integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==
|
||||
|
||||
nuxt-env@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/nuxt-env/-/nuxt-env-0.1.0.tgz#8ac50b9ff45391ad3044ea932cbd05f06a585f87"
|
||||
integrity sha512-7mTao3qG0zfN0hahk3O6SuDy0KEwYmNojammWQsMwhqMn3aUjX4nMYnWDa0pua+2/rwAY9oG53jQtLgJdG7f9w==
|
||||
|
||||
nuxt@2.16.3:
|
||||
version "2.16.3"
|
||||
resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-2.16.3.tgz#d0338ab9145bc60aa3d927f03d20e439b9adddf4"
|
||||
|
@ -11842,6 +11845,11 @@ type-is@^1.6.18:
|
|||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
type-of-is@^3.4.0:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/type-of-is/-/type-of-is-3.5.1.tgz#eec2fc89b828dbf9900eb6416eee30f4fe0fcd31"
|
||||
integrity sha512-SOnx8xygcAh8lvDU2exnK2bomASfNjzB3Qz71s2tw9QnX8fkAo7aC+D0H7FV0HjRKj94CKV2Hi71kVkkO6nOxg==
|
||||
|
||||
typed-array-length@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
|
||||
|
|
Loading…
Add table
Reference in a new issue