@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
618 lines (616 loc) • 22.7 kB
JavaScript
/*
* 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;