mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-21 23:37:55 +00:00
632 lines
19 KiB
JavaScript
632 lines
19 KiB
JavaScript
import BigNumber from 'bignumber.js'
|
|
|
|
import ElementService from '@baserow/modules/builder/services/element'
|
|
import PublicBuilderService from '@baserow/modules/builder/services/publishedBuilder'
|
|
import { calculateTempOrder } from '@baserow/modules/core/utils/order'
|
|
|
|
const populateElement = (element, registry) => {
|
|
const elementType = registry.get('element', element.type)
|
|
element._ = {
|
|
contentLoading: true,
|
|
content: [],
|
|
hasNextPage: false,
|
|
reset: 0,
|
|
shouldBeFocused: false,
|
|
elementNamespacePath: null,
|
|
...elementType.getPopulateStoreProperties(),
|
|
}
|
|
|
|
return element
|
|
}
|
|
|
|
const state = {
|
|
// The currently selected element
|
|
selected: null,
|
|
}
|
|
|
|
const updateContext = {
|
|
updateTimeout: null,
|
|
promiseResolve: null,
|
|
lastUpdatedValues: null,
|
|
valuesToUpdate: {},
|
|
moveTimeout: null,
|
|
}
|
|
|
|
/**
|
|
* As the store data come first from the SSR generated version, we don't have the
|
|
* BigNumber anymore so we need to make sure we use BigNumber when we compare things.
|
|
* @param {Object} element
|
|
* @returns a BigNumber object or null if the element or the order is missing.
|
|
*/
|
|
const getOrder = (element) => {
|
|
return element?.order ? new BigNumber(element.order) : null
|
|
}
|
|
|
|
/** Recursively order the elements from up to down and left to right */
|
|
const orderElements = (elements, parentElementId = null) => {
|
|
return elements
|
|
.filter(
|
|
({ parent_element_id: curentParentElementId }) =>
|
|
curentParentElementId === parentElementId
|
|
)
|
|
.sort((a, b) => {
|
|
if (a.place_in_container !== b.place_in_container) {
|
|
return a.place_in_container > b.place_in_container ? 1 : -1
|
|
}
|
|
|
|
return getOrder(a).gt(getOrder(b)) ? 1 : -1
|
|
})
|
|
.map((element) => [element, ...orderElements(elements, element.id)])
|
|
.flat()
|
|
}
|
|
|
|
const updateCachedValues = (page) => {
|
|
page.orderedElements = orderElements(page.elements)
|
|
page.elementMap = Object.fromEntries(
|
|
page.elements.map((element) => [`${element.id}`, element])
|
|
)
|
|
}
|
|
|
|
const mutations = {
|
|
SET_ITEMS(state, { page, elements }) {
|
|
state.selected = null
|
|
page.elements = elements.map((element) =>
|
|
populateElement(element, this.$registry)
|
|
)
|
|
updateCachedValues(page)
|
|
},
|
|
ADD_ITEM(state, { page, element, beforeId = null }) {
|
|
page.elements.push(populateElement(element, this.$registry))
|
|
updateCachedValues(page)
|
|
},
|
|
UPDATE_ITEM(state, { page, element: elementToUpdate, values }) {
|
|
let updateCached = false
|
|
page.elements.forEach((element) => {
|
|
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)
|
|
}
|
|
})
|
|
if (state.selected?.id === elementToUpdate.id) {
|
|
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)
|
|
}
|
|
},
|
|
DELETE_ITEM(state, { page, elementId }) {
|
|
const index = page.elements.findIndex((element) => element.id === elementId)
|
|
if (index > -1) {
|
|
page.elements.splice(index, 1)
|
|
}
|
|
updateCachedValues(page)
|
|
},
|
|
MOVE_ITEM(state, { page, index, oldIndex }) {
|
|
page.elements.splice(index, 0, page.elements.splice(oldIndex, 1)[0])
|
|
},
|
|
SELECT_ITEM(state, { element }) {
|
|
state.selected = element
|
|
},
|
|
CLEAR_ITEMS(state, { page }) {
|
|
page.elements = []
|
|
updateCachedValues(page)
|
|
},
|
|
_SET_ELEMENT_NAMESPACE_PATH(state, { element, path }) {
|
|
element._.elementNamespacePath = path
|
|
},
|
|
SET_REPEAT_ELEMENT_COLLAPSED(state, { element, collapsed }) {
|
|
element._.collapsed = collapsed
|
|
},
|
|
}
|
|
|
|
const actions = {
|
|
clearAll({ commit }, { page }) {
|
|
commit('CLEAR_ITEMS', { page })
|
|
},
|
|
forceCreate({ dispatch, commit }, { page, element }) {
|
|
commit('ADD_ITEM', { page, element })
|
|
dispatch('_setElementNamespacePath', { page, element })
|
|
|
|
const elementType = this.$registry.get('element', element.type)
|
|
elementType.afterCreate(element, page)
|
|
},
|
|
forceUpdate({ commit }, { page, element, values }) {
|
|
commit('UPDATE_ITEM', { page, element, values })
|
|
const elementType = this.$registry.get('element', element.type)
|
|
elementType.afterUpdate(element, page)
|
|
},
|
|
forceDelete({ commit, getters }, { page, elementId }) {
|
|
const elementToDelete = getters.getElementById(page, elementId)
|
|
|
|
if (getters.getSelected?.id === elementId) {
|
|
commit('SELECT_ITEM', { element: null })
|
|
}
|
|
commit('DELETE_ITEM', { page, elementId })
|
|
|
|
const elementType = this.$registry.get('element', elementToDelete.type)
|
|
elementType.afterDelete(elementToDelete, page)
|
|
},
|
|
forceMove(
|
|
{ commit, getters },
|
|
{ page, elementId, beforeElementId, parentElementId, placeInContainer }
|
|
) {
|
|
const element = getters.getElementById(page, elementId)
|
|
|
|
let tempOrder = ''
|
|
// Compute temporary order while waiting for the update from the server
|
|
if (beforeElementId) {
|
|
// If we have a before element then we should place the element
|
|
// between the before element and the element before the before element.
|
|
const beforeElement = getters.getElementById(page, beforeElementId)
|
|
const beforeBeforeElement = getters.getPreviousElement(
|
|
page,
|
|
beforeElement
|
|
)
|
|
const afterOrder = getOrder(beforeElement)
|
|
const beforeOrder = getOrder(beforeBeforeElement)
|
|
tempOrder = calculateTempOrder(beforeOrder, afterOrder)
|
|
} else {
|
|
// Otherwise it's should be placed as the last in the column so we get the last
|
|
// element and we just add one.
|
|
const lastElement = getters
|
|
.getElementsInPlace(page, parentElementId, placeInContainer)
|
|
.at(-1)
|
|
tempOrder = calculateTempOrder(getOrder(lastElement), null)
|
|
}
|
|
|
|
commit('UPDATE_ITEM', {
|
|
page,
|
|
element,
|
|
values: {
|
|
order: tempOrder,
|
|
parent_element_id: parentElementId,
|
|
place_in_container: placeInContainer,
|
|
},
|
|
})
|
|
},
|
|
select({ commit }, { element }) {
|
|
updateContext.lastUpdatedValues = null
|
|
commit('SELECT_ITEM', { element })
|
|
},
|
|
async create(
|
|
{ dispatch },
|
|
{
|
|
page,
|
|
elementType: elementTypeName,
|
|
beforeId = null,
|
|
configuration = null,
|
|
forceCreate = true,
|
|
}
|
|
) {
|
|
const elementType = this.$registry.get('element', elementTypeName)
|
|
const { data: element } = await ElementService(this.$client).create(
|
|
page.id,
|
|
elementTypeName,
|
|
beforeId,
|
|
elementType.getDefaultValues(page, configuration)
|
|
)
|
|
|
|
if (forceCreate) {
|
|
await dispatch('forceCreate', { page, element })
|
|
await dispatch('select', { element })
|
|
}
|
|
|
|
return element
|
|
},
|
|
async update({ dispatch }, { page, element, values }) {
|
|
const oldValues = {}
|
|
const newValues = {}
|
|
Object.keys(values).forEach((name) => {
|
|
if (Object.prototype.hasOwnProperty.call(element, name)) {
|
|
oldValues[name] = element[name]
|
|
newValues[name] = values[name]
|
|
}
|
|
})
|
|
|
|
await dispatch('forceUpdate', { page, element, values: newValues })
|
|
|
|
try {
|
|
await ElementService(this.$client).update(element.id, values)
|
|
} catch (error) {
|
|
await dispatch('forceUpdate', { page, element, values: oldValues })
|
|
throw error
|
|
}
|
|
},
|
|
|
|
async debouncedUpdateSelected({ dispatch, getters }, { page, values }) {
|
|
const element = getters.getSelected
|
|
|
|
const oldValues = {}
|
|
Object.keys(values).forEach((name) => {
|
|
if (Object.prototype.hasOwnProperty.call(element, name)) {
|
|
oldValues[name] = element[name]
|
|
// Accumulate the changed values to send all the ongoing changes with the
|
|
// final request
|
|
updateContext.valuesToUpdate[name] = values[name]
|
|
}
|
|
})
|
|
|
|
await dispatch('forceUpdate', {
|
|
page,
|
|
element,
|
|
values: updateContext.valuesToUpdate,
|
|
})
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const fire = async () => {
|
|
const toUpdate = updateContext.valuesToUpdate
|
|
updateContext.valuesToUpdate = {}
|
|
try {
|
|
await ElementService(this.$client).update(element.id, toUpdate)
|
|
updateContext.lastUpdatedValues = null
|
|
resolve()
|
|
} catch (error) {
|
|
// Revert to old values on error
|
|
await dispatch('forceUpdate', {
|
|
page,
|
|
element,
|
|
values: updateContext.lastUpdatedValues,
|
|
})
|
|
updateContext.lastUpdatedValues = null
|
|
reject(error)
|
|
}
|
|
}
|
|
|
|
if (updateContext.promiseResolve) {
|
|
updateContext.promiseResolve()
|
|
updateContext.promiseResolve = null
|
|
}
|
|
|
|
clearTimeout(updateContext.updateTimeout)
|
|
|
|
if (!updateContext.lastUpdatedValues) {
|
|
updateContext.lastUpdatedValues = oldValues
|
|
}
|
|
|
|
updateContext.updateTimeout = setTimeout(fire, 500)
|
|
updateContext.promiseResolve = resolve
|
|
})
|
|
},
|
|
async delete({ dispatch, getters }, { page, elementId }) {
|
|
const elementToDelete = getters.getElementById(page, elementId)
|
|
const descendants = getters.getDescendants(page, elementToDelete)
|
|
|
|
// First delete all children
|
|
await Promise.all(
|
|
descendants.map((descendant) =>
|
|
dispatch('forceDelete', { page, elementId: descendant.id })
|
|
)
|
|
)
|
|
|
|
await dispatch('forceDelete', { page, elementId })
|
|
|
|
try {
|
|
await ElementService(this.$client).delete(elementId)
|
|
} catch (error) {
|
|
await dispatch('forceCreate', {
|
|
page,
|
|
element: elementToDelete,
|
|
})
|
|
// Then restore all children
|
|
await Promise.all(
|
|
descendants.map((descendant) =>
|
|
dispatch('forceCreate', { page, element: descendant })
|
|
)
|
|
)
|
|
throw error
|
|
}
|
|
},
|
|
async fetch({ dispatch, commit }, { page }) {
|
|
const { data: elements } = await ElementService(this.$client).fetchAll(
|
|
page.id
|
|
)
|
|
|
|
commit('SET_ITEMS', { page, elements })
|
|
|
|
// Set the element namespace path of all elements we've fetched.
|
|
await Promise.all(
|
|
elements.map((element) =>
|
|
dispatch('_setElementNamespacePath', { page, element })
|
|
)
|
|
)
|
|
|
|
return elements
|
|
},
|
|
async fetchPublished({ dispatch, commit }, { page }) {
|
|
const { data: elements } = await PublicBuilderService(
|
|
this.$client
|
|
).fetchElements(page)
|
|
|
|
commit('SET_ITEMS', { page, elements })
|
|
|
|
// Set the element namespace ath of all published elements we've fetched.
|
|
await Promise.all(
|
|
elements.map((element) =>
|
|
dispatch('_setElementNamespacePath', { page, element })
|
|
)
|
|
)
|
|
|
|
return elements
|
|
},
|
|
async move(
|
|
{ commit, dispatch, getters },
|
|
{
|
|
page,
|
|
elementId,
|
|
beforeElementId,
|
|
parentElementId = null,
|
|
placeInContainer = null,
|
|
}
|
|
) {
|
|
const element = getters.getElementById(page, elementId)
|
|
const { order: previousOrder, place_in_container: previousPlace } = element
|
|
|
|
await dispatch('forceMove', {
|
|
page,
|
|
elementId,
|
|
beforeElementId,
|
|
parentElementId,
|
|
placeInContainer,
|
|
})
|
|
|
|
const fire = async () => {
|
|
try {
|
|
const { data: elementUpdated } = await ElementService(
|
|
this.$client
|
|
).move(elementId, beforeElementId, parentElementId, placeInContainer)
|
|
|
|
dispatch('forceUpdate', {
|
|
page,
|
|
element: elementUpdated,
|
|
values: { ...elementUpdated },
|
|
})
|
|
} catch (error) {
|
|
// Restore previous order and place_in_container properties
|
|
await dispatch('forceUpdate', {
|
|
page,
|
|
element,
|
|
values: { order: previousOrder, place_in_container: previousPlace },
|
|
})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
clearTimeout(updateContext.moveTimeout)
|
|
updateContext.moveTimeout = setTimeout(fire, 1000)
|
|
},
|
|
async duplicate({ commit, dispatch, getters }, { page, elementId }) {
|
|
const {
|
|
data: { elements, workflow_actions: workflowActions },
|
|
} = await ElementService(this.$client).duplicate(elementId)
|
|
|
|
const elementPromises = elements.map((element) =>
|
|
dispatch('forceCreate', { page, element })
|
|
)
|
|
const workflowActionPromises = workflowActions.map((workflowAction) =>
|
|
dispatch(
|
|
'workflowAction/forceCreate',
|
|
{ page, workflowAction },
|
|
{ root: true }
|
|
)
|
|
)
|
|
|
|
await Promise.all(elementPromises.concat(workflowActionPromises))
|
|
|
|
const elementToDuplicate = getters.getElementById(page, elementId)
|
|
const elementToSelect = elements.find(
|
|
({ parent_element_id: parentId }) =>
|
|
parentId === elementToDuplicate.parent_element_id
|
|
)
|
|
|
|
commit('SELECT_ITEM', { element: elementToSelect })
|
|
|
|
return elements
|
|
},
|
|
emitElementEvent({ getters }, { event, elements, ...rest }) {
|
|
elements.forEach((element) => {
|
|
const elementType = this.$registry.get('element', element.type)
|
|
elementType.onElementEvent(event, { element, ...rest })
|
|
})
|
|
},
|
|
async moveElement({ dispatch, getters }, { page, element, placement }) {
|
|
const elementType = this.$registry.get('element', element.type)
|
|
await elementType.moveElement(page, element, placement)
|
|
},
|
|
async selectNextElement({ dispatch, getters }, { page, element, placement }) {
|
|
const elementType = this.$registry.get('element', element.type)
|
|
await elementType.selectNextElement(page, element, placement)
|
|
},
|
|
_setElementNamespacePath({ commit, dispatch, getters }, { page, element }) {
|
|
const elementType = this.$registry.get('element', element.type)
|
|
const elementNamespacePath = elementType.getElementNamespacePath(
|
|
element,
|
|
page
|
|
)
|
|
commit('_SET_ELEMENT_NAMESPACE_PATH', {
|
|
element,
|
|
path: elementNamespacePath,
|
|
})
|
|
},
|
|
setRepeatElementCollapsed({ commit }, { element, collapsed }) {
|
|
commit('SET_REPEAT_ELEMENT_COLLAPSED', {
|
|
element,
|
|
collapsed,
|
|
})
|
|
},
|
|
}
|
|
|
|
const getters = {
|
|
getElementById: (state, getters) => (page, id) => {
|
|
if (id && Object.prototype.hasOwnProperty.call(page.elementMap, `${id}`)) {
|
|
return page.elementMap[`${id}`]
|
|
}
|
|
return null
|
|
},
|
|
getElementsOrdered: (state, getters) => (page) => {
|
|
return page.orderedElements
|
|
},
|
|
getRootElements: (state, getters) => (page) => {
|
|
return getters
|
|
.getElementsOrdered(page)
|
|
.filter((e) => e.parent_element_id === null)
|
|
},
|
|
getChildren: (state, getters) => (page, element) => {
|
|
return getters
|
|
.getElementsOrdered(page)
|
|
.filter((e) => e.parent_element_id === element.id)
|
|
},
|
|
getDescendants: (state, getters) => (page, element) => {
|
|
const getAllDescendants = (page, element) => {
|
|
const children = getters.getChildren(page, element)
|
|
if (children.length === 0) {
|
|
return []
|
|
} else {
|
|
return children.flatMap((child) => [
|
|
child,
|
|
...getAllDescendants(page, child),
|
|
])
|
|
}
|
|
}
|
|
return getAllDescendants(page, element)
|
|
},
|
|
getParent: (state, getters) => (page, element) => {
|
|
return getters.getElementById(page, element?.parent_element_id)
|
|
},
|
|
/**
|
|
* Given an element, return all its ancestors until we reach the root element.
|
|
* If `parentFirst` is `true` then we reverse the array of elements so that
|
|
* the element's immediate parent is first, otherwise the root element will be first.
|
|
*/
|
|
getAncestors:
|
|
(state, getters) =>
|
|
(
|
|
page,
|
|
element,
|
|
{ parentFirst = false, predicate = () => true, includeSelf = false } = {}
|
|
) => {
|
|
const getElementAncestors = (element) => {
|
|
const parentElement = getters.getParent(page, element)
|
|
if (parentElement) {
|
|
return [...getElementAncestors(parentElement), parentElement]
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
const ancestors = (
|
|
includeSelf
|
|
? [...getElementAncestors(element), element]
|
|
: getElementAncestors(element)
|
|
).filter(predicate)
|
|
return parentFirst ? ancestors.reverse() : ancestors
|
|
},
|
|
getSiblings: (state, getters) => (page, element) => {
|
|
return getters
|
|
.getElementsOrdered(page)
|
|
.filter((e) => e.parent_element_id === element.parent_element_id)
|
|
},
|
|
getElementPosition:
|
|
(state, getters) =>
|
|
(page, element, sameType = false) => {
|
|
const elements = getters.getElementsOrdered(page)
|
|
|
|
return (
|
|
(sameType
|
|
? elements.filter(({ type }) => type === element.type)
|
|
: elements
|
|
).findIndex(({ id }) => id === element.id) + 1
|
|
)
|
|
},
|
|
getElementsInPlace:
|
|
(state, getters) => (page, parentId, placeInContainer) => {
|
|
return getters
|
|
.getElementsOrdered(page)
|
|
.filter(
|
|
(e) =>
|
|
e.parent_element_id === parentId &&
|
|
e.place_in_container === placeInContainer
|
|
)
|
|
},
|
|
getPreviousElement: (state, getters) => (page, before) => {
|
|
const elementsInPlace = getters.getElementsInPlace(
|
|
page,
|
|
before.parent_element_id,
|
|
before.place_in_container
|
|
)
|
|
return before
|
|
? elementsInPlace
|
|
.reverse()
|
|
.find((e) => getOrder(e).lt(getOrder(before))) || null
|
|
: elementsInPlace.at(-1)
|
|
},
|
|
getNextElement: (state, getters) => (page, after) => {
|
|
const elementsInPlace = getters.getElementsInPlace(
|
|
page,
|
|
after.parent_element_id,
|
|
after.place_in_container
|
|
)
|
|
|
|
return elementsInPlace.find((e) => getOrder(e).gt(getOrder(after)))
|
|
},
|
|
getSelected(state) {
|
|
return state.selected
|
|
},
|
|
getElementNamespacePath: (state) => (element) => {
|
|
return element._.elementNamespacePath
|
|
},
|
|
/**
|
|
* Given an element, return its closest sibling element.
|
|
*/
|
|
getClosestSiblingElement: (state, getters) => (page, element) => {
|
|
if (!element) {
|
|
return null
|
|
}
|
|
|
|
const siblings = getters.getSiblings(page, element)
|
|
|
|
// Exclude the element itself from the list of siblings
|
|
const otherSiblings = siblings.filter((el) => el.id !== element.id)
|
|
|
|
// If the element has siblings, return the closest previous sibling.
|
|
// Default to the first (zeroth) sibling.
|
|
if (otherSiblings.length) {
|
|
const index = siblings.findIndex((el) => el.id === element.id)
|
|
const nextIndex = Math.max(index - 1, 0)
|
|
return otherSiblings[nextIndex]
|
|
}
|
|
|
|
// Return the container element if the element has a parent
|
|
if (element.parent_element_id) {
|
|
return getters.getElementById(page, element.parent_element_id)
|
|
}
|
|
|
|
// Find root element
|
|
const rootElements = getters
|
|
.getRootElements(page)
|
|
.filter((el) => el.id !== element.id)
|
|
if (rootElements.length) {
|
|
return rootElements[0]
|
|
}
|
|
|
|
return null
|
|
},
|
|
getRepeatElementCollapsed: (state) => (element) => {
|
|
return element._.collapsed
|
|
},
|
|
}
|
|
|
|
export default {
|
|
namespaced: true,
|
|
state,
|
|
getters,
|
|
actions,
|
|
mutations,
|
|
}
|