diff --git a/Makefile b/Makefile index 255ef25b81..75a51ea83c 100644 --- a/Makefile +++ b/Makefile @@ -13,12 +13,6 @@ MAKEFLAGS+=--no-builtin-rules # Project settings INSTANCE_PORT=8080 -# The defaults from the UI configuration -# export RAZZLE_DEV_PROXY_API_PATH=http://localhost:$(INSTANCE_PORT)/Plone -# export RAZZLE_API_PATH=http://localhost:3000 -# Uncomment the following to run against the proxy hosting testbed -# export RAZZLE_DEV_PROXY_API_PATH= -# export RAZZLE_API_PATH=http://localhost:49080/api/Plone # Recipe snippets for reuse @@ -83,7 +77,7 @@ docs-build: .PHONY: start # Run both the back-end and the front end start: - $(MAKE) -e -j 2 start-backend start-frontend + $(MAKE) -j 2 start-backend start-frontend .PHONY: start-frontend start-frontend: dist @@ -101,10 +95,6 @@ start-backend-docker: start-backend-docker-guillotina: docker-compose -f g-api/docker-compose.yml up -d -.PHONY: start-proxy -start-proxy: start - docker-compose up -d traefik - .PHONY: start-test start-test: ## Start Test @echo "$(GREEN)==> Start Test$(RESET)" diff --git a/api/buildout.cfg b/api/buildout.cfg index 86ef7e3cc6..cc6aeb7298 100644 --- a/api/buildout.cfg +++ b/api/buildout.cfg @@ -2,7 +2,7 @@ index = https://pypi.org/simple/ extends = http://dist.plone.org/release/5.2.4/versions.cfg find-links += http://dist.plone.org/thirdparty/ -parts = instance plonesite site-packages robot-server +parts = instance plonesite robot-server versions = versions extensions = mr.developer @@ -10,7 +10,7 @@ auto-checkout = always-checkout = force [sources] -kitconcept.volto = git https://github.com/kitconcept/kitconcept.volto.git branch=main +kitconcept.volto = git https://github.com/kitconcept/kitconcept.volto.git branch=workingcopysupport plone.rest = git git@github.com:plone/plone.rest.git branch=master plone.restapi = git git://github.com/plone/plone.restapi.git pushurl=git@github.com:plone/plone.restapi.git branch=master @@ -62,14 +62,6 @@ upgrade-portal = False upgrade-all-profiles = False site-replace = True -[site-packages] -# Reproduce a single directory tree of the Python packages installed in this buildout's -# `rel_client` part. Useful for searching, browsing, or otherwise exploring all the -# source code involved in the application in a way that's more readable and avoids -# duplicates from older versions of eggs. -recipe = collective.recipe.omelette -eggs = ${instance:eggs} - [versions] setuptools = zc.buildout = diff --git a/docker-compose.yml b/docker-compose.yml index 0b9513f01c..ec3f315e7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,16 +9,6 @@ services: - ADDONS=kitconcept.volto - 'PROFILES=kitconcept.volto:default-homepage' image: plone - labels: - traefik.enable: "true" - traefik.http.routers.plone.entrypoints: "web" - traefik.http.routers.plone.rule: "PathPrefix(`/api`)" - # https://doc.traefik.io/traefik/middlewares/stripprefix/#configuration-examples - traefik.http.middlewares.strip-api-prefix.stripprefix.prefixes: "/api" - traefik.http.middlewares.rewrite-api-vhost.replacepathregex.regex: "^(.*)$$" - traefik.http.middlewares.rewrite-api-vhost.replacepathregex.replacement: "/VirtualHostBase/http/localhost:49080/VirtualHostRoot/_vh_api$$1" - traefik.http.routers.plone.middlewares: "strip-api-prefix@docker,rewrite-api-vhost@docker" - traefik.http.services.plone.loadbalancer.server.port: "8080" frontend: ports: @@ -30,38 +20,3 @@ services: # environment: # - INTERNAL_API_PATH=plone:8080/Plone # - ADDONS="volto-slate:asDefault" - labels: - traefik.enable: "true" - traefik.http.routers.frontend.entrypoints: "web" - traefik.http.routers.frontend.rule: "PathPrefix(`/ui`)" - # https://doc.traefik.io/traefik/middlewares/stripprefix/#configuration-examples - traefik.http.middlewares.strip-ui-prefix.stripprefix.prefixes: "/ui" - traefik.http.routers.frontend.middlewares: "strip-ui-prefix@docker" - traefik.http.services.frontend.loadbalancer.server.port: "3000" - - traefik: - image: "traefik" - # Disabled so that the proxy can also be used to test deployment scenarios against - # a back-end and/or front-end running on the local host. - # depends_on: - # - plone - # - frontend - command: - # - "--log.level=DEBUG" - - "--providers.docker=true" - - "--providers.docker.exposedbydefault=false" - - "--entrypoints.web.address=:49080" - - "--providers.file.directory=/etc/traefik.d/" - volumes: - - "/var/run/docker.sock:/var/run/docker.sock:ro" - - "./traefik.d/:/etc/traefik.d/" - ports: - - "49080:49080" - -networks: - default: - ipam: - driver: "default" - config: - # Use the same random subnet each time - - subnet: "192.168.80.0/24" diff --git a/docs/source/recipes/folder-structure.md b/docs/source/recipes/folder-structure.md index daa1555f23..d4218d899c 100644 --- a/docs/source/recipes/folder-structure.md +++ b/docs/source/recipes/folder-structure.md @@ -4,26 +4,20 @@ Volto is based on React, Redux, and React-Router. All of the code is located in the `src` folder. The following convention for locating resources is used. -## Components - -`components` contains all the React components, AKA views. This includes views for the -management interface and the theme. - ## Actions `actions` contains all the redux actions for fetching all backend data like content, users and external resources that are pulled into our app in general. -## Reducers +## Components -`reducers` contains all the Redux reducers that manage the life-cycle for Redux actions -and make the according changes to state. +`components` contains all the views. This includes views for the management +interface and the theme. -## Selectors +## Config -`selectors` contains all the [Redux -selectors](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow#selectors) -that interpret state into the form used by UI components. +In this folder all configuration is stored. All configuration can be overridden +in your theme package. ## Constants @@ -33,10 +27,9 @@ The constants contain all constants including the action types. `helpers` contains helper methods like for example url helpers. -## Config +## Reducers -In this folder all configuration is stored. All configuration can be overridden -in your theme package. +All the reducers are located here. ## Theme diff --git a/src/actions/types/types.js b/src/actions/types/types.js index a076777276..34f43ec791 100644 --- a/src/actions/types/types.js +++ b/src/actions/types/types.js @@ -4,7 +4,6 @@ */ import { GET_TYPES } from '@plone/volto/constants/ActionTypes'; -import { loggedIn } from '@plone/volto/selectors/userSession/userSession'; /** * Get types function. @@ -14,7 +13,7 @@ import { loggedIn } from '@plone/volto/selectors/userSession/userSession'; */ export function getTypes(url) { return (dispatch, getState) => { - if (loggedIn(getState())) { + if (getState().userSession.token) { dispatch({ type: GET_TYPES, request: { diff --git a/src/actions/types/types.test.js b/src/actions/types/types.test.js index 486b8b64f9..885be2ec12 100644 --- a/src/actions/types/types.test.js +++ b/src/actions/types/types.test.js @@ -1,15 +1,13 @@ import { getTypes } from './types'; import { GET_TYPES } from '@plone/volto/constants/ActionTypes'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; - -const actions = { user: [{ id: 'logout' }] }; -const actionsById = arrayWIdsToObject(actions); describe('Types action', () => { describe('getTypes', () => { it('should create an action to get the types', () => { const getState = () => ({ - actions: { actions, actionsById }, + userSession: { + token: 'thetoken', + }, }); const url = '/blog'; const dispatch = jest.fn(); diff --git a/src/components/manage/Contents/Contents.jsx b/src/components/manage/Contents/Contents.jsx index 384459aaf9..e1b0255640 100644 --- a/src/components/manage/Contents/Contents.jsx +++ b/src/components/manage/Contents/Contents.jsx @@ -51,7 +51,6 @@ import { updateColumnsContent, } from '@plone/volto/actions'; import Indexes, { defaultIndexes } from '@plone/volto/constants/Indexes'; -import { loggedIn } from '@plone/volto/selectors/userSession/userSession'; import { ContentsIndexHeader, ContentsItem, @@ -1091,7 +1090,9 @@ class Contents extends Component { const selected = this.state.selected.length > 0; const filteredItems = this.state.filteredItems || this.state.selected; const path = getBaseUrl(this.props.pathname); - const folderContentsAction = this.props.actionsById.object.folderContents; + const folderContentsAction = find(this.props.objectActions, { + id: 'folderContents', + }); const loading = (this.props.clipboardRequest?.loading && @@ -1101,7 +1102,7 @@ class Contents extends Component { (this.props.orderRequest?.loading && !this.props.orderRequest?.error) || (this.props.searchRequest?.loading && !this.props.searchRequest?.error); - return this.props.userLoggedIn && this.props.objectActions.length > 0 ? ( + return this.props.token && this.props.objectActions.length > 0 ? ( <> {folderContentsAction ? ( @@ -1767,8 +1768,7 @@ export const __test__ = compose( connect( (store, props) => { return { - userLoggedIn: loggedIn(store), - actionsById: store.actions.actionsById, + token: store.userSession.token, items: store.search.items, sort: store.content.update.sort, index: store.content.updatecolumns.idx, @@ -1809,8 +1809,7 @@ export default compose( connect( (store, props) => { return { - userLoggedIn: loggedIn(store), - actionsById: store.actions.actionsById, + token: store.userSession.token, items: store.search.items, sort: store.content.update.sort, index: store.content.updatecolumns.idx, diff --git a/src/components/manage/Contents/Contents.test.jsx b/src/components/manage/Contents/Contents.test.jsx index 8e021e3ec3..5e8f9f9bf7 100644 --- a/src/components/manage/Contents/Contents.test.jsx +++ b/src/components/manage/Contents/Contents.test.jsx @@ -5,7 +5,6 @@ import { Provider } from 'react-intl-redux'; import { MemoryRouter } from 'react-router-dom'; import { __test__ as Contents } from './Contents'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; const mockStore = configureStore(); @@ -32,23 +31,24 @@ jest.mock('moment', () => })), ); -const actions = { - document_actions: [], - object: [ - { - icon: '', - id: 'folderContents', - title: 'Contents', - }, - ], - user: [{ id: 'logout' }], -}; -const actionsById = arrayWIdsToObject(actions); - describe('Contents', () => { it('renders a folder contents view component', () => { const store = mockStore({ - actions: { actions, actionsById }, + actions: { + actions: { + document_actions: [], + object: [ + { + icon: '', + id: 'folderContents', + title: 'Contents', + }, + ], + }, + }, + userSession: { + token: '14134234123qwdaf', + }, search: { items: [ { diff --git a/src/components/manage/Edit/Edit.jsx b/src/components/manage/Edit/Edit.jsx index 4f7442d248..7de4b1cfdf 100644 --- a/src/components/manage/Edit/Edit.jsx +++ b/src/components/manage/Edit/Edit.jsx @@ -16,7 +16,6 @@ import qs from 'query-string'; import { find } from 'lodash'; import { toast } from 'react-toastify'; -import { loggedIn } from '@plone/volto/selectors/userSession/userSession'; import { Forbidden, Form, @@ -351,7 +350,7 @@ class Edit extends Component { )} {!editPermission && ( <> - {this.props.userLoggedIn ? ( + {this.props.token ? ( ({ objectActions: state.actions.actions.object, - userLoggedIn: loggedIn(state), + token: state.userSession.token, content: state.content.data, compare_to: state.content.subrequests?.compare_to?.data, schema: state.schema.schema, @@ -472,7 +471,7 @@ export default compose( connect( (state, props) => ({ objectActions: state.actions.actions.object, - userLoggedIn: loggedIn(state), + token: state.userSession.token, content: state.content.data, compare_to: state.content.subrequests?.compare_to?.data, schema: state.schema.schema, diff --git a/src/components/manage/Edit/Edit.test.jsx b/src/components/manage/Edit/Edit.test.jsx index ed0dc3f278..5e28911e95 100644 --- a/src/components/manage/Edit/Edit.test.jsx +++ b/src/components/manage/Edit/Edit.test.jsx @@ -2,8 +2,8 @@ import React from 'react'; import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; +import jwt from 'jsonwebtoken'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; import { __test__ as Edit } from './Edit'; const mockStore = configureStore(); @@ -13,23 +13,24 @@ jest.mock('react-portal', () => ({ })); jest.mock('../Form/Form', () => jest.fn(() =>
)); -const actions = { - document_actions: [], - object: [ - { - icon: '', - id: 'edit', - title: 'Edit', - }, - ], - user: [{ id: 'logout' }], -}; -const actionsById = arrayWIdsToObject(actions); - describe('Edit', () => { it('renders an empty edit component', () => { const store = mockStore({ - actions: { actions, actionsById }, + userSession: { + token: jwt.sign({ fullname: 'John Doe' }, 'secret'), + }, + actions: { + actions: { + document_actions: [], + object: [ + { + icon: '', + id: 'edit', + title: 'Edit', + }, + ], + }, + }, schema: { schema: null, }, @@ -60,7 +61,21 @@ describe('Edit', () => { it('renders an edit component', () => { const store = mockStore({ - actions: { actions, actionsById }, + userSession: { + token: jwt.sign({ fullname: 'John Doe' }, 'secret'), + }, + actions: { + actions: { + document_actions: [], + object: [ + { + icon: '', + id: 'edit', + title: 'Edit', + }, + ], + }, + }, schema: { schema: { some: 'field', diff --git a/src/components/manage/Preferences/ChangePassword.jsx b/src/components/manage/Preferences/ChangePassword.jsx index d737c7a17a..308eb88e2e 100644 --- a/src/components/manage/Preferences/ChangePassword.jsx +++ b/src/components/manage/Preferences/ChangePassword.jsx @@ -12,11 +12,11 @@ import { Link, withRouter } from 'react-router-dom'; import { Portal } from 'react-portal'; import { defineMessages, injectIntl } from 'react-intl'; import { Container } from 'semantic-ui-react'; +import jwtDecode from 'jwt-decode'; import { toast } from 'react-toastify'; import { Form, Icon, Toast, Toolbar } from '@plone/volto/components'; import { updatePassword } from '@plone/volto/actions'; -import { userData } from '@plone/volto/selectors/userSession/userSession'; import { getBaseUrl } from '@plone/volto/helpers'; import backSVG from '@plone/volto/icons/back.svg'; @@ -226,7 +226,9 @@ export default compose( injectIntl, connect( (state, props) => ({ - userId: userData(state).userId, + userId: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', loading: state.users.update_password.loading, pathname: props.location.pathname, }), diff --git a/src/components/manage/Preferences/PersonalInformation.jsx b/src/components/manage/Preferences/PersonalInformation.jsx index 9e15931923..774ef0646a 100644 --- a/src/components/manage/Preferences/PersonalInformation.jsx +++ b/src/components/manage/Preferences/PersonalInformation.jsx @@ -8,11 +8,11 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { defineMessages, injectIntl } from 'react-intl'; +import jwtDecode from 'jwt-decode'; import { toast } from 'react-toastify'; import { Form, Toast } from '@plone/volto/components'; import { getUser, updateUser } from '@plone/volto/actions'; -import { userData } from '@plone/volto/selectors/userSession/userSession'; const messages = defineMessages({ personalInformation: { @@ -230,7 +230,9 @@ export default compose( connect( (state, props) => ({ user: state.users.user, - userId: userData(state).userId, + userId: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', loaded: state.users.get.loaded, loading: state.users.update.loading, }), diff --git a/src/components/manage/Sharing/Sharing.jsx b/src/components/manage/Sharing/Sharing.jsx index 4dc1f43d0e..91ceff5224 100644 --- a/src/components/manage/Sharing/Sharing.jsx +++ b/src/components/manage/Sharing/Sharing.jsx @@ -20,10 +20,10 @@ import { Segment, Table, } from 'semantic-ui-react'; +import jwtDecode from 'jwt-decode'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { updateSharing, getSharing } from '@plone/volto/actions'; -import { userData } from '@plone/volto/selectors/userSession/userSession'; import { getBaseUrl } from '@plone/volto/helpers'; import { Icon, Toolbar, Toast } from '@plone/volto/components'; import { toast } from 'react-toastify'; @@ -467,7 +467,9 @@ export default compose( updateRequest: state.sharing.update, pathname: props.location.pathname, title: state.content.data.title, - login: userData(state).userId, + login: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', }), { updateSharing, getSharing }, ), diff --git a/src/components/manage/Toolbar/PersonalTools.jsx b/src/components/manage/Toolbar/PersonalTools.jsx index cf67dbe0b3..aec591438e 100644 --- a/src/components/manage/Toolbar/PersonalTools.jsx +++ b/src/components/manage/Toolbar/PersonalTools.jsx @@ -6,11 +6,11 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; +import jwtDecode from 'jwt-decode'; import cx from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { Icon } from '@plone/volto/components'; import { getUser } from '@plone/volto/actions'; -import { userData } from '@plone/volto/selectors/userSession/userSession'; import { userHasRoles } from '@plone/volto/helpers'; import logoutSVG from '@plone/volto/icons/log-out.svg'; @@ -183,7 +183,9 @@ export default injectIntl( connect( (state) => ({ user: state.users.user, - userId: userData(state).userId, + userId: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', }), { getUser }, )(PersonalTools), diff --git a/src/components/manage/Toolbar/Toolbar.jsx b/src/components/manage/Toolbar/Toolbar.jsx index 565ef498c4..1058eff2a7 100644 --- a/src/components/manage/Toolbar/Toolbar.jsx +++ b/src/components/manage/Toolbar/Toolbar.jsx @@ -7,6 +7,7 @@ import React, { Component } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; +import jwtDecode from 'jwt-decode'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib'; @@ -27,10 +28,6 @@ import { setExpandedToolbar, unlockContent, } from '@plone/volto/actions'; -import { - loggedIn, - userData, -} from '@plone/volto/selectors/userSession/userSession'; import { Icon } from '@plone/volto/components'; import { BodyClass, getBaseUrl } from '@plone/volto/helpers'; import { Pluggable } from '@plone/volto/components/manage/Pluggable'; @@ -143,7 +140,8 @@ class Toolbar extends Component { object_buttons: PropTypes.arrayOf(PropTypes.object), user: PropTypes.arrayOf(PropTypes.object), }), - userLoggedIn: PropTypes.bool, + token: PropTypes.string, + userId: PropTypes.string, pathname: PropTypes.string.isRequired, content: PropTypes.shape({ '@type': PropTypes.string, @@ -172,6 +170,8 @@ class Toolbar extends Component { */ static defaultProps = { actions: null, + token: null, + userId: null, content: null, hideDefaultViewButtons: false, types: [], @@ -308,7 +308,7 @@ class Toolbar extends Component { const { expanded } = this.state; return ( - this.props.userLoggedIn && ( + this.props.token && ( <> ({ actions: state.actions.actions, - userLoggedIn: loggedIn(state), - userId: userData(state).userId, + token: state.userSession.token, + userId: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', content: state.content.data, pathname: props.pathname, types: filter(state.types.types, 'addable'), diff --git a/src/components/manage/Toolbar/Toolbar.test.jsx b/src/components/manage/Toolbar/Toolbar.test.jsx index 4343432987..005f6fb9bc 100644 --- a/src/components/manage/Toolbar/Toolbar.test.jsx +++ b/src/components/manage/Toolbar/Toolbar.test.jsx @@ -8,114 +8,112 @@ import thunk from 'redux-thunk'; import { PluggablesProvider } from '@plone/volto/components/manage/Pluggable'; import Toolbar from './Toolbar'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; const mockStore = configureStore([thunk]); -const actions = { - document_actions: [], - object: [ - { - icon: '', - id: 'view', - title: 'View', - }, - { - icon: '', - id: 'edit', - title: 'Edit', - }, - { - icon: '', - id: 'folderContents', - title: 'Contents', - }, - { - icon: '', - id: 'history', - title: 'History', - }, - { - icon: '', - id: 'local_roles', - title: 'Sharing', - }, - ], - object_buttons: [ - { - icon: '', - id: 'cut', - title: 'Cut', - }, - { - icon: '', - id: 'copy', - title: 'Copy', - }, - { - icon: '', - id: 'delete', - title: 'Delete', - }, - { - icon: '', - id: 'rename', - title: 'Rename', - }, - { - icon: '', - id: 'ical_import_enable', - title: 'Enable icalendar import', - }, - ], - portal_tabs: [], - site_actions: [ - { - icon: '', - id: 'sitemap', - title: 'Site Map', - }, - { - icon: '', - id: 'accessibility', - title: 'Accessibility', - }, - { - icon: '', - id: 'contact', - title: 'Contact', - }, - ], - user: [ - { - icon: '', - id: 'preferences', - title: 'Preferences', - }, - { - icon: '', - id: 'dashboard', - title: 'Dashboard', - }, - { - icon: '', - id: 'plone_setup', - title: 'Site Setup', - }, - { - icon: '', - id: 'logout', - title: 'Log out', - }, - ], -}; -const actionsById = arrayWIdsToObject(actions); - describe('Toolbar', () => { it('renders the Toolbar component', () => { const store = mockStore({ types: { types: [{ title: 'Document', addable: true }] }, - actions: { actions, actionsById }, + actions: { + actions: { + document_actions: [], + object: [ + { + icon: '', + id: 'view', + title: 'View', + }, + { + icon: '', + id: 'edit', + title: 'Edit', + }, + { + icon: '', + id: 'folderContents', + title: 'Contents', + }, + { + icon: '', + id: 'history', + title: 'History', + }, + { + icon: '', + id: 'local_roles', + title: 'Sharing', + }, + ], + object_buttons: [ + { + icon: '', + id: 'cut', + title: 'Cut', + }, + { + icon: '', + id: 'copy', + title: 'Copy', + }, + { + icon: '', + id: 'delete', + title: 'Delete', + }, + { + icon: '', + id: 'rename', + title: 'Rename', + }, + { + icon: '', + id: 'ical_import_enable', + title: 'Enable icalendar import', + }, + ], + portal_tabs: [], + site_actions: [ + { + icon: '', + id: 'sitemap', + title: 'Site Map', + }, + { + icon: '', + id: 'accessibility', + title: 'Accessibility', + }, + { + icon: '', + id: 'contact', + title: 'Contact', + }, + ], + user: [ + { + icon: '', + id: 'preferences', + title: 'Preferences', + }, + { + icon: '', + id: 'dashboard', + title: 'Dashboard', + }, + { + icon: '', + id: 'plone_setup', + title: 'Site Setup', + }, + { + icon: '', + id: 'logout', + title: 'Log out', + }, + ], + }, + }, userSession: { token: jwt.sign({ fullname: 'John Doe' }, 'secret'), }, @@ -146,7 +144,105 @@ describe('Toolbar', () => { it('renders the Toolbar component with lock', () => { const store = mockStore({ types: { types: [{ title: 'Document', addable: true }] }, - actions: { actions, actionsById }, + actions: { + actions: { + document_actions: [], + object: [ + { + icon: '', + id: 'view', + title: 'View', + }, + { + icon: '', + id: 'edit', + title: 'Edit', + }, + { + icon: '', + id: 'folderContents', + title: 'Contents', + }, + { + icon: '', + id: 'history', + title: 'History', + }, + { + icon: '', + id: 'local_roles', + title: 'Sharing', + }, + ], + object_buttons: [ + { + icon: '', + id: 'cut', + title: 'Cut', + }, + { + icon: '', + id: 'copy', + title: 'Copy', + }, + { + icon: '', + id: 'delete', + title: 'Delete', + }, + { + icon: '', + id: 'rename', + title: 'Rename', + }, + { + icon: '', + id: 'ical_import_enable', + title: 'Enable icalendar import', + }, + ], + portal_tabs: [], + site_actions: [ + { + icon: '', + id: 'sitemap', + title: 'Site Map', + }, + { + icon: '', + id: 'accessibility', + title: 'Accessibility', + }, + { + icon: '', + id: 'contact', + title: 'Contact', + }, + ], + user: [ + { + icon: '', + id: 'preferences', + title: 'Preferences', + }, + { + icon: '', + id: 'dashboard', + title: 'Dashboard', + }, + { + icon: '', + id: 'plone_setup', + title: 'Site Setup', + }, + { + icon: '', + id: 'logout', + title: 'Log out', + }, + ], + }, + }, userSession: { token: jwt.sign({ fullname: 'John Doe' }, 'secret'), }, diff --git a/src/components/theme/Anontools/Anontools.jsx b/src/components/theme/Anontools/Anontools.jsx index 9ba2b4f1b5..8b50fcb659 100644 --- a/src/components/theme/Anontools/Anontools.jsx +++ b/src/components/theme/Anontools/Anontools.jsx @@ -9,9 +9,7 @@ import { connect } from 'react-redux'; import { Link } from 'react-router-dom'; import { Menu } from 'semantic-ui-react'; import { FormattedMessage } from 'react-intl'; - import config from '@plone/volto/registry'; -import { loggedIn } from '@plone/volto/selectors/userSession/userSession'; /** * Anontools container class. @@ -25,7 +23,7 @@ export class Anontools extends Component { * @static */ static propTypes = { - userLoggedIn: PropTypes.bool, + token: PropTypes.string, content: PropTypes.shape({ '@id': PropTypes.string, }), @@ -37,6 +35,7 @@ export class Anontools extends Component { * @static */ static defaultProps = { + token: null, content: { '@id': null, }, @@ -50,7 +49,7 @@ export class Anontools extends Component { render() { const { settings } = config; return ( - !this.props.userLoggedIn && ( + !this.props.token && ( ({ - userLoggedIn: loggedIn(state), + token: state.userSession.token, content: state.content.data, }))(Anontools); diff --git a/src/components/theme/Anontools/Anontools.test.jsx b/src/components/theme/Anontools/Anontools.test.jsx index 3b64f27631..628e214925 100644 --- a/src/components/theme/Anontools/Anontools.test.jsx +++ b/src/components/theme/Anontools/Anontools.test.jsx @@ -5,17 +5,13 @@ import { Provider } from 'react-intl-redux'; import { MemoryRouter } from 'react-router-dom'; import Anontools from './Anontools'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; const mockStore = configureStore(); -const actions = { user: [{ id: 'login' }] }; -const actionsById = arrayWIdsToObject(actions); - describe('Anontools', () => { - it('renders an anontools component when no use is logged in', () => { + it('renders an anontools component when no token is specified', () => { const store = mockStore({ - actions: { actions, actionsById }, + userSession: { token: null }, content: { data: { '@id': 'myid' } }, intl: { locale: 'en', @@ -33,10 +29,9 @@ describe('Anontools', () => { expect(json).toMatchSnapshot(); }); - it('should not render an anontools component when a user is logged in', () => { - const actions = { user: [{ id: 'logout' }] }; + it('should not render an anontools component when a token is specified', () => { const store = mockStore({ - actions: { actions, actionsById: arrayWIdsToObject(actions) }, + userSession: { token: '1234' }, content: { data: {} }, intl: { locale: 'en', diff --git a/src/components/theme/Anontools/__snapshots__/Anontools.test.jsx.snap b/src/components/theme/Anontools/__snapshots__/Anontools.test.jsx.snap index 68bfb1abfb..70b76d32f0 100644 --- a/src/components/theme/Anontools/__snapshots__/Anontools.test.jsx.snap +++ b/src/components/theme/Anontools/__snapshots__/Anontools.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Anontools renders an anontools component when no use is logged in 1`] = ` +exports[`Anontools renders an anontools component when no token is specified 1`] = `
@@ -19,4 +19,4 @@ exports[`Anontools renders an anontools component when no use is logged in 1`] =
`; -exports[`Anontools should not render an anontools component when a user is logged in 1`] = `null`; +exports[`Anontools should not render an anontools component when a token is specified 1`] = `null`; diff --git a/src/components/theme/App/App.jsx b/src/components/theme/App/App.jsx index 6d027ed5e4..2028678106 100644 --- a/src/components/theme/App/App.jsx +++ b/src/components/theme/App/App.jsx @@ -4,6 +4,7 @@ */ import React, { Component } from 'react'; +import jwtDecode from 'jwt-decode'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; @@ -37,10 +38,6 @@ import { getTypes, getWorkflow, } from '@plone/volto/actions'; -import { - loggedIn, - userData, -} from '@plone/volto/selectors/userSession/userSession'; import clearSVG from '@plone/volto/icons/clear.svg'; import MultilingualRedirector from '../MultilingualRedirector/MultilingualRedirector'; @@ -130,8 +127,8 @@ class App extends Component { [trim(join(split(this.props.pathname, '/'), ' section-'))]: this.props.pathname !== '/', siteroot: this.props.pathname === '/', - 'is-authenticated': !!this.props.userLoggedIn, - 'is-anonymous': !this.props.userLoggedIn, + 'is-authenticated': !!this.props.token, + 'is-anonymous': !this.props.token, 'cms-ui': isCmsUI, 'public-ui': !isCmsUI, })} @@ -186,7 +183,7 @@ class App extends Component { export const __test__ = connect( (state, props) => ({ pathname: props.location.pathname, - userLoggedIn: loggedIn(state), + token: state.userSession.token, content: state.content.data, apiError: state.apierror.error, connectionRefused: state.apierror.connectionRefused, @@ -231,8 +228,10 @@ export default compose( connect( (state, props) => ({ pathname: props.location.pathname, - userLoggedIn: loggedIn(state), - userId: userData(state).userId, + token: state.userSession.token, + userId: state.userSession.token + ? jwtDecode(state.userSession.token).sub + : '', content: state.content.data, apiError: state.apierror.error, connectionRefused: state.apierror.connectionRefused, diff --git a/src/components/theme/App/App.test.jsx b/src/components/theme/App/App.test.jsx index a6b2748998..2a34423864 100644 --- a/src/components/theme/App/App.test.jsx +++ b/src/components/theme/App/App.test.jsx @@ -6,7 +6,6 @@ import { MemoryRouter } from 'react-router-dom'; import config from '@plone/volto/registry'; import { __test__ as App } from './App'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; beforeAll(() => { config.settings.navDepth = 1; @@ -36,13 +35,12 @@ jest.mock('semantic-ui-react', () => ({ })); jest.mock('../Footer/Footer', () => jest.fn(() =>
- {!this.props.userLoggedIn && ( + {!this.props.token && (
@@ -75,5 +76,5 @@ class Header extends Component { } export default connect((state) => ({ - userLoggedIn: loggedIn(state), + token: state.userSession.token, }))(Header); diff --git a/src/components/theme/Header/Header.test.jsx b/src/components/theme/Header/Header.test.jsx index beb7ffa41b..8002a8cbba 100644 --- a/src/components/theme/Header/Header.test.jsx +++ b/src/components/theme/Header/Header.test.jsx @@ -4,7 +4,6 @@ import configureStore from 'redux-mock-store'; import { Provider } from 'react-intl-redux'; import Header from './Header'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; const mockStore = configureStore(); @@ -22,13 +21,10 @@ jest.mock('../LanguageSelector/LanguageSelector', () => jest.fn(() =>
), ); -const actions = { user: [{ id: 'login' }] }; -const actionsById = arrayWIdsToObject(actions); - describe('Header', () => { it('renders a header component', () => { const store = mockStore({ - actions: { actions, actionsById }, + userSession: { token: null }, intl: { locale: 'en', messages: {}, diff --git a/src/components/theme/Login/Login.test.jsx b/src/components/theme/Login/Login.test.jsx index 8f0021212b..d1c2c9265e 100644 --- a/src/components/theme/Login/Login.test.jsx +++ b/src/components/theme/Login/Login.test.jsx @@ -6,7 +6,6 @@ import { MemoryRouter } from 'react-router-dom'; import config from '@plone/volto/registry'; import Login from './Login'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; beforeAll(() => { config.settings.nonContentRoutes = []; @@ -15,13 +14,9 @@ beforeAll(() => { const mockStore = configureStore(); -const actions = { user: [{ id: 'login' }] }; -const actionsById = arrayWIdsToObject(actions); - describe('Login', () => { it('renders a login component', () => { const store = mockStore({ - actions: { actions, actionsById }, userSession: { login: {}, }, diff --git a/src/components/theme/View/LinkView.jsx b/src/components/theme/View/LinkView.jsx index 8e8c0fbdea..042f45b6ff 100644 --- a/src/components/theme/View/LinkView.jsx +++ b/src/components/theme/View/LinkView.jsx @@ -27,7 +27,7 @@ class LinkView extends Component { description: PropTypes.string, remoteUrl: PropTypes.string, }), - userLoggedIn: PropTypes.bool, + token: PropTypes.string, }; /** @@ -37,10 +37,11 @@ class LinkView extends Component { */ static defaultProps = { content: null, + token: null, }; componentDidMount() { - if (!this.props.userLoggedIn) { + if (!this.props.token) { const { remoteUrl } = this.props.content; if (isInternalURL(remoteUrl)) { this.props.history.replace(flattenToAppURL(remoteUrl)); diff --git a/src/components/theme/View/LinkView.test.jsx b/src/components/theme/View/LinkView.test.jsx index 3b3905e382..00fc7e6f40 100644 --- a/src/components/theme/View/LinkView.test.jsx +++ b/src/components/theme/View/LinkView.test.jsx @@ -7,7 +7,7 @@ test('renders a link view component', () => { const component = renderer.create( {config.settings.showTags && @@ -267,7 +266,7 @@ export default compose( connect( (state, props) => ({ actions: state.actions.actions, - userLoggedIn: loggedIn(state), + token: state.userSession.token, content: state.content.data, error: state.content.get.error, apiError: state.apierror.error, diff --git a/src/components/theme/View/View.test.jsx b/src/components/theme/View/View.test.jsx index 8cf86c13ab..b962b91fb5 100644 --- a/src/components/theme/View/View.test.jsx +++ b/src/components/theme/View/View.test.jsx @@ -5,7 +5,6 @@ import { Provider } from 'react-intl-redux'; import View from './View'; import config from '@plone/volto/registry'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; beforeAll(() => { config.set('views', { @@ -130,18 +129,18 @@ const actions = { }, { icon: '', - id: 'login', - title: 'Log in', + id: 'logout', + title: 'Log out', }, ], }; -const actionsById = arrayWIdsToObject(actions); describe('View', () => { it('renders an empty view', () => { const store = mockStore({ - actions: { actions, actionsById }, + actions: { actions }, content: { get: { error: null } }, + userSession: { token: null }, apierror: {}, intl: { locale: 'en', @@ -159,8 +158,9 @@ describe('View', () => { it('renders a summary view', () => { const store = mockStore({ - actions: { actions, actionsById }, + actions: { actions }, content: { data: { layout: 'summary_view' }, get: { error: null } }, + userSession: { token: null }, apierror: {}, intl: { locale: 'en', @@ -178,8 +178,9 @@ describe('View', () => { it('renders a tabular view', () => { const store = mockStore({ - actions: { actions, actionsById }, + actions: { actions }, content: { data: { layout: 'tabular_view' }, get: { error: null } }, + userSession: { token: null }, apierror: {}, intl: { locale: 'en', @@ -197,8 +198,9 @@ describe('View', () => { it('renders a document view', () => { const store = mockStore({ - actions: { actions, actionsById }, + actions: { actions }, content: { data: {}, get: { error: null } }, + userSession: { token: null }, apierror: {}, intl: { locale: 'en', diff --git a/src/config/RichTextEditor/ToHTML.jsx b/src/config/RichTextEditor/ToHTML.jsx index 4d17c4554b..1a3a12446f 100644 --- a/src/config/RichTextEditor/ToHTML.jsx +++ b/src/config/RichTextEditor/ToHTML.jsx @@ -1,8 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; import { isEmpty } from 'lodash'; - -import { loggedIn } from '@plone/volto/selectors/userSession/userSession'; import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink'; const styles = { @@ -221,9 +219,9 @@ const blocks = { }; const LinkEntity = connect((state) => ({ - userLoggedIn: loggedIn(state), -}))(({ userLoggedIn, key, url, target, targetUrl, download, children }) => { - const to = userLoggedIn ? url : targetUrl || url; + token: state.userSession.token, +}))(({ token, key, url, target, targetUrl, download, children }) => { + const to = token ? url : targetUrl || url; return ( { return language; }; -/** - * Create an object with properties for each object with an `id` in an array - * @function arrayWIdsToObject - * @param {Array} An array of objects, each has an `id` property with a string value - * @returns {Object} Oobject with property for each array object keyed by the `id` - */ -export function arrayWIdsToObject(arrayOfObjs) { - /* The user may be authenticated by different means, including outside the UI. Defer - * to the response from Plone, sepcifically whether Plone presents an option to log - * in. */ - return Object.fromEntries( - Object.entries(arrayOfObjs).map((entry) => [ - entry[0], - Object.fromEntries(entry[1].map((action) => [action.id, action])), - ]), - ); -} - /** * Lookup if a given expander is set in apiExpanders * @param {string} language Language to be normalized diff --git a/src/reducers/actions/actions.js b/src/reducers/actions/actions.js index 8191d4f722..4a95ef7ee4 100644 --- a/src/reducers/actions/actions.js +++ b/src/reducers/actions/actions.js @@ -4,7 +4,6 @@ */ import { LIST_ACTIONS } from '@plone/volto/constants/ActionTypes'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; const initialState = { error: null, @@ -19,7 +18,6 @@ const initialState = { loaded: false, loading: false, }; -initialState.actionsById = arrayWIdsToObject(initialState.actions); /** * Actions reducer. @@ -40,11 +38,8 @@ export default function actions(state = initialState, action = {}) { case `${LIST_ACTIONS}_SUCCESS`: return { ...state, - /* Also transform the arrays of actions into objects of actions by action id. - * Makes it much easier to check for the presence of an action. */ error: null, actions: action.result, - actionsById: arrayWIdsToObject(action.result), loaded: true, loading: false, }; diff --git a/src/reducers/actions/actions.test.js b/src/reducers/actions/actions.test.js index 0a1d49febb..d00d51ce7a 100644 --- a/src/reducers/actions/actions.test.js +++ b/src/reducers/actions/actions.test.js @@ -1,23 +1,18 @@ import actions from './actions'; import { LIST_ACTIONS } from '@plone/volto/constants/ActionTypes'; -import { arrayWIdsToObject } from '@plone/volto/helpers/Utils/Utils'; - -const actionsFromApi = { - object: [], - object_buttons: [], - site_actions: [], - user: [], - document_actions: [], - portal_tabs: [], -}; -const actionsById = arrayWIdsToObject(actionsFromApi); describe('Actions reducer', () => { it('should return the initial state', () => { expect(actions()).toEqual({ error: null, - actions: actionsFromApi, - actionsById, + actions: { + object: [], + object_buttons: [], + site_actions: [], + user: [], + document_actions: [], + portal_tabs: [], + }, loaded: false, loading: false, }); @@ -30,8 +25,14 @@ describe('Actions reducer', () => { }), ).toEqual({ error: null, - actions: actionsFromApi, - actionsById, + actions: { + object: [], + object_buttons: [], + site_actions: [], + user: [], + document_actions: [], + portal_tabs: [], + }, loaded: false, loading: true, }); @@ -102,35 +103,6 @@ describe('Actions reducer', () => { document_actions: [], portal_tabs: [], }, - actionsById: { - document_actions: {}, - object: {}, - object_buttons: {}, - portal_tabs: {}, - site_actions: {}, - user: { - preferences: { - icon: '', - id: 'preferences', - title: 'Preferences', - }, - dashboard: { - icon: '', - id: 'dashboard', - title: 'Dashboard', - }, - logout: { - icon: '', - id: 'logout', - title: 'Log out', - }, - plone_setup: { - icon: '', - id: 'plone_setup', - title: 'Site Setup', - }, - }, - }, loaded: true, loading: false, }); @@ -145,14 +117,6 @@ describe('Actions reducer', () => { ).toEqual({ error: 'failed', actions: {}, - actionsById: { - document_actions: {}, - object: {}, - object_buttons: {}, - portal_tabs: {}, - site_actions: {}, - user: {}, - }, loaded: false, loading: false, }); diff --git a/src/selectors/userSession/userSession.js b/src/selectors/userSession/userSession.js deleted file mode 100644 index d548c49e1d..0000000000 --- a/src/selectors/userSession/userSession.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Authenticated user state selectors. - * @module selectors/userSession/userSession - */ - -import jwtDecode from 'jwt-decode'; - -/** - * Is a user logged in selector - * @function loggedIn - * @param {Object} state Current full Redux store state. - * @returns {boolean} `true` if a user is currently authenticated, `false` otherwise. - */ -export function loggedIn(state) { - /* The user may be authenticated by different means, including outside the UI. Defer - * to the response from Plone, sepcifically whether Plone presents an option to log - * in. */ - return !state.actions.actionsById.user.login; -} - -/** - * Retreive user data for the currently authenticated user - * @function userData - * @param {Object} state Current full Redux store state. - * @returns {Object} An object with properties for each user data - */ -export function userData(state) { - return { - userId: state.userSession.token - ? jwtDecode(state.userSession.token).sub - : '', - }; -} diff --git a/traefik.d/local.yml b/traefik.d/local.yml deleted file mode 100644 index 8e3321a3da..0000000000 --- a/traefik.d/local.yml +++ /dev/null @@ -1,50 +0,0 @@ -# Proxy the back-end and front-end running on the local host -http: - routers: - - router0: - entryPoints: - - "web" - service: "plone" - rule: "PathPrefix(`/api`)" - middlewares: - - "strip-api-prefix" - - "rewrite-api-vhost" - - router1: - entryPoints: - - "web" - service: "frontend" - rule: "PathPrefix(`/`)" - # FIXME: It should be possible to host the UI at a path prefix but currently isn't - # because of how the UI app code assembles API URLs - # rule: "PathPrefix(`/ui`)" - # middlewares: - # - "strip-ui-prefix" - - middlewares: - - strip-api-prefix: - stripprefix: - prefixes: - - "/api" - rewrite-api-vhost: - replacepathregex: - regex: "^(.*)$" - replacement: "/VirtualHostBase/http/localhost:49080/VirtualHostRoot/_vh_api$1" - - # strip-ui-prefix: - # stripprefix: - # prefixes: - # - "/ui" - - services: - plone: - loadBalancer: - servers: - - url: "http://192.168.80.1:8080/" - - frontend: - loadBalancer: - servers: - - url: "http://192.168.80.1:3000/"