import { v1 as uuidv1 } from 'uuid' import { StoreItemLookupError } from '@baserow/modules/core/errors' import { uuid } from '@baserow/modules/core/utils/string' import { createFiltersTree, readDefaultViewIdFromCookie, saveDefaultViewIdInCookie, } from '@baserow/modules/database/utils/view' import ViewService from '@baserow/modules/database/services/view' import FilterService from '@baserow/modules/database/services/filter' import DecorationService from '@baserow/modules/database/services/decoration' import SortService from '@baserow/modules/database/services/sort' import GroupByService from '@baserow/modules/database/services/groupBy' import { clone } from '@baserow/modules/core/utils/object' import { DATABASE_ACTION_SCOPES } from '@baserow/modules/database/utils/undoRedoConstants' import { createNewUndoRedoActionGroupId } from '@baserow/modules/database/utils/action' export function populateFilter(filter) { filter._ = { hover: false, loading: false, } return filter } export function populateFilterGroup(filterGroup) { filterGroup._ = { hover: false, loading: false, } return filterGroup } export function populateSort(sort) { sort._ = { hover: false, loading: false, } return sort } export function populateGroupBy(groupBy) { groupBy._ = { hover: false, loading: false, width: null, } return groupBy } export function populateDecoration(decoration) { decoration._ = { loading: false } return decoration } export function populateView(view, registry) { const type = registry.get('view', view.type) view._ = view._ || { type: type.serialize(), selected: false, loading: false, focusFilter: null, } view.isShared = type.isShared(view) if (Object.prototype.hasOwnProperty.call(view, 'filters')) { view.filters.forEach((filter) => { populateFilter(filter) }) } else { view.filters = [] } if (Object.prototype.hasOwnProperty.call(view, 'filter_groups')) { view.filter_groups.forEach((filterGroup) => { populateFilterGroup(filterGroup) }) } else { view.filter_groups = [] } if (Object.prototype.hasOwnProperty.call(view, 'sortings')) { view.sortings.forEach((sort) => { populateSort(sort) }) } else { view.sortings = [] } if (Object.prototype.hasOwnProperty.call(view, 'group_bys')) { view.group_bys.forEach((groupBy) => { populateGroupBy(groupBy) }) } else { view.group_bys = [] } if (Object.prototype.hasOwnProperty.call(view, 'group_bys')) { view.group_bys.forEach((groupBy) => { populateGroupBy(groupBy) }) } else { view.group_bys = [] } if (Object.prototype.hasOwnProperty.call(view, 'decorations')) { view.decorations.forEach((decoration) => { populateDecoration(decoration) }) } else { view.decorations = [] } return type.populate(view) } export const state = () => ({ types: {}, loading: false, items: [], selected: {}, defaultViewId: null, }) export const mutations = { SET_ITEMS(state, applications) { state.items = applications }, SET_LOADING(state, value) { state.loading = value }, SET_ITEM_LOADING(state, { view, value }) { if (!Object.prototype.hasOwnProperty.call(view, '_')) { return } view._.loading = value }, ADD_ITEM(state, item) { if (!state.items.some((existingItem) => existingItem.id === item.id)) state.items = [...state.items, item].sort((a, b) => a.order - b.order) }, UPDATE_ITEM(state, { id, view, values, repopulate, readOnly }) { if (!readOnly) { const index = state.items.findIndex((item) => item.id === id) Object.assign(state.items[index], state.items[index], values) if (repopulate === true) { populateView(state.items[index], this.$registry) } } else { Object.assign(view, view, values) } }, ORDER_ITEMS(state, { ownershipType, order }) { if (ownershipType === undefined) { const firstView = state.items.find((item) => item.id === order[0]) ownershipType = firstView.ownership_type } const items = state.items.filter( (view) => view.ownership_type === ownershipType ) items.forEach((view) => { const index = order.findIndex((value) => value === view.id) view.order = index === -1 ? 0 : index + 1 }) }, DELETE_ITEM(state, id) { const index = state.items.findIndex((item) => item.id === id) state.items.splice(index, 1) }, SET_SELECTED(state, view) { Object.values(state.items).forEach((item) => { item._.selected = false }) view._.selected = true state.selected = view }, UNSELECT(state) { Object.values(state.items).forEach((item) => { item._.selected = false }) state.selected = {} }, ADD_FILTER(state, { view, filter }) { filter.view = view.id view.filters.push(filter) }, FINALIZE_FILTER(state, { view, oldId, id }) { const index = view.filters.findIndex((item) => item.id === oldId) if (index !== -1) { view.filters[index].id = id view.filters[index]._.loading = false } }, SET_FILTER_FOCUS(state, { view, filterId }) { view._.focusFilter = filterId }, DELETE_FILTER(state, { view, id }) { const index = view.filters.findIndex((item) => item.id === id) if (index !== -1) { view.filters.splice(index, 1) } }, ADD_FILTER_GROUP(state, { view, filterGroup }) { filterGroup.view = view.id view.filter_groups.push(filterGroup) }, FINALIZE_FILTER_GROUP(state, { view, oldId, id }) { const index = view.filter_groups.findIndex((item) => item.id === oldId) if (index !== -1) { view.filter_groups[index].id = id view.filter_groups[index]._.loading = false } }, UPDATE_FILTER_GROUP(state, { filterGroup, values }) { Object.assign(filterGroup, filterGroup, values) }, DELETE_FILTER_GROUP(state, { view, id }) { const index = view.filter_groups.findIndex((item) => item.id === id) if (index !== -1) { view.filter_groups.splice(index, 1) } }, DELETE_FIELD_FILTERS(state, { view, fieldId }) { for (let i = view.filters.length - 1; i >= 0; i--) { if (view.filters[i].field === fieldId) { view.filters.splice(i, 1) } } }, UPDATE_FILTER(state, { filter, values }) { Object.assign(filter, filter, values) }, SET_FILTER_LOADING(state, { filter, value }) { filter._.loading = value }, ADD_DECORATION(state, { view, decoration }) { view.decorations.push({ type: null, value_provider_type: null, value_provider_conf: null, ...decoration, }) }, FINALIZE_DECORATION(state, { view, oldId, id }) { const index = view.decorations.findIndex((item) => item.id === oldId) if (index !== -1) { view.decorations[index].id = id view.decorations[index]._.loading = false } }, DELETE_DECORATION(state, { view, id }) { const index = view.decorations.findIndex((item) => item.id === id) if (index !== -1) { view.decorations.splice(index, 1) } }, UPDATE_DECORATION(state, { decoration, values }) { Object.assign(decoration, decoration, values) }, SET_DECORATION_LOADING(state, { decoration, value }) { decoration._.loading = value }, ADD_SORT(state, { view, sort }) { view.sortings.push(sort) }, FINALIZE_SORT(state, { view, oldId, id }) { const index = view.sortings.findIndex((item) => item.id === oldId) if (index !== -1) { view.sortings[index].id = id view.sortings[index]._.loading = false } }, DELETE_SORT(state, { view, id }) { const index = view.sortings.findIndex((item) => item.id === id) if (index !== -1) { view.sortings.splice(index, 1) } }, DELETE_FIELD_SORTINGS(state, { view, fieldId }) { for (let i = view.sortings.length - 1; i >= 0; i--) { if (view.sortings[i].field === fieldId) { view.sortings.splice(i, 1) } } }, UPDATE_SORT(state, { sort, values }) { Object.assign(sort, sort, values) }, SET_SORT_LOADING(state, { sort, value }) { sort._.loading = value }, ADD_GROUP_BY(state, { view, groupBy }) { view.group_bys.push(groupBy) }, FINALIZE_GROUP_BY(state, { view, oldId, id }) { const index = view.group_bys.findIndex((item) => item.id === oldId) if (index !== -1) { view.group_bys[index].id = id view.group_bys[index]._.loading = false } }, DELETE_GROUP_BY(state, { view, id }) { const index = view.group_bys.findIndex((item) => item.id === id) if (index !== -1) { view.group_bys.splice(index, 1) } }, DELETE_FIELD_GROUP_BYS(state, { view, fieldId }) { for (let i = view.group_bys.length - 1; i >= 0; i--) { if (view.group_bys[i].field === fieldId) { view.group_bys.splice(i, 1) } } }, UPDATE_GROUP_BY(state, { groupBy, values }) { Object.assign(groupBy, groupBy, values) }, SET_GROUP_BY_LOADING(state, { groupBy, value }) { groupBy._.loading = value }, /** * Data for defaultViewId for Vuex store: * { * defaultViewId: view1Id, * } */ SET_DEFAULT_VIEW_ID(state, viewId) { state.defaultViewId = viewId }, } export const actions = { /** * Changes the loading state of a specific view. */ setItemLoading({ commit }, { view, value }) { commit('SET_ITEM_LOADING', { view, value }) }, /** * Fetches all the views of a given table. The is mostly called when the user * selects a different table. */ async fetchAll({ commit, getters, dispatch, state }, table) { commit('SET_LOADING', true) commit('UNSELECT', {}) try { const { data } = await ViewService(this.$client).fetchAll( table.id, true, true, true, true ) data.forEach((part, index, d) => { populateView(data[index], this.$registry) }) commit('SET_ITEMS', data) commit('SET_LOADING', false) // Get the default view for the table. const defaultViewId = readDefaultViewIdFromCookie(this.$cookies, table.id) if (defaultViewId !== null) { commit('SET_DEFAULT_VIEW_ID', defaultViewId) } } catch (error) { commit('SET_ITEMS', []) commit('SET_LOADING', false) throw error } }, /** * Creates a new view with the provided type for the given table. */ async create( { commit, getters, rootGetters, dispatch }, { type, table, values } ) { if (Object.prototype.hasOwnProperty.call(values, 'type')) { throw new Error( 'The key "type" is a reserved, but is already set on the ' + 'values when creating a new view.' ) } if (!this.$registry.exists('view', type)) { throw new Error(`A view with type "${type}" doesn't exist.`) } const postData = clone(values) postData.type = type const { data } = await ViewService(this.$client).create(table.id, postData) return await dispatch('forceCreate', { data }) }, /** * Forcefully create a new view without making a request to the server. */ forceCreate({ commit }, { data }) { populateView(data, this.$registry) commit('ADD_ITEM', data) return { view: data } }, /** * Updates the values of the view with the provided id. */ async update( { commit, dispatch }, { view, values, readOnly = false, refreshFromFetch = false, optimisticUpdate = true, } ) { commit('SET_ITEM_LOADING', { view, value: true }) const oldValues = {} const newValues = {} Object.keys(values).forEach((name) => { if (Object.prototype.hasOwnProperty.call(view, name)) { oldValues[name] = view[name] newValues[name] = values[name] } }) function updatePublicViewHasPassword() { // public_view_has_password needs to be updated after the api request // is finished and the modal closes. const viewHasPassword = Object.keys(values).includes( 'public_view_password' ) ? values.public_view_password !== '' : view.public_view_has_password // update the password protection toggle state accordingly dispatch('forceUpdate', { view, values: { public_view_has_password: viewHasPassword, }, }) } if (optimisticUpdate) { dispatch('forceUpdate', { view, values: newValues, repopulate: true, readOnly, }) } try { if (!readOnly) { dispatch( 'undoRedo/updateCurrentScopeSet', DATABASE_ACTION_SCOPES.view(view.id), { root: true, } ) // in some cases view may return extra data that were not present in values const newValues = ( await ViewService(this.$client).update(view.id, values) ).data if (refreshFromFetch || !optimisticUpdate) { dispatch('forceUpdate', { view, values: newValues, repopulate: true }) } updatePublicViewHasPassword() } commit('SET_ITEM_LOADING', { view, value: false }) } catch (error) { commit('SET_ITEM_LOADING', { view, value: false }) dispatch('forceUpdate', { view, values: oldValues }) throw error } }, /** * Updates the order of all the views in a table. */ async order({ commit, getters }, { table, ownershipType, order, oldOrder }) { commit('ORDER_ITEMS', { ownershipType, order }) try { await ViewService(this.$client).order(table.id, ownershipType, order) } catch (error) { commit('ORDER_ITEMS', { ownershipType, order: oldOrder }) throw error } }, /** * Forcefully update an existing view without making a request to the backend. */ forceUpdate( { commit }, { view, values, repopulate = false, readOnly = false } ) { commit('UPDATE_ITEM', { id: view.id, view, values, repopulate, readOnly, }) }, /** * Duplicates an existing view. */ async duplicate({ commit, dispatch }, view) { const { data } = await ViewService(this.$client).duplicate(view.id) await dispatch('forceCreate', { data }) return data }, /** * Deletes an existing view with the provided id. A request to the server is first * made and after that it will be deleted from the store. */ async delete({ commit, dispatch }, view) { try { await ViewService(this.$client).delete(view.id) dispatch('forceDelete', view) } catch (error) { // If the view to delete wasn't found we can just delete it from the // state. if (error.response && error.response.status === 404) { dispatch('forceDelete', view) } else { throw error } } }, /** * Removes the view from the this store without making a delete request to the server. */ forceDelete({ commit, dispatch, getters, rootGetters }, view) { // If the currently selected view is selected. if (view._.selected && view.id === getters.getSelectedId) { commit('UNSELECT') const route = this.$router.history.current const tableId = view.table.id // If the current route is the same table as the deleting view. if ( route.name === 'database-table' && parseInt(route.params.tableId) === tableId ) { // Check if there are any other views and figure out what the next selected // view should be. This is always the first one in the list. const otherViews = getters.getAll .filter((v) => view.id !== v.id) .sort((a, b) => a.order - b.order) const nextView = otherViews.length > 0 ? otherViews[0] : null if (nextView !== null) { // If there is a next view, we can redirect to that page. this.$router.replace({ params: { viewId: nextView.id } }) } else if (route.params.viewId) { // If there isn't a next view and the user was already viewing a view, we // need to redirect to the empty table page. this.$router.replace({ params: { viewId: null } }) } else { // If there isn't a next view and the user wasn't looking at a view, we need // to refresh to show an empty table page. Changing the view id to 0, // which never exists forces the table page to show empty. We have // to do it this way because we can't navigate to the page without view. this.$router.replace({ params: { viewId: '0' } }) } } } commit('DELETE_ITEM', view.id) }, /** * Select a view and fetch all the applications related to that view. Note that * only the views of the selected table are stored in this store. It might be * possible you need to select the table first. */ select({ commit, dispatch }, view) { commit('SET_SELECTED', view) commit('SET_DEFAULT_VIEW_ID', view.id) // Set the default view for the table. saveDefaultViewIdInCookie(this.$cookies, view, this.$config) dispatch( 'undoRedo/updateCurrentScopeSet', DATABASE_ACTION_SCOPES.view(view.id), { root: true, } ) return { view } }, /** * Unselect the currently selected view. */ unselect({ commit, dispatch }) { commit('UNSELECT', {}) dispatch( 'undoRedo/updateCurrentScopeSet', DATABASE_ACTION_SCOPES.view(null), { root: true, } ) }, /** * Selects a view by a given view id. Note that only the views of the selected * table are stored in this store. It might be possible you need to select the * table first. */ selectById({ dispatch, getters }, id) { const view = getters.get(id) if (view === undefined) { throw new StoreItemLookupError(`View with id ${id} is not found.`) } return dispatch('select', view) }, /** * Changes the loading state of a specific filter. */ setFilterLoading({ commit }, { filter, value }) { commit('SET_FILTER_LOADING', { filter, value }) }, /** * Focus a specific filter. */ setFocusFilter({ commit }, { view, filterId }) { commit('SET_FILTER_FOCUS', { view, filterId }) }, /** * Creates a new filter and adds it to the store right away. If the API call succeeds * the filter ID will be added, but if it fails it will be removed from the store. * It also create the filter group if it doesn't exist yet in the same optimistic * way, removing it if the API call fails. */ async createFilter( { commit }, { view, field, values, emitEvent = true, readOnly = false, filterGroupId = null, parentGroupId = null, } ) { // If the type is not provided we are going to choose the first available type. if (!Object.prototype.hasOwnProperty.call(values, 'type')) { const viewFilterTypes = this.$registry.getAll('viewFilter') const compatibleType = Object.values(viewFilterTypes).find( (viewFilterType) => { return viewFilterType.fieldIsCompatible(field) } ) if (compatibleType === undefined) { throw new Error( `No compatible filter type could be found for field' ${field.type}` ) } values.type = compatibleType.type } // If the value is not provided, then we use the default value related to the type. if (!Object.prototype.hasOwnProperty.call(values, 'value')) { const viewFilterType = this.$registry.get('viewFilter', values.type) values.value = viewFilterType.getDefaultValue(field) } // Some filter input components expect the preload values to exist, that's why we // need to add an empty object if it doesn't yet exist. They can all handle // empty preload_values. if (!Object.prototype.hasOwnProperty.call(values, 'preload_values')) { values.preload_values = {} } // If the filter group doesn't exist yet optimistically create it. // If we first create the filter group and only once that succeeds create the // filter itself, we can run into a situation where a user with a slow connection // will see an empty group first and the filter only after a while. This code // will optimistically create both the group and the filter to provide a smoother // experience. const createNewFilterGroup = filterGroupId && view.filter_groups.findIndex((group) => group.id === filterGroupId) === -1 const filterGroup = {} if (createNewFilterGroup) { populateFilterGroup(filterGroup) filterGroup.id = filterGroupId filterGroup._.loading = !readOnly filterGroup.filter_type = 'AND' filterGroup.parent_group = parentGroupId commit('ADD_FILTER_GROUP', { view, filterGroup }) } const filter = Object.assign({}, values) populateFilter(filter) filter.id = uuidv1() filter._.loading = !readOnly filter.group = filterGroupId values.group = filterGroupId commit('ADD_FILTER', { view, filter }) if (emitEvent) { this.$bus.$emit('view-filter-created', { view, filter }) } commit('SET_FILTER_FOCUS', { view, filterId: filter.id }) const undoRedoActionGroupId = createNewUndoRedoActionGroupId() if (!readOnly) { if (createNewFilterGroup) { // The group needs to be created first before we can create the filter // in the case we're trying to create a new filter in a new group. try { const { data } = await FilterService(this.$client).createGroup( view.id, parentGroupId, undoRedoActionGroupId ) commit('FINALIZE_FILTER_GROUP', { view, oldId: filterGroup.id, id: data.id, }) // update the group id with the created group id values.group = data.id commit('UPDATE_FILTER', { filter, values: { group: data.id } }) } catch (error) { commit('DELETE_FILTER_GROUP', { view, id: filterGroup.id }) commit('DELETE_FILTER', { view, id: filter.id }) throw error } } try { const { data } = await FilterService(this.$client).create( view.id, values, undoRedoActionGroupId ) commit('FINALIZE_FILTER', { view, oldId: filter.id, id: data.id }) } catch (error) { commit('DELETE_FILTER', { view, id: filter.id }) throw error } } return { filter } }, /** * Creates a new filter group and adds it to the store right away. If the API * call succeeds the filter group ID will be updated, but if it fails it will be * removed from the store. */ async createFilterGroup({ commit }, { view, readOnly = false }) { const filterGroup = {} populateFilterGroup(filterGroup) filterGroup.id = uuidv1() filterGroup._.loading = !readOnly filterGroup.filter_type = 'AND' commit('ADD_FILTER_GROUP', { view, filterGroup }) try { const { data } = await FilterService(this.$client).createGroup(view.id) commit('FINALIZE_FILTER_GROUP', { view, oldId: filterGroup.id, id: data.id, }) } catch (error) { commit('DELETE_FILTER_GROUP', { view, id: filterGroup.id }) throw error } return { filterGroup } }, /** * Forcefully create a new view filter group without making a request to the backend. */ forceCreateFilterGroup({ commit }, { view, values }) { const filterGroup = Object.assign({}, values) populateFilterGroup(filterGroup) commit('ADD_FILTER_GROUP', { view, filterGroup }) }, /** * Forcefully create a new view filter without making a request to the backend. */ forceCreateFilter({ commit }, { view, values }) { const filter = Object.assign({}, values) // clone the object populateFilter(filter) commit('ADD_FILTER', { view, filter }) }, /** * Updates the filter values in the store right away. If the API call fails the * changes will be undone. */ async updateFilter( { dispatch, commit }, { filter, values, readOnly = false } ) { commit('SET_FILTER_LOADING', { filter, value: true }) const oldValues = {} const newValues = {} Object.keys(values).forEach((name) => { if (Object.prototype.hasOwnProperty.call(filter, name)) { oldValues[name] = filter[name] newValues[name] = values[name] } }) // When updating a filter, the preload values must be cleared because they // might not match the filter anymore. newValues.preload_values = {} dispatch('forceUpdateFilter', { filter, values: newValues }) try { if (!readOnly) { await FilterService(this.$client).update(filter.id, values) } commit('SET_FILTER_LOADING', { filter, value: false }) } catch (error) { dispatch('forceUpdateFilter', { filter, values: oldValues }) commit('SET_FILTER_LOADING', { filter, value: false }) throw error } }, /** * */ async updateFilterGroup( { dispatch }, { filterGroup, values, readOnly = false } ) { const oldValues = {} const newValues = {} Object.keys(values).forEach((name) => { if (Object.prototype.hasOwnProperty.call(filterGroup, name)) { oldValues[name] = filterGroup[name] newValues[name] = values[name] } }) dispatch('forceUpdateFilterGroup', { filterGroup, values: newValues, }) try { if (!readOnly) { await FilterService(this.$client).updateGroup(filterGroup.id, values) } } catch (error) { dispatch('forceUpdateFilterGroup', { filterGroup, values: oldValues, }) throw error } }, /** * Forcefully update an existing view filter group without making a request to the backend. */ forceUpdateFilterGroup({ commit }, { filterGroup, values }) { commit('UPDATE_FILTER_GROUP', { filterGroup, values }) }, /** * Forcefully update an existing view filter without making a request to the backend. */ forceUpdateFilter({ commit }, { filter, values }) { commit('UPDATE_FILTER', { filter, values }) }, /** * Deletes an existing filter. A request to the server will be made first and * after that it will be deleted. */ async deleteFilter({ dispatch, commit }, { view, filter, readOnly = false }) { commit('SET_FILTER_LOADING', { filter, value: true }) try { if (!readOnly) { await FilterService(this.$client).delete(filter.id) } dispatch('forceDeleteFilter', { view, filter }) } catch (error) { commit('SET_FILTER_LOADING', { filter, value: false }) throw error } }, /** * Forcefully delete an existing filter without making a request to the backend. */ forceDeleteFilter({ commit }, { view, filter }) { commit('DELETE_FILTER', { view, id: filter.id }) }, /** * Deletes an existing filter. A request to the server will be made first and * after that it will be deleted. */ async deleteFilterGroup( { dispatch, commit }, { view, filterGroup, readOnly = false } ) { const filters = view.filters.filter((f) => f.group === filterGroup.id) for (const filter of filters) { commit('SET_FILTER_LOADING', { filter, value: true }) } try { if (!readOnly) { await FilterService(this.$client).deleteGroup(filterGroup.id) } dispatch('forceDeleteFilterGroup', { view, filterGroup, }) } catch (error) { for (const filter of filters) { commit('SET_FILTER_LOADING', { filter, value: false }) } throw error } }, /** * Forcefully delete an existing filter group without making a request to the backend. * This function will also delete all the filters that are part of the group and all * the child groups and filters. */ forceDeleteFilterGroup({ commit }, { view, filterGroup }) { const filtersTree = createFiltersTree( view.filter_type, view.filters, view.filter_groups ) const groupNode = filtersTree.findNodeByGroupId(filterGroup.id) if (groupNode === null) { return } const deleteFromNode = (node) => { for (const child in node.children) { deleteFromNode(node.children[child]) } for (const filter of node.filters) { commit('DELETE_FILTER', { view, id: filter.id }) } commit('DELETE_FILTER_GROUP', { view, id: node.groupId }) } deleteFromNode(groupNode) }, /** * When a field is deleted the related filters are also automatically deleted in the * backend so they need to be removed here. */ deleteFieldFilters({ commit, getters }, { field }) { getters.getAll.forEach((view) => { commit('DELETE_FIELD_FILTERS', { view, fieldId: field.id }) }) }, /** * Creates a new decoration and adds it to the store right away. If the API call succeeds * the decorator ID will be updatede, but if it fails it will be removed from the store. */ async createDecoration({ commit }, { view, values, readOnly = false }) { const decoration = { ...values } populateDecoration(decoration) decoration.id = uuid() decoration._.loading = !readOnly commit('ADD_DECORATION', { view, decoration }) try { if (!readOnly) { const { data } = await DecorationService(this.$client).create( view.id, values ) commit('FINALIZE_DECORATION', { view, oldId: decoration.id, id: data.id, }) } } catch (error) { commit('DELETE_DECORATION', { view, id: decoration.id }) throw error } return { decoration } }, /** * Forcefully create a new view decoration without making a request to the backend. */ forceCreateDecoration({ commit }, { view, values }) { const decoration = { ...values } populateDecoration(decoration) commit('ADD_DECORATION', { view, decoration }) }, /** * Updates the decoration values in the store right away. If the API call fails the * changes will be undone. */ async updateDecoration( { dispatch, commit }, { decoration, values, readOnly = false } ) { commit('SET_DECORATION_LOADING', { decoration, value: true }) const oldValues = {} const newValues = {} Object.keys(values).forEach((name) => { if (Object.prototype.hasOwnProperty.call(decoration, name)) { oldValues[name] = decoration[name] newValues[name] = values[name] } }) dispatch('forceUpdateDecoration', { decoration, values: newValues }) try { if (!readOnly) { await DecorationService(this.$client).update(decoration.id, values) } commit('SET_DECORATION_LOADING', { decoration, value: false }) } catch (error) { dispatch('forceUpdateDecoration', { decoration, values: oldValues }) commit('SET_DECORATION_LOADING', { decoration, value: false }) throw error } }, /** * Forcefully update an existing view decoration without making a request to the * backend. */ forceUpdateDecoration({ commit }, { decoration, values }) { commit('UPDATE_DECORATION', { decoration, values }) }, /** * Deletes an existing decoration. A request to the server will be made first and * after that it will be deleted. */ async deleteDecoration( { dispatch, commit }, { view, decoration, readOnly = false } ) { commit('SET_DECORATION_LOADING', { decoration, value: true }) dispatch('forceDeleteDecoration', { view, decoration }) try { if (!readOnly) { await DecorationService(this.$client).delete(decoration.id) } } catch (error) { // Restore decoration in case of error dispatch('forceCreateDecoration', { view, values: decoration, }) commit('SET_DECORATION_LOADING', { decoration, value: false }) throw error } }, /** * Forcefully delete an existing decoration without making a request to the backend. */ forceDeleteDecoration({ commit }, { view, decoration }) { commit('DELETE_DECORATION', { view, id: decoration.id }) }, /** * Changes the loading state of a specific sort. */ setSortLoading({ commit }, { sort, value }) { commit('SET_SORT_LOADING', { sort, value }) }, /** * Creates a new sort and adds it to the store right away. If the API call succeeds * the row ID will be added, but if it fails it will be removed from the store. */ async createSort({ getters, commit }, { view, values, readOnly = false }) { // If the order is not provided we are going to choose the ascending order. if (!Object.prototype.hasOwnProperty.call(values, 'order')) { values.order = 'ASC' } const sort = Object.assign({}, values) populateSort(sort) sort.id = uuid() sort._.loading = !readOnly commit('ADD_SORT', { view, sort }) if (!readOnly) { try { const { data } = await SortService(this.$client).create(view.id, values) commit('FINALIZE_SORT', { view, oldId: sort.id, id: data.id }) } catch (error) { commit('DELETE_SORT', { view, id: sort.id }) throw error } } return { sort } }, /** * Forcefully create a new view sorting without making a request to the backend. */ forceCreateSort({ commit }, { view, values }) { const sort = Object.assign({}, values) populateSort(sort) commit('ADD_SORT', { view, sort }) }, /** * Updates the sort values in the store right away. If the API call fails the * changes will be undone. */ async updateSort({ dispatch, commit }, { sort, values, readOnly = false }) { commit('SET_SORT_LOADING', { sort, value: true }) const oldValues = {} const newValues = {} Object.keys(values).forEach((name) => { if (Object.prototype.hasOwnProperty.call(sort, name)) { oldValues[name] = sort[name] newValues[name] = values[name] } }) dispatch('forceUpdateSort', { sort, values: newValues }) try { if (!readOnly) { await SortService(this.$client).update(sort.id, values) } commit('SET_SORT_LOADING', { sort, value: false }) } catch (error) { dispatch('forceUpdateSort', { sort, values: oldValues }) commit('SET_SORT_LOADING', { sort, value: false }) throw error } }, /** * Forcefully update an existing view sort without making a request to the backend. */ forceUpdateSort({ commit }, { sort, values }) { commit('UPDATE_SORT', { sort, values }) }, /** * Deletes an existing sort. A request to the server will be made first and * after that it will be deleted. */ async deleteSort({ dispatch, commit }, { view, sort, readOnly = false }) { commit('SET_SORT_LOADING', { sort, value: true }) try { if (!readOnly) { await SortService(this.$client).delete(sort.id) } dispatch('forceDeleteSort', { view, sort }) } catch (error) { commit('SET_SORT_LOADING', { sort, value: false }) throw error } }, /** * Forcefully delete an existing view sort without making a request to the backend. */ forceDeleteSort({ commit }, { view, sort }) { commit('DELETE_SORT', { view, id: sort.id }) }, /** * When a field is deleted the related sortings are also automatically deleted in the * backend so they need to be removed here. */ deleteFieldSortings({ commit, getters }, { field }) { getters.getAll.forEach((view) => { commit('DELETE_FIELD_SORTINGS', { view, fieldId: field.id }) }) }, /** * Changes the loading state of a specific groupBy. */ setGroupByLoading({ commit }, { groupBy, value }) { commit('SET_GROUP_BY_LOADING', { groupBy, value }) }, /** * Creates a new groupBy and adds it to the store right away. If the API call succeeds * the row ID will be added, but if it fails it will be removed from the store. */ async createGroupBy({ getters, commit }, { view, values, readOnly = false }) { // If the order is not provided we are going to choose the ascending order. if (!Object.prototype.hasOwnProperty.call(values, 'order')) { values.order = 'ASC' } if (!Object.prototype.hasOwnProperty.call(values, 'width')) { values.width = 200 } const groupBy = Object.assign({}, values) populateGroupBy(groupBy) groupBy.id = uuid() groupBy._.loading = !readOnly commit('ADD_GROUP_BY', { view, groupBy }) if (!readOnly) { try { const { data } = await GroupByService(this.$client).create( view.id, values ) commit('FINALIZE_GROUP_BY', { view, oldId: groupBy.id, id: data.id }) } catch (error) { commit('DELETE_GROUP_BY', { view, id: groupBy.id }) throw error } } return { groupBy } }, /** * Forcefully create a new view group by without making a request to the backend. */ forceCreateGroupBy({ commit }, { view, values }) { const groupBy = Object.assign({}, values) populateGroupBy(groupBy) commit('ADD_GROUP_BY', { view, groupBy }) }, /** * Updates the groupBy values in the store right away. If the API call fails the * changes will be undone. */ async updateGroupBy( { dispatch, commit }, { groupBy, values, readOnly = false } ) { commit('SET_GROUP_BY_LOADING', { groupBy, value: true }) const oldValues = {} const newValues = {} Object.keys(values).forEach((name) => { if (Object.prototype.hasOwnProperty.call(groupBy, name)) { oldValues[name] = groupBy[name] newValues[name] = values[name] } }) dispatch('forceUpdateGroupBy', { groupBy, values: newValues }) try { if (!readOnly) { await GroupByService(this.$client).update(groupBy.id, values) } commit('SET_GROUP_BY_LOADING', { groupBy, value: false }) } catch (error) { dispatch('forceUpdateGroupBy', { groupBy, values: oldValues }) commit('SET_GROUP_BY_LOADING', { groupBy, value: false }) throw error } }, /** * Forcefully update an existing view groupBy without making a request to the backend. */ forceUpdateGroupBy({ commit }, { groupBy, values }) { commit('UPDATE_GROUP_BY', { groupBy, values }) }, /** * Deletes an existing groupBy. A request to the server will be made first and * after that it will be deleted. */ async deleteGroupBy( { dispatch, commit }, { view, groupBy, readOnly = false } ) { commit('SET_GROUP_BY_LOADING', { groupBy, value: true }) try { if (!readOnly) { await GroupByService(this.$client).delete(groupBy.id) } dispatch('forceDeleteGroupBy', { view, groupBy }) } catch (error) { commit('SET_GROUP_BY_LOADING', { groupBy, value: false }) throw error } }, /** * Forcefully delete an existing view groupBy without making a request to the backend. */ forceDeleteGroupBy({ commit }, { view, groupBy }) { commit('DELETE_GROUP_BY', { view, id: groupBy.id }) }, /** * When a field is deleted the related group bys are also automatically deleted in the * backend so they need to be removed here. */ deleteFieldGroupBys({ commit, getters }, { field }) { getters.getAll.forEach((view) => { commit('DELETE_FIELD_GROUP_BYS', { view, fieldId: field.id }) }) }, /** * Is called when a field is restored. Will force create all filters and sortings * provided along with the field. */ fieldRestored({ dispatch, commit, getters }, { field, fieldType, view }) { dispatch('resetFieldsFiltersSortsAndGroupBysInView', { field, view }) }, /** * Called when a field is restored. Will force create all filters and sortings * provided along with the field. */ resetFieldsFiltersSortsAndGroupBysInView( { dispatch, commit, getters }, { field, view } ) { if (field.filters != null) { commit('DELETE_FIELD_FILTERS', { view, fieldId: field.id }) field.filters .filter((filter) => filter.view === view.id) .forEach((filter) => { dispatch('forceCreateFilter', { view, values: filter }) }) } if (field.sortings != null) { commit('DELETE_FIELD_SORTINGS', { view, fieldId: field.id }) field.sortings .filter((sorting) => sorting.view === view.id) .forEach((sorting) => { dispatch('forceCreateSort', { view, values: sorting }) }) } if (field.group_bys != null) { commit('DELETE_FIELD_GROUP_BYS', { view, fieldId: field.id }) field.group_bys .filter((groupBy) => groupBy.view === view.id) .forEach((groupBy) => { dispatch('forceCreateSort', { view, values: groupBy }) }) } }, /** * Is called when a field is updated. It will check if there are filters related * to the delete field. */ fieldUpdated({ dispatch, commit, getters }, { field, fieldType }) { // Remove all filters are not compatible anymore. getters.getAll.forEach((view) => { view.filters .filter((filter) => filter.field === field.id) .forEach((filter) => { const filterType = this.$registry.get('viewFilter', filter.type) const compatible = filterType.fieldIsCompatible(field) if (!compatible) { commit('DELETE_FILTER', { view, id: filter.id }) } }) }) // Remove all the field sortings because the new field does not support sortings // at all. if (!fieldType.getCanSortInView(field)) { dispatch('deleteFieldSortings', { field }) } // Remove all the field group bys because the new field does not support group bys // at all. if (!fieldType.getCanGroupByInView(field)) { dispatch('deleteFieldGroupBys', { field }) } }, /** * Is called when a field is deleted. It will remove all filters and sortings * related to the field. */ fieldDeleted({ dispatch }, { field }) { dispatch('deleteFieldFilters', { field }) dispatch('deleteFieldSortings', { field }) dispatch('deleteFieldGroupBys', { field }) }, } export const getters = { hasSelected(state) { return Object.prototype.hasOwnProperty.call(state.selected, '_') }, getSelected(state) { return state.selected }, getSelectedId(state) { return state.selected.id || 0 }, get: (state) => (id) => { return state.items.find((item) => item.id === id) }, first(state, getters) { const items = getters.getAllOrdered return items.length > 0 ? items[0] : null }, // currently only used during unit tests: defaultId: (state) => { return state.defaultViewId }, default: (state, getters) => { return getters.get(state.defaultViewId) }, getAll(state) { return state.items }, getAllOrdered(state) { return state.items.map((item) => item).sort((a, b) => a.order - b.order) }, } export default { namespaced: true, state, getters, actions, mutations, }