UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

618 lines (616 loc) 22.7 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import { createReducer } from '@reduxjs/toolkit'; import { CLEAR_DROP_TARGETS, CLEAR_SELECT_FOR_EDIT, closeToolsPanel, contentTypeDropTargetsResponse, FETCH_ASSETS_PANEL_ITEMS, FETCH_ASSETS_PANEL_ITEMS_COMPLETE, FETCH_ASSETS_PANEL_ITEMS_FAILED, FETCH_CONTENT_MODEL_COMPLETE, fetchAudiencesPanelModel, fetchAudiencesPanelModelComplete, fetchAudiencesPanelModelFailed, fetchComponentsByContentType, fetchComponentsByContentTypeComplete, fetchComponentsByContentTypeFailed, fetchGuestModelComplete, fetchPrimaryGuestModelComplete, guestCheckIn, guestCheckOut, guestModelUpdated, guestPathUpdated, initIcePanelConfig, initRichTextEditorConfig, initToolbarConfig, initToolsPanelConfig, openToolsPanel, popIcePanelPage, popToolsPanelPage, pushIcePanelPage, pushToolsPanelPage, SELECT_FOR_EDIT, SET_ACTIVE_TARGETING_MODEL, SET_ACTIVE_TARGETING_MODEL_COMPLETE, SET_ACTIVE_TARGETING_MODEL_FAILED, SET_CONTENT_TYPE_FILTER, SET_HOST_HEIGHT, SET_HOST_SIZE, SET_HOST_WIDTH, SET_ITEM_BEING_DRAGGED, setEditModePadding, setHighlightMode, setPreviewEditMode, toggleEditModePadding, UPDATE_AUDIENCES_PANEL_MODEL, updateIcePanelWidth, updateToolsPanelWidth } from '../actions/preview'; import { applyDeserializedXMLTransforms, createEntityState, createLookupTable, nnou, nou, reversePluckProps } from '../../utils/object'; import { changeSite } from '../actions/sites'; import { deserialize, fromString } from '../../utils/xml'; import { defineMessages } from 'react-intl'; import { fetchSiteUiConfigComplete } from '../actions/configuration'; const messages = defineMessages({ emptyUiConfigMessageTitle: { id: 'emptyUiConfigMessageTitle.title', defaultMessage: 'Configuration is empty' }, emptyUiConfigMessageSubtitle: { id: 'emptyUiConfigMessageTitle.subtitle', defaultMessage: 'Nothing is set to be shown here.' }, noUiConfigMessageTitle: { id: 'noUiConfigMessageTitle.title', defaultMessage: 'Configuration file missing' }, noUiConfigMessageSubtitle: { id: 'noUiConfigMessageTitle.subtitle', defaultMessage: 'Add & configure `ui.xml` on your project to show content here.' } }); const audiencesPanelInitialState = { isFetching: null, isApplying: false, error: null, model: null, applied: false }; const assetsPanelInitialState = createEntityState({ page: [], query: { keywords: '', offset: 0, limit: 10, filters: { 'mime-type': ['image/png', 'image/jpeg', 'image/gif', 'video/mp4', 'image/svg+xml'] } } }); const componentsInitialState = createEntityState({ page: [], query: { keywords: '', offset: 0, limit: 10 }, contentTypeFilter: 'all', inPageInstances: {} }); const initialState = { editMode: true, highlightMode: 'all', hostSize: { width: null, height: null }, toolsPanelPageStack: [], showToolsPanel: process.env.REACT_APP_SHOW_TOOLS_PANEL ? process.env.REACT_APP_SHOW_TOOLS_PANEL === 'true' : true, toolsPanelWidth: 240, icePanelWidth: 240, icePanelStack: [], guest: null, assets: assetsPanelInitialState, audiencesPanel: audiencesPanelInitialState, components: componentsInitialState, dropTargets: { selectedContentType: null, byId: null }, toolsPanel: null, toolbar: { leftSection: null, middleSection: null, rightSection: null }, icePanel: null, richTextEditor: null, editModePadding: false }; const minDrawerWidth = 240; const maxDrawerWidth = 500; const fetchGuestModelsCompleteHandler = (state, { type, payload }) => { var _a; if (nnou(state.guest)) { return Object.assign(Object.assign({}, state), { guest: Object.assign(Object.assign({}, state.guest), { modelId: type === fetchPrimaryGuestModelComplete.type ? payload.model.craftercms.id : state.guest.modelId, models: Object.assign(Object.assign({}, state.guest.models), payload.modelLookup), modelIdByPath: Object.assign(Object.assign({}, state.guest.modelIdByPath), payload.modelIdByPath), hierarchyMap: Object.assign( Object.assign({}, (_a = state.guest) === null || _a === void 0 ? void 0 : _a.hierarchyMap), payload.hierarchyMap ) }) }); } else { // TODO: Currently getting models before check in some cases when coming from a different site. console.error('[reducer/preview] Guest models received before guest check in.'); return state; } }; const reducer = createReducer(initialState, { [openToolsPanel.type]: (state) => { return Object.assign(Object.assign({}, state), { showToolsPanel: true }); }, [closeToolsPanel.type]: (state) => { return Object.assign(Object.assign({}, state), { showToolsPanel: false }); }, [SET_HOST_SIZE]: (state, { payload }) => { if (isNaN(payload.width)) { payload.width = state.hostSize.width; } if (isNaN(payload.height)) { payload.height = state.hostSize.height; } return Object.assign(Object.assign({}, state), { hostSize: Object.assign(Object.assign({}, state.hostSize), { width: minFrameSize(payload.width), height: minFrameSize(payload.height) }) }); }, [SET_HOST_WIDTH]: (state, { payload }) => { if (isNaN(payload)) { return state; } return Object.assign(Object.assign({}, state), { hostSize: Object.assign(Object.assign({}, state.hostSize), { width: minFrameSize(payload) }) }); }, [SET_HOST_HEIGHT]: (state, { payload }) => { if (isNaN(payload)) { return state; } return Object.assign(Object.assign({}, state), { hostSize: Object.assign(Object.assign({}, state.hostSize), { height: minFrameSize(payload) }) }); }, [FETCH_CONTENT_MODEL_COMPLETE]: (state, { payload }) => { return Object.assign(Object.assign({}, state), { currentModels: payload }); }, [guestCheckIn.type]: (state, { payload }) => { const { location, path } = payload; const href = location.href; const origin = location.origin; const url = href.replace(location.origin, ''); return Object.assign(Object.assign({}, state), { guest: { url, origin, modelId: null, path, models: null, hierarchyMap: null, modelIdByPath: null, selected: null, itemBeingDragged: null } }); }, [guestCheckOut.type]: (state) => { let nextState = state; if (state.guest) { nextState = Object.assign(Object.assign({}, nextState), { guest: null }); } // If guest checks out, doesn't mean site is changing necessarily // hence content types haven't changed // if (state.contentTypes) { // nextState = { ...nextState, contentTypes: null }; // } return nextState; }, [fetchPrimaryGuestModelComplete.type]: fetchGuestModelsCompleteHandler, [fetchGuestModelComplete.type]: fetchGuestModelsCompleteHandler, [guestModelUpdated.type]: (state, { payload: { model } }) => Object.assign(Object.assign({}, state), { guest: Object.assign(Object.assign({}, state.guest), { models: Object.assign(Object.assign({}, state.guest.models), { [model.craftercms.id]: model }) }) }), [SELECT_FOR_EDIT]: (state, { payload }) => { if (state.guest === null) { return state; } return Object.assign(Object.assign({}, state), { guest: Object.assign(Object.assign({}, state.guest), { selected: [payload] }) }); }, [CLEAR_SELECT_FOR_EDIT]: (state, { payload }) => { if (state.guest === null) { return state; } return Object.assign(Object.assign({}, state), { guest: Object.assign(Object.assign({}, state.guest), { selected: null }) }); }, [SET_ITEM_BEING_DRAGGED]: (state, { payload }) => { if (nou(state.guest)) { return state; } return Object.assign(Object.assign({}, state), { guest: Object.assign(Object.assign({}, state.guest), { itemBeingDragged: payload }) }); }, [fetchAudiencesPanelModel.type]: (state) => Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { isFetching: true, error: null }) }), [fetchAudiencesPanelModelComplete.type]: (state, { payload }) => { return Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { isFetching: false, error: null, model: payload }) }); }, [fetchAudiencesPanelModelFailed.type]: (state, { payload }) => Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { error: payload.response, isFetching: false }) }), [UPDATE_AUDIENCES_PANEL_MODEL]: (state, { payload }) => Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { applied: false, model: Object.assign(Object.assign({}, state.audiencesPanel.model), payload) }) }), [SET_ACTIVE_TARGETING_MODEL]: (state, { payload }) => Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { isApplying: true }) }), [SET_ACTIVE_TARGETING_MODEL_COMPLETE]: (state, { payload }) => Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { isApplying: false, applied: true }) }), [SET_ACTIVE_TARGETING_MODEL_FAILED]: (state, { payload }) => Object.assign(Object.assign({}, state), { audiencesPanel: Object.assign(Object.assign({}, state.audiencesPanel), { isApplying: false, applied: false, error: payload.response }) }), [FETCH_ASSETS_PANEL_ITEMS]: (state, { payload: query }) => { let newQuery = Object.assign(Object.assign({}, state.assets.query), query); return Object.assign(Object.assign({}, state), { assets: Object.assign(Object.assign({}, state.assets), { isFetching: true, query: newQuery, pageNumber: Math.ceil(newQuery.offset / newQuery.limit) }) }); }, [FETCH_ASSETS_PANEL_ITEMS_COMPLETE]: (state, { payload: searchResult }) => { let itemsLookupTable = createLookupTable(searchResult.items, 'path'); let page = [...state.assets.page]; page[state.assets.pageNumber] = searchResult.items.map((item) => item.path); return Object.assign(Object.assign({}, state), { assets: Object.assign(Object.assign({}, state.assets), { byId: Object.assign(Object.assign({}, state.assets.byId), itemsLookupTable), page, count: searchResult.total, isFetching: false, error: null }) }); }, [FETCH_ASSETS_PANEL_ITEMS_FAILED]: (state, { payload }) => Object.assign(Object.assign({}, state), { assets: Object.assign(Object.assign({}, state.assets), { error: payload.response, isFetching: false }) }), [fetchComponentsByContentType.type]: (state, { payload }) => { var _a; return Object.assign(Object.assign({}, state), { components: Object.assign(Object.assign({}, state.components), { isFetching: true, query: Object.assign(Object.assign({}, state.components.query), payload), pageNumber: Math.ceil( ((_a = payload.offset) !== null && _a !== void 0 ? _a : state.components.query.offset) / state.components.query.limit ) }) }); }, [fetchComponentsByContentTypeComplete.type]: (state, { payload }) => { let page = [...state.components.page]; page[state.components.pageNumber] = Object.keys(payload.lookup); return Object.assign(Object.assign({}, state), { components: Object.assign(Object.assign({}, state.components), { byId: Object.assign(Object.assign({}, state.components.byId), payload.lookup), page, count: payload.count, isFetching: false, error: null }) }); }, [fetchComponentsByContentTypeFailed.type]: (state, { payload }) => Object.assign(Object.assign({}, state), { components: Object.assign(Object.assign({}, state.components), { error: payload.response, isFetching: false }) }), [contentTypeDropTargetsResponse.type]: (state, { payload }) => Object.assign(Object.assign({}, state), { dropTargets: Object.assign(Object.assign({}, state.dropTargets), { selectedContentType: payload.contentTypeId, byId: Object.assign(Object.assign({}, state.dropTargets.byId), createLookupTable(payload.dropTargets)) }) }), [CLEAR_DROP_TARGETS]: (state, { payload }) => Object.assign(Object.assign({}, state), { dropTargets: Object.assign(Object.assign({}, state.dropTargets), { selectedContentType: null, byId: null }) }), [SET_CONTENT_TYPE_FILTER]: (state, { payload }) => Object.assign(Object.assign({}, state), { components: Object.assign(Object.assign({}, state.components), { isFetching: null, contentTypeFilter: payload, pageNumber: 0, query: Object.assign(Object.assign({}, state.components.query), { offset: 0 }) }) }), [setPreviewEditMode.type]: (state, { payload }) => { var _a; return Object.assign(Object.assign({}, state), { editMode: payload.editMode, highlightMode: (_a = payload.highlightMode) !== null && _a !== void 0 ? _a : state.highlightMode }); }, [updateToolsPanelWidth.type]: (state, { payload }) => { if (payload.width < minDrawerWidth || payload.width > maxDrawerWidth) { return state; } return Object.assign(Object.assign({}, state), { toolsPanelWidth: payload.width }); }, [updateIcePanelWidth.type]: (state, { payload }) => { if (payload.width < minDrawerWidth || payload.width > maxDrawerWidth) { return state; } return Object.assign(Object.assign({}, state), { icePanelWidth: payload.width }); }, [pushToolsPanelPage.type]: (state, { payload }) => { return Object.assign(Object.assign({}, state), { toolsPanelPageStack: [...state.toolsPanelPageStack, payload] }); }, [popToolsPanelPage.type]: (state) => { let stack = [...state.toolsPanelPageStack]; stack.pop(); return Object.assign(Object.assign({}, state), { toolsPanelPageStack: stack }); }, [pushIcePanelPage.type]: (state, { payload }) => { return Object.assign(Object.assign({}, state), { icePanelStack: [...state.icePanelStack, payload] }); }, [popIcePanelPage.type]: (state) => { let stack = [...state.icePanelStack]; stack.pop(); return Object.assign(Object.assign({}, state), { icePanelStack: stack }); }, [guestPathUpdated.type]: (state, { payload }) => Object.assign(Object.assign({}, state), { guest: Object.assign(Object.assign({}, state.guest), { path: payload.path }) }), [setHighlightMode.type]: (state, { payload }) => Object.assign(Object.assign({}, state), { highlightMode: payload.highlightMode }), [changeSite.type]: (state) => { return Object.assign( Object.assign({}, state), reversePluckProps(initialState, 'editMode', 'highlightMode', 'showToolsPanel', 'toolsPanelWidth', 'icePanelWidth') ); }, [initToolsPanelConfig.type]: (state, { payload }) => { let toolsPanelConfig = { widgets: [ { id: 'craftercms.component.EmptyState', uiKey: -1, configuration: { title: messages.noUiConfigMessageTitle, subtitle: messages.noUiConfigMessageSubtitle } } ] }; const arrays = ['widgets', 'permittedRoles', 'excludes']; const lookupTables = ['fields']; const configDOM = fromString(payload.configXml); const toolsPanelPages = configDOM.querySelector( '[id="craftercms.components.ToolsPanel"] > configuration > widgets' ); if (toolsPanelPages) { toolsPanelConfig = applyDeserializedXMLTransforms(deserialize(toolsPanelPages), { arrays, lookupTables }); } const toolsPanelWidth = payload.toolsPanelWidth < minDrawerWidth ? minDrawerWidth : payload.toolsPanelWidth > maxDrawerWidth ? maxDrawerWidth : payload.toolsPanelWidth; return Object.assign( Object.assign(Object.assign({}, state), payload.storedPage && { toolsPanelPageStack: [payload.storedPage] }), { toolsPanel: toolsPanelConfig, toolsPanelWidth: toolsPanelWidth !== null && toolsPanelWidth !== void 0 ? toolsPanelWidth : state.toolsPanelWidth } ); }, // After re-fetching site ui config (e.g. when config is modified), we need the tools to be // re-initialized with the latest config. The components checks for whether their property is null before // initializing so props must be nulled when config gets re-fetched in order for the components to re-initialize. [fetchSiteUiConfigComplete.type]: (state) => Object.assign(Object.assign({}, state), { toolsPanel: initialState.toolsPanel, toolbar: initialState.toolbar, icePanel: initialState.icePanel, richTextEditor: initialState.richTextEditor }), [initToolbarConfig.type]: (state, { payload }) => { let toolbarConfig = { leftSection: { widgets: [] }, middleSection: { widgets: [] }, rightSection: { widgets: [] } }; const arrays = ['widgets']; const configDOM = fromString(payload.configXml); const toolbar = configDOM.querySelector('[id="craftercms.components.PreviewToolbar"] > configuration'); if (toolbar) { const leftSection = toolbar.querySelector('leftSection > widgets'); if (leftSection) { toolbarConfig.leftSection = applyDeserializedXMLTransforms(deserialize(leftSection), { arrays }); } const middleSection = toolbar.querySelector('middleSection > widgets'); if (middleSection) { toolbarConfig.middleSection = applyDeserializedXMLTransforms(deserialize(middleSection), { arrays }); } const rightSection = toolbar.querySelector('rightSection > widgets'); if (rightSection) { toolbarConfig.rightSection = applyDeserializedXMLTransforms(deserialize(rightSection), { arrays }); } } return Object.assign(Object.assign({}, state), { toolbar: toolbarConfig }); }, [initIcePanelConfig.type]: (state, { payload }) => { let icePanelConfig = { widgets: [ { id: 'craftercms.component.EmptyState', uiKey: -1, configuration: { title: messages.noUiConfigMessageTitle, subtitle: messages.noUiConfigMessageSubtitle } } ] }; const arrays = ['widgets', 'devices', 'values']; const configDOM = fromString(payload.configXml); const icePanel = configDOM.querySelector('[id="craftercms.components.ICEToolsPanel"] > configuration > widgets'); if (icePanel) { const lookupTables = ['fields']; icePanel.querySelectorAll('widget').forEach((e) => { var _a; if (e.getAttribute('id') === 'craftercms.components.ToolsPanelPageButton') { let target = 'icePanel'; (_a = e.querySelector(':scope > configuration')) === null || _a === void 0 ? void 0 : _a.setAttribute('target', target); } }); icePanelConfig = applyDeserializedXMLTransforms(deserialize(icePanel), { arrays, lookupTables }); } const icePanelWidth = payload.icePanelWidth < minDrawerWidth ? minDrawerWidth : payload.icePanelWidth > maxDrawerWidth ? maxDrawerWidth : payload.icePanelWidth; return Object.assign( Object.assign(Object.assign({}, state), payload.storedPage && { icePanelStack: [payload.storedPage] }), { icePanel: icePanelConfig, icePanelWidth: icePanelWidth !== null && icePanelWidth !== void 0 ? icePanelWidth : state.icePanelWidth } ); }, [initRichTextEditorConfig.type]: (state, { payload }) => { let rteConfig = {}; const arrays = ['setups']; const renameTable = { '#text': 'data' }; const configDOM = fromString(payload.configXml); const rte = configDOM.querySelector('[id="craftercms.components.TinyMCE"] > configuration'); if (rte) { try { const conf = applyDeserializedXMLTransforms(deserialize(rte), { arrays, renameTable }).configuration; let setups = {}; conf.setups.forEach((setup) => { setup.tinymceOptions = JSON.parse(setup.tinymceOptions.replaceAll('{site}', payload.siteId)); setups[setup.id] = setup; }); rteConfig = setups; } catch (e) { console.error(e); } } return Object.assign(Object.assign({}, state), { richTextEditor: rteConfig }); }, [setEditModePadding.type]: (state, { payload }) => Object.assign(Object.assign({}, state), { editModePadding: payload.editModePadding }), [toggleEditModePadding.type]: (state) => Object.assign(Object.assign({}, state), { editModePadding: !state.editModePadding }) }); function minFrameSize(suggestedSize) { return suggestedSize === null ? null : suggestedSize < 320 ? 320 : suggestedSize; } export default reducer;