diff --git a/docs/source/addons/index.md b/docs/source/addons/index.md index 94dd2a6c12..ed13a38d17 100644 --- a/docs/source/addons/index.md +++ b/docs/source/addons/index.md @@ -15,6 +15,7 @@ myst: i18n best-practices theme +public-folder ``` There are several advanced scenarios where we might want to have more control diff --git a/docs/source/addons/public-folder.md b/docs/source/addons/public-folder.md new file mode 100644 index 0000000000..188a9e155a --- /dev/null +++ b/docs/source/addons/public-folder.md @@ -0,0 +1,29 @@ +--- +myst: + html_meta: + "description": "How to add static served files from your add-on to your build" + "property=og:description": "How to add static served files to the build from an add-on" + "property=og:title": "Add static files from your add-on to your build" + "keywords": "Volto, Plone, Semantic UI, CSS, Volto theme, add-on, static, assets, files, build" +--- + +# Add static files from your add-on to your build + +In the Volto build process, you can add static files to your build, then serve them along with the compiled files. +Static files are not transformed or compiled by the build process. +They are served as is from the root of the Volto site. +It is useful to define static files such as the following: + +- {file}`robots.txt` +- favicon files +- manifest files +- any other static files + + +## Procedure to include static files + +Create a folder named `public` at the root of your add-on, and add the static files to it. +The build process will copy the files, taking into account all add-ons' defined order. +The build process copies first the static files defined by Volto, then the static files from add-ons as defined by their configuration order. +The last defined file overwrites any previously defined files. + diff --git a/docs/source/client/quick-start.md b/docs/source/client/quick-start.md index 63ed27773f..c3c8944dc8 100644 --- a/docs/source/client/quick-start.md +++ b/docs/source/client/quick-start.md @@ -37,6 +37,7 @@ import ploneClient from '@plone/client'; const client = ploneClient.initialize({ apiPath: 'http://localhost:8080/Plone', + token: '', // Optional: auth_token to authorize the user }); ``` @@ -64,6 +65,7 @@ import { usePathname } from 'next/navigation'; const client = ploneClient.initialize({ apiPath: 'http://localhost:8080/Plone', + token: '', // Optional: auth_token to authorize the user }); export default function Title() { diff --git a/docs/source/configuration/settings-reference.md b/docs/source/configuration/settings-reference.md index 87ba1afce2..b42797413c 100644 --- a/docs/source/configuration/settings-reference.md +++ b/docs/source/configuration/settings-reference.md @@ -522,11 +522,4 @@ criticalCssPath this file exists it is loaded and its content is embedded inline into the generated HTML. By default this path is `public/critical.css`. See the {doc}`../deploying/performance` section for more details. - -extractScripts - An object that allows you to configure the insertion of scripts on the page - in some particular cases. - For the moment it admits only one property: `errorPages` whose value is a Boolean. - - If `extractScripts.errorPages` is `true`, the JS will be inserted into the error page. ``` diff --git a/docs/source/release-notes/index.md b/docs/source/release-notes/index.md index bc714d07d0..bac17f20ad 100644 --- a/docs/source/release-notes/index.md +++ b/docs/source/release-notes/index.md @@ -17,6 +17,64 @@ myst: +## 18.0.0-alpha.35 (2024-06-13) + +### Breaking + +- Improve container detection, `config.settings.containerBlockTypes` is no longer needed @sneridagh [#6099](https://github.com/plone/volto/issues/6099) + +### Bugfix + +- Support nested directories in public folder add-on sync folders both in dev and build mode @sneridagh [#6098](https://github.com/plone/volto/issues/6098) +- export getFieldURL from Url.js in helpers @dobri1408 [#6100](https://github.com/plone/volto/issues/6100) + +## 18.0.0-alpha.34 (2024-06-13) + +### Feature + +- Added blocks layout navigator @robgietema @sneridagh [#5642](https://github.com/plone/volto/issues/5642) +- Add support for reading the add-ons `tsconfig.json` paths and add them to the build resolve aliases @sneridagh [#6096](https://github.com/plone/volto/issues/6096) + +### Bugfix + +- Fix internalUrl Widget to Reflect Prop Changes via onChangeBlock @dorbi1408 @ichim-david [#6036](https://github.com/plone/volto/issues/6036) +- Add default 'l' and 'center' values to size and align fields of `Image` block. + This fixes data not having any value adding proper options to the `Image` block. @ichim-david [#6046](https://github.com/plone/volto/issues/6046) +- Fix public folder in dev mode, now it starts by default with the default Volto core defined public files @sneridagh [#6081](https://github.com/plone/volto/issues/6081) +- Fix link in pop-up in `RelationsMatrix.jsx`. @stevepiercy [#6085](https://github.com/plone/volto/issues/6085) +- Fix Uncaught RangeError: date value is not finite in DateTimeFormat.format. @mauritsvanrees [#6087](https://github.com/plone/volto/issues/6087) +- relations control panel. Restrict eglible relation targets according relation constraints of fields vocabulary. @ksuess [#6091](https://github.com/plone/volto/issues/6091) +- Better `Icon` component JSDoc typings @sneridagh [#6095](https://github.com/plone/volto/issues/6095) + +## 18.0.0-alpha.33 (2024-06-06) + +### Breaking + +- Fix JavaScript events association on error pages. Also remove settings `config.settings.serverConfig.extractScripts.errorPages`. Now scripts are added to error pages, regardless of whether we are in production mode or not. @wesleybl [#6048](https://github.com/plone/volto/issues/6048) +- Breaking from the original slots implementation: + Now `config.getSlots` in the configuration registry takes the argument `location` instead of `pathname`. + This allows to have more expressive conditions, and fulfill the use case of the `Add` form. + @sneridagh [#6063](https://github.com/plone/volto/issues/6063) + +### Feature + +- Added object browser icon view @robgietema [#5279](https://github.com/plone/volto/issues/5279) +- Refactor TextWidget. @Tishasoumya-02 [#6020](https://github.com/plone/volto/issues/6020) +- Refactor IdWidget -@Tishasoumya-02 [#6027](https://github.com/plone/volto/issues/6027) +- The `ContentTypeCondition` now supports the `Add` form, and detects when you create a content type that is set in the condition. @sneridagh + Added a new `BodyClass` helper while adding a new content type of the form `is-adding-contenttype-mycontenttype`. @sneridagh [#6063](https://github.com/plone/volto/issues/6063) +- Add support for configurable `public` directory defined per add-on. @sneridagh [#6072](https://github.com/plone/volto/issues/6072) + +### Bugfix + +- Fix block chooser search is not focusable when clicked on add button @iRohitSingh [#5866](https://github.com/plone/volto/issues/5866) +- Fixed skiplink links not tracking focus correctly @JeffersonBledsoe [#5959](https://github.com/plone/volto/issues/5959) +- Remove left and right padding from _Event > Edit recurrence > Repeat on_ buttons when repeating for weekly or yearly events for big fonts, preventing overflow. @sabrina-bongiovanni [#6070](https://github.com/plone/volto/issues/6070) + +### Internal + +- Fix test script in monorepo root @sneridagh [#6051](https://github.com/plone/volto/issues/6051) + ## 18.0.0-alpha.32 (2024-05-23) ### Feature diff --git a/docs/source/upgrade-guide/index.md b/docs/source/upgrade-guide/index.md index 119f96f537..5d89860e86 100644 --- a/docs/source/upgrade-guide/index.md +++ b/docs/source/upgrade-guide/index.md @@ -40,6 +40,14 @@ For this purpose, we have developed a {ref}`new utility +## 1.0.0-alpha.16 (2024-06-06) + +### Bugfix + +- Fixed querystring search query type @pnicolli [#6034](https://github.com/plone/volto/pull/6034) +- Fixed login mutation @sneridagh [#6053](https://github.com/plone/volto/pull/6053) + +### Internal + +- Remove custom test runner, using `vitest` config instead @sneridagh [#6056](https://github.com/plone/volto/pull/6056) + ## 1.0.0-alpha.15 (2024-05-23) ### Breaking diff --git a/packages/client/news/6034.bugfix b/packages/client/news/6034.bugfix deleted file mode 100644 index c4ca1815a9..0000000000 --- a/packages/client/news/6034.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed querystring search query type @pnicolli diff --git a/packages/client/news/6053.bugfix b/packages/client/news/6053.bugfix deleted file mode 100644 index ce7c60c564..0000000000 --- a/packages/client/news/6053.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed login mutation @sneridagh diff --git a/packages/client/news/6056.internal b/packages/client/news/6056.internal deleted file mode 100644 index e0091b0624..0000000000 --- a/packages/client/news/6056.internal +++ /dev/null @@ -1 +0,0 @@ -Remove custom test runner, using `vitest` config instead @sneridagh diff --git a/packages/client/news/6076.documentation b/packages/client/news/6076.documentation new file mode 100644 index 0000000000..138045af4e --- /dev/null +++ b/packages/client/news/6076.documentation @@ -0,0 +1 @@ +Add documentation about optional `token` parameter for `ploneClient` initialization. @MAX-786 \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json index 6d7a5020b0..e3563d194d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,7 @@ } ], "license": "MIT", - "version": "1.0.0-alpha.15", + "version": "1.0.0-alpha.16", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8b6e851f3e..09194352a8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,6 +8,13 @@ +## 2.0.0-alpha.11 (2024-06-06) + +### Bugfix + +- Fix ignored classname in breadcrumbs RAC @gomez [#6018](https://github.com/plone/volto/issues/6018) +- Make css layer more specific @pnicolli [#6065](https://github.com/plone/volto/issues/6065) + ## 2.0.0-alpha.10 (2024-05-30) ### Bugfix diff --git a/packages/components/news/6018.bugfix b/packages/components/news/6018.bugfix deleted file mode 100644 index 3761764bc6..0000000000 --- a/packages/components/news/6018.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix ignored classname in breadcrumbs RAC @gomez diff --git a/packages/components/news/6065.bugfix b/packages/components/news/6065.bugfix deleted file mode 100644 index c3f1a69561..0000000000 --- a/packages/components/news/6065.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make css layer more specific @pnicolli diff --git a/packages/components/package.json b/packages/components/package.json index a8d0662114..34e00c6126 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -8,7 +8,7 @@ } ], "license": "MIT", - "version": "2.0.0-alpha.10", + "version": "2.0.0-alpha.11", "repository": { "type": "git", "url": "http://github.com/plone/components.git" diff --git a/packages/coresandbox/src/components/Blocks/InputBlock/Data.tsx b/packages/coresandbox/src/components/Blocks/InputBlock/Data.tsx new file mode 100644 index 0000000000..be3693be48 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/InputBlock/Data.tsx @@ -0,0 +1,31 @@ +import { useIntl } from 'react-intl'; +import { BlockDataForm } from '@plone/volto/components/manage/Form'; +import type { BlockEditProps } from '@plone/types'; + +const InputBlockData = (props: BlockEditProps) => { + const { block, blocksConfig, contentType, data, navRoot, onChangeBlock } = + props; + const intl = useIntl(); + const schema = blocksConfig[data['@type']].blockSchema({ intl, props }); + + return ( + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + onChangeBlock={onChangeBlock} + formData={data} + blocksConfig={blocksConfig} + navRoot={navRoot} + contentType={contentType} + /> + ); +}; + +export default InputBlockData; diff --git a/packages/coresandbox/src/components/Blocks/InputBlock/Edit.tsx b/packages/coresandbox/src/components/Blocks/InputBlock/Edit.tsx new file mode 100644 index 0000000000..543f4aeba8 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/InputBlock/Edit.tsx @@ -0,0 +1,50 @@ +import React, { useEffect } from 'react'; +import { SidebarPortal } from '@plone/volto/components'; +import Data from './Data'; +import type { BlockEditProps } from '@plone/types'; +import { Input, Button } from 'semantic-ui-react'; +import { Icon } from '@plone/volto/components'; +import aheadSVG from '@plone/volto/icons/ahead.svg'; + +const InputBlockEdit = (props: BlockEditProps) => { + const { selected, block, data, onChangeBlock } = props; + const [url, setUrl] = React.useState(data?.url); + + useEffect(() => { + setUrl(data?.url); + }, [data?.url]); + + return ( + <> +
Input Block Edit
+ + setUrl(e.target.value)} + placeholder={ + 'Change url to check if the widgets from sidebar are getting updated' + } + value={url} + id="input_block" + /> + + + + + + ); +}; + +export default InputBlockEdit; diff --git a/packages/coresandbox/src/components/Blocks/InputBlock/View.tsx b/packages/coresandbox/src/components/Blocks/InputBlock/View.tsx new file mode 100644 index 0000000000..d3605c5ab0 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/InputBlock/View.tsx @@ -0,0 +1,12 @@ +import type { BlockViewProps } from '@plone/types'; + +const InputBlockView = (props: BlockViewProps) => { + return ( +
+
Input Block
+

{JSON.stringify(props.data)}

+
+ ); +}; + +export default InputBlockView; diff --git a/packages/coresandbox/src/components/Blocks/InputBlock/schema.ts b/packages/coresandbox/src/components/Blocks/InputBlock/schema.ts new file mode 100644 index 0000000000..0451ac4f70 --- /dev/null +++ b/packages/coresandbox/src/components/Blocks/InputBlock/schema.ts @@ -0,0 +1,19 @@ +import type { BlockConfigBase } from '@plone/types'; + +export const inputBlockSchema: BlockConfigBase['blockSchema'] = ({ intl }) => ({ + title: 'Input Block', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['url'], + }, + ], + properties: { + url: { + widget: 'internal_url', + title: 'url', + }, + }, + required: [], +}); diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index b5c04d775b..264be43d9b 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -2,8 +2,11 @@ import ListingBlockVariationTeaserContent from './components/Blocks/Listing/List import NewsAndEvents from './components/Views/NewsAndEvents'; import TestBlockView from './components/Blocks/TestBlock/View'; import TestBlockEdit from './components/Blocks/TestBlock/Edit'; +import InputBlockView from './components/Blocks/InputBlock/View'; +import InputBlockEdit from './components/Blocks/InputBlock/Edit'; import { flattenToAppURL } from '@plone/volto/helpers'; import { SliderSchema as TestBlockSchema } from './components/Blocks/TestBlock/schema'; +import { inputBlockSchema } from './components/Blocks/InputBlock/schema'; import { multipleFieldsetsSchema } from './components/Blocks/TestBlock/schema'; import { conditionalVariationsSchemaEnhancer } from './components/Blocks/schemaEnhancers'; import codeSVG from '@plone/volto/icons/code.svg'; @@ -37,6 +40,20 @@ const testBlock: BlockConfigBase = { ], extensions: {}, }; +const inputBlock: BlockConfigBase = { + id: 'inputBlock', + title: 'Input Block', + icon: codeSVG, + group: 'common', + view: InputBlockView, + edit: InputBlockEdit, + blockSchema: inputBlockSchema, + restricted: false, + mostUsed: true, + sidebarTab: 1, + + extensions: {}, +}; const testBlockConditional: BlockConfigBase = { ...testBlock, @@ -154,6 +171,7 @@ export const workingCopyFixture = (config: ConfigType) => { declare module '@plone/types' { export interface BlocksConfigData { testBlock: BlockConfigBase; + inputBlock: BlockConfigBase; testBlockConditional: BlockConfigBase; testBlockWithConditionalVariations: BlockConfigBase; testBlockMultipleFieldsets: BlockConfigBase; @@ -164,6 +182,7 @@ declare module '@plone/types' { const applyConfig = (config: ConfigType) => { config.blocks.blocksConfig.testBlock = testBlock; + config.blocks.blocksConfig.inputBlock = inputBlock; config.blocks.blocksConfig.testBlockConditional = testBlockConditional; config.blocks.blocksConfig.testBlockWithConditionalVariations = testBlockWithConditionalVariations; diff --git a/packages/registry/CHANGELOG.md b/packages/registry/CHANGELOG.md index 05faaac055..78db32970c 100644 --- a/packages/registry/CHANGELOG.md +++ b/packages/registry/CHANGELOG.md @@ -8,6 +8,12 @@ +## 1.6.0 (2024-06-13) + +### Feature + +- Add support for reading the add-ons `tsconfig.json` paths and add them to the build resolve aliases @sneridagh [#6096](https://github.com/plone/volto/issues/6096) + ## 1.5.7 (2024-05-15) ### Bugfix diff --git a/packages/registry/package.json b/packages/registry/package.json index 8d6772574b..e23cc91c80 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "1.5.7", + "version": "1.6.0", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" diff --git a/packages/registry/src/addon-registry.js b/packages/registry/src/addon-registry.js index 29de18b88c..e137ddd33f 100644 --- a/packages/registry/src/addon-registry.js +++ b/packages/registry/src/addon-registry.js @@ -166,12 +166,12 @@ class AddonConfigurationRegistry { * Returns a tuple `[baseUrl, pathsConfig]` * */ - getTSConfigPaths() { + getTSConfigPaths(rootPath = this.projectRootPath) { let configFile; - if (fs.existsSync(`${this.projectRootPath}/tsconfig.json`)) - configFile = `${this.projectRootPath}/tsconfig.json`; - else if (fs.existsSync(`${this.projectRootPath}/jsconfig.json`)) - configFile = `${this.projectRootPath}/jsconfig.json`; + if (fs.existsSync(`${rootPath}/tsconfig.json`)) + configFile = `${rootPath}/tsconfig.json`; + else if (fs.existsSync(`${rootPath}/jsconfig.json`)) + configFile = `${rootPath}/jsconfig.json`; let pathsConfig; let baseUrl; @@ -265,6 +265,8 @@ class AddonConfigurationRegistry { if (!this.addonNames.includes(name)) this.addonNames.push(name); }); } + const packageTSConfig = this.getTSConfigPaths(basePath); + this.packages[name] = { name, version: pkg.version, @@ -272,6 +274,8 @@ class AddonConfigurationRegistry { isRegisteredAddon: this.addonNames.includes(name), modulePath, packageJson, + basePath, + tsConfigPaths: packageTSConfig[1] ? packageTSConfig : null, addons: pkg.addons || [], }; } @@ -366,7 +370,31 @@ class AddonConfigurationRegistry { } /** - * Returns a mapping name:diskpath to be uses in webpack's resolve aliases + * Returns a list of aliases given the defined paths in `tsconfig.json` + */ + getAliasesFromTSConfig(basePath, tsConfig) { + const [baseUrl, options] = tsConfig; + const fullPathsPath = baseUrl ? `${basePath}/${baseUrl}` : basePath; + + let aliases = {}; + Object.keys(options || {}).forEach((item) => { + const name = item.replace(/\/\*$/, ''); + // webpack5 allows arrays here, fix later + const value = path.resolve( + fullPathsPath, + options[item][0].replace(/\/\*$/, ''), + ); + + aliases[name] = value; + }); + + return aliases; + } + + /** + * Returns a mapping name:diskpath to be uses in webpack's resolve aliases. + * It includes all registered add-ons and their `src` paths, and also the paths + * defined in the `tsconfig.json` files of the add-ons. */ getResolveAliases() { const pairs = [ @@ -376,7 +404,20 @@ class AddonConfigurationRegistry { ]), ]; - return fromEntries(pairs); + let aliasesFromTSPaths = {}; + Object.keys(this.packages).forEach((o) => { + if (this.packages[o].tsConfigPaths) { + aliasesFromTSPaths = { + ...aliasesFromTSPaths, + ...this.getAliasesFromTSConfig( + this.packages[o].basePath, + this.packages[o].tsConfigPaths, + ), + }; + } + }); + + return { ...fromEntries(pairs), ...aliasesFromTSPaths }; } /** diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index d6b4fdca86..34cce11565 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -8,6 +8,16 @@ +## 3.6.2 (2024-06-06) + +### Bugfix + +- 'Fix `poToJson` script, making it support `volto.config.js` @sneridagh [#6073](https://github.com/plone/volto/issues/6073) + +### Documentation + +- Improve comments @sneridagh [#6072](https://github.com/plone/volto/issues/6072) + ## 3.6.1 (2024-03-18) ### Bugfix diff --git a/packages/scripts/i18n.cjs b/packages/scripts/i18n.cjs index 1eace16642..bd4a1a1f87 100755 --- a/packages/scripts/i18n.cjs +++ b/packages/scripts/i18n.cjs @@ -13,7 +13,6 @@ const babel = require('@babel/core'); const path = require('path'); const projectRootPath = path.resolve('.'); -const packageJson = require(path.join(projectRootPath, 'package.json')); const { program } = require('commander'); const chalk = require('chalk'); @@ -155,9 +154,9 @@ function poToJson({ registry, addonMode }) { (item.comments[0] && item.comments[0].startsWith('. Default: ') ? item.comments[0].replace('. Default: ', '') : item.comments[0] && - item.comments[0].startsWith('defaultMessage:') - ? item.comments[0].replace('defaultMessage: ', '') - : '') + item.comments[0].startsWith('defaultMessage:') + ? item.comments[0].replace('defaultMessage: ', '') + : '') : item.msgstr[0]; } }); @@ -182,9 +181,14 @@ function poToJson({ registry, addonMode }) { } if (!addonMode) { - // Merge addons locales - if (packageJson.addons) { - registry.getAddonDependencies().forEach((addon) => { + // Merge addons locales - using getAddonDependencies because it preserves + // the order of the addons in the registry, even if they are add-on dependencies + // of an add-on + registry.getAddonDependencies().forEach((addonDep) => { + // What comes from getAddonDependencies is in the form of `@package/addon:profile` + const addon = addonDep.split(':')[0]; + // Check if the addon is available in the registry, just in case + if (registry.packages[addon]) { const addonlocale = `${registry.packages[addon].modulePath}/../${filename}`; if (fs.existsSync(addonlocale)) { const addonItems = Pofile.parse( @@ -197,9 +201,10 @@ function poToJson({ registry, addonMode }) { console.log(`Merging ${addon} locales for ${lang}`); } } - }); - } + } + }); } + // Merge project locales, the project customization wins mergeMessages(result, projectLocalesItems, lang); fs.writeFileSync(`locales/${lang}.json`, JSON.stringify(result)); diff --git a/packages/scripts/package.json b/packages/scripts/package.json index a52c30c938..66d18276c1 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -9,7 +9,7 @@ } ], "license": "MIT", - "version": "3.6.1", + "version": "3.6.2", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index dc29dc7b3c..f3dc2a8b8f 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -8,6 +8,29 @@ +## 1.0.0-alpha.16 (2024-06-13) + +### Breaking + +- Remove unused `config.settings.containerBlockTypes` @sneridagh [#6099](https://github.com/plone/volto/issues/6099) + +## 1.0.0-alpha.15 (2024-06-13) + +### Bugfix + +- Better `styleClassNameExtenders` typings @sneridagh [#6095](https://github.com/plone/volto/issues/6095) + +## 1.0.0-alpha.14 (2024-06-06) + +### Breaking + +- The `GetSlotArgs` type no longer supports `pathname` as a key, instead using `location`. @sneridagh [#6063](https://github.com/plone/volto/issues/6063) + +### Bugfix + +- BlockExtension as Interface @sneridagh [#6049](https://github.com/plone/volto/issues/6049) +- Improved image typings @pnicolli [#6064](https://github.com/plone/volto/issues/6064) + ## 1.0.0-alpha.13 (2024-05-23) ### Feature diff --git a/packages/types/news/6049.bugfix b/packages/types/news/6049.bugfix deleted file mode 100644 index 943b532f2f..0000000000 --- a/packages/types/news/6049.bugfix +++ /dev/null @@ -1 +0,0 @@ - BlockExtension as Interface @sneridagh diff --git a/packages/types/news/6063.breaking b/packages/types/news/6063.breaking deleted file mode 100644 index f6b506818a..0000000000 --- a/packages/types/news/6063.breaking +++ /dev/null @@ -1 +0,0 @@ -The `GetSlotArgs` type no longer supports `pathname` as a key, instead using `location`. @sneridagh diff --git a/packages/types/news/6064.bugfix b/packages/types/news/6064.bugfix deleted file mode 100644 index a7e6ecab64..0000000000 --- a/packages/types/news/6064.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improved image typings @pnicolli diff --git a/packages/types/package.json b/packages/types/package.json index 9714d1afa7..d40f32daa1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "1.0.0-alpha.13", + "version": "1.0.0-alpha.16", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" diff --git a/packages/types/src/config/Settings.d.ts b/packages/types/src/config/Settings.d.ts index acf2da7ae4..68df1ab205 100644 --- a/packages/types/src/config/Settings.d.ts +++ b/packages/types/src/config/Settings.d.ts @@ -1,3 +1,6 @@ +import { Content } from '../content'; +import { BlocksConfigData } from './Blocks'; + type apiExpandersType = | { match: string; GET_CONTENT: string[] } | { @@ -8,6 +11,18 @@ type apiExpandersType = | (() => { [key: string]: string }); }; +type styleClassNameExtendersType = ({ + block, + content, + data, + classNames, +}: { + block: string; + content: Content; + data: BlocksConfigData; + classNames: string[]; +}) => string[]; + export interface SettingsConfig { [key: string]: unknown; host: string; @@ -78,11 +93,10 @@ export interface SettingsConfig { errorHandlers: unknown[]; styleClassNameConverters: unknown; hashLinkSmoothScroll: boolean; - styleClassNameExtenders: unknown; + styleClassNameExtenders: styleClassNameExtendersType[]; querystringSearchGet: boolean; blockSettingsTabFieldsetsInitialStateOpen: boolean; excludeLinksAndReferencesMenuItem: boolean; - containerBlockTypes: string[]; siteTitleFormat: { includeSiteTitle: boolean; titleAndSiteTitleSeparator: string; diff --git a/packages/volto-slate/CHANGELOG.md b/packages/volto-slate/CHANGELOG.md index fb2885ca27..7c9c204ef2 100644 --- a/packages/volto-slate/CHANGELOG.md +++ b/packages/volto-slate/CHANGELOG.md @@ -8,6 +8,12 @@ +## 18.0.0-alpha.13 (2024-06-13) + +### Bugfix + +- Fix removal of slate formatting applied to text when toggling the list buttons @robgietema [#6080](https://github.com/plone/volto/issues/6080) + ## 18.0.0-alpha.12 (2024-04-23) ### Bugfix diff --git a/packages/volto-slate/package.json b/packages/volto-slate/package.json index 8937fe8037..a663a4ae54 100644 --- a/packages/volto-slate/package.json +++ b/packages/volto-slate/package.json @@ -1,6 +1,6 @@ { "name": "@plone/volto-slate", - "version": "18.0.0-alpha.12", + "version": "18.0.0-alpha.13", "description": "Slate.js integration with Volto", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", diff --git a/packages/volto-slate/src/utils/blocks.js b/packages/volto-slate/src/utils/blocks.js index 127d9a721e..4175ebeddb 100644 --- a/packages/volto-slate/src/utils/blocks.js +++ b/packages/volto-slate/src/utils/blocks.js @@ -264,7 +264,7 @@ export const toggleBlock = (editor, format, allowedChildren) => { } else if (!isListItem && !wantsList) { toggleFormat(editor, format, allowedChildren); } else if (isListItem && wantsList && isActive) { - clearFormatting(editor); + clearList(editor); } else { console.warn('toggleBlock case not covered, please examine:', { wantsList, @@ -298,6 +298,21 @@ export const switchListType = (editor, format) => { Transforms.wrapNodes(editor, block); }; +/* + * Clear list by exploding the block + */ +export const clearList = (editor) => { + const { slate } = config.settings; + Transforms.unwrapNodes(editor, { + match: (n) => slate.listTypes.includes(n.type), + split: true, + }); + Transforms.setNodes(editor, { + type: 'p', + }); + Editor.normalize(editor); +}; + export const changeBlockToList = (editor, format) => { const { slate } = config.settings; const [match] = Editor.nodes(editor, { diff --git a/packages/volto/CHANGELOG.md b/packages/volto/CHANGELOG.md index bc714d07d0..bac17f20ad 100644 --- a/packages/volto/CHANGELOG.md +++ b/packages/volto/CHANGELOG.md @@ -17,6 +17,64 @@ myst: +## 18.0.0-alpha.35 (2024-06-13) + +### Breaking + +- Improve container detection, `config.settings.containerBlockTypes` is no longer needed @sneridagh [#6099](https://github.com/plone/volto/issues/6099) + +### Bugfix + +- Support nested directories in public folder add-on sync folders both in dev and build mode @sneridagh [#6098](https://github.com/plone/volto/issues/6098) +- export getFieldURL from Url.js in helpers @dobri1408 [#6100](https://github.com/plone/volto/issues/6100) + +## 18.0.0-alpha.34 (2024-06-13) + +### Feature + +- Added blocks layout navigator @robgietema @sneridagh [#5642](https://github.com/plone/volto/issues/5642) +- Add support for reading the add-ons `tsconfig.json` paths and add them to the build resolve aliases @sneridagh [#6096](https://github.com/plone/volto/issues/6096) + +### Bugfix + +- Fix internalUrl Widget to Reflect Prop Changes via onChangeBlock @dorbi1408 @ichim-david [#6036](https://github.com/plone/volto/issues/6036) +- Add default 'l' and 'center' values to size and align fields of `Image` block. + This fixes data not having any value adding proper options to the `Image` block. @ichim-david [#6046](https://github.com/plone/volto/issues/6046) +- Fix public folder in dev mode, now it starts by default with the default Volto core defined public files @sneridagh [#6081](https://github.com/plone/volto/issues/6081) +- Fix link in pop-up in `RelationsMatrix.jsx`. @stevepiercy [#6085](https://github.com/plone/volto/issues/6085) +- Fix Uncaught RangeError: date value is not finite in DateTimeFormat.format. @mauritsvanrees [#6087](https://github.com/plone/volto/issues/6087) +- relations control panel. Restrict eglible relation targets according relation constraints of fields vocabulary. @ksuess [#6091](https://github.com/plone/volto/issues/6091) +- Better `Icon` component JSDoc typings @sneridagh [#6095](https://github.com/plone/volto/issues/6095) + +## 18.0.0-alpha.33 (2024-06-06) + +### Breaking + +- Fix JavaScript events association on error pages. Also remove settings `config.settings.serverConfig.extractScripts.errorPages`. Now scripts are added to error pages, regardless of whether we are in production mode or not. @wesleybl [#6048](https://github.com/plone/volto/issues/6048) +- Breaking from the original slots implementation: + Now `config.getSlots` in the configuration registry takes the argument `location` instead of `pathname`. + This allows to have more expressive conditions, and fulfill the use case of the `Add` form. + @sneridagh [#6063](https://github.com/plone/volto/issues/6063) + +### Feature + +- Added object browser icon view @robgietema [#5279](https://github.com/plone/volto/issues/5279) +- Refactor TextWidget. @Tishasoumya-02 [#6020](https://github.com/plone/volto/issues/6020) +- Refactor IdWidget -@Tishasoumya-02 [#6027](https://github.com/plone/volto/issues/6027) +- The `ContentTypeCondition` now supports the `Add` form, and detects when you create a content type that is set in the condition. @sneridagh + Added a new `BodyClass` helper while adding a new content type of the form `is-adding-contenttype-mycontenttype`. @sneridagh [#6063](https://github.com/plone/volto/issues/6063) +- Add support for configurable `public` directory defined per add-on. @sneridagh [#6072](https://github.com/plone/volto/issues/6072) + +### Bugfix + +- Fix block chooser search is not focusable when clicked on add button @iRohitSingh [#5866](https://github.com/plone/volto/issues/5866) +- Fixed skiplink links not tracking focus correctly @JeffersonBledsoe [#5959](https://github.com/plone/volto/issues/5959) +- Remove left and right padding from _Event > Edit recurrence > Repeat on_ buttons when repeating for weekly or yearly events for big fonts, preventing overflow. @sabrina-bongiovanni [#6070](https://github.com/plone/volto/issues/6070) + +### Internal + +- Fix test script in monorepo root @sneridagh [#6051](https://github.com/plone/volto/issues/6051) + ## 18.0.0-alpha.32 (2024-05-23) ### Feature diff --git a/packages/volto/__tests__/addon-registry-project.test.js b/packages/volto/__tests__/addon-registry-project.test.js index 5fa7cd7808..9040c08af0 100644 --- a/packages/volto/__tests__/addon-registry-project.test.js +++ b/packages/volto/__tests__/addon-registry-project.test.js @@ -46,15 +46,18 @@ describe('AddonConfigurationRegistry - Project', () => { version: '0.0.0', }, 'test-released-addon': { + basePath: `${base}/node_modules/test-released-addon`, isPublishedPackage: true, modulePath: `${base}/node_modules/test-released-addon`, name: 'test-released-addon', packageJson: `${base}/node_modules/test-released-addon/package.json`, addons: ['test-released-unmentioned:extra1,extra2'], isRegisteredAddon: true, + tsConfigPaths: null, version: '0.0.0', }, 'test-released-source-addon': { + basePath: `${base}/node_modules/test-released-source-addon`, isPublishedPackage: true, modulePath: `${base}/node_modules/test-released-source-addon/src`, name: 'test-released-source-addon', @@ -62,15 +65,18 @@ describe('AddonConfigurationRegistry - Project', () => { razzleExtender: `${base}/node_modules/test-released-source-addon/razzle.extend.js`, addons: [], isRegisteredAddon: true, + tsConfigPaths: null, version: '0.0.0', }, 'test-released-unmentioned': { addons: [], + basePath: `${base}/node_modules/test-released-unmentioned`, isPublishedPackage: true, modulePath: `${base}/node_modules/test-released-unmentioned`, name: 'test-released-unmentioned', packageJson: `${base}/node_modules/test-released-unmentioned/package.json`, isRegisteredAddon: true, + tsConfigPaths: null, version: '0.0.0', }, 'my-volto-config-addon': { diff --git a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js index 20b1a097e6..9337d8cff4 100644 --- a/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js +++ b/packages/volto/cypress/tests/core/controlpanels/dexterity-controlpanel-layout.js @@ -27,6 +27,8 @@ describe('ControlPanel: Dexterity Content-Types Layout', () => { ); cy.get('#page-controlpanel-layout button').click(); + cy.get('#sidebar .formtabs').contains('Settings').click(); + // Wait a bit for draftjs to load, without this the title block // custom placeholder is missing and cypress gives a timeout error cy.wait(1000); diff --git a/packages/volto/cypress/tests/coresandbox/internalUrl.js b/packages/volto/cypress/tests/coresandbox/internalUrl.js new file mode 100644 index 0000000000..9a6026e687 --- /dev/null +++ b/packages/volto/cypress/tests/coresandbox/internalUrl.js @@ -0,0 +1,33 @@ +context('Internal Url ', () => { + describe('Internal Url Widget is updating after block has been updated with new data via onChangeBlock.', () => { + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'document', + contentTitle: 'Test document', + }); + }); + + it('Internal Url Widget is updating after block has been updated with new data via onChangeBlock.', function () { + cy.visit('/document'); + cy.wait('@content'); + + cy.navigate('/document/edit'); + cy.wait('@schema'); + + // Add input block + cy.get('button.block-add-button').click(); + cy.get('.blocks-chooser .title').contains('Common').click(); + cy.get('.blocks-chooser .common') + .contains('Input') + .click({ force: true }); + cy.get('#input_block').type('link-test.com'); + cy.get('#add_link').click(); + cy.get('#field-url').should('have.value', 'link-test.com'); + }); + }); +}); diff --git a/packages/volto/locales/ca/LC_MESSAGES/volto.po b/packages/volto/locales/ca/LC_MESSAGES/volto.po index c5d1440d68..4dfefdc7ae 100644 --- a/packages/volto/locales/ca/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ca/LC_MESSAGES/volto.po @@ -2481,6 +2481,11 @@ msgstr "Obre el menú" msgid "Open object browser" msgstr "Obre el navegador d'objectes" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/de/LC_MESSAGES/volto.po b/packages/volto/locales/de/LC_MESSAGES/volto.po index 05460062ad..58c6b0ea39 100644 --- a/packages/volto/locales/de/LC_MESSAGES/volto.po +++ b/packages/volto/locales/de/LC_MESSAGES/volto.po @@ -2480,6 +2480,11 @@ msgstr "Menü öffnen" msgid "Open object browser" msgstr "Objekt-Browser öffnen" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/en/LC_MESSAGES/volto.po b/packages/volto/locales/en/LC_MESSAGES/volto.po index 99ff970059..8df419fde2 100644 --- a/packages/volto/locales/en/LC_MESSAGES/volto.po +++ b/packages/volto/locales/en/LC_MESSAGES/volto.po @@ -2475,6 +2475,11 @@ msgstr "" msgid "Open object browser" msgstr "" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/es/LC_MESSAGES/volto.po b/packages/volto/locales/es/LC_MESSAGES/volto.po index 323c212b87..1fcb01ff3e 100644 --- a/packages/volto/locales/es/LC_MESSAGES/volto.po +++ b/packages/volto/locales/es/LC_MESSAGES/volto.po @@ -2482,6 +2482,11 @@ msgstr "Abrir menú" msgid "Open object browser" msgstr "Abrir buscador de objetos" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/eu/LC_MESSAGES/volto.po b/packages/volto/locales/eu/LC_MESSAGES/volto.po index 49936fe350..4b49744385 100644 --- a/packages/volto/locales/eu/LC_MESSAGES/volto.po +++ b/packages/volto/locales/eu/LC_MESSAGES/volto.po @@ -2482,6 +2482,11 @@ msgstr "Menua ireki" msgid "Open object browser" msgstr "Ireki elementu bilatzailea" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/fi/LC_MESSAGES/volto.po b/packages/volto/locales/fi/LC_MESSAGES/volto.po index ea67919876..a3303a3cb3 100644 --- a/packages/volto/locales/fi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fi/LC_MESSAGES/volto.po @@ -2480,6 +2480,11 @@ msgstr "Avaa valikko" msgid "Open object browser" msgstr "" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/fr/LC_MESSAGES/volto.po b/packages/volto/locales/fr/LC_MESSAGES/volto.po index 422561a85a..e5ac29e9c8 100644 --- a/packages/volto/locales/fr/LC_MESSAGES/volto.po +++ b/packages/volto/locales/fr/LC_MESSAGES/volto.po @@ -2482,6 +2482,11 @@ msgstr "Ouvrir le menu" msgid "Open object browser" msgstr "Ouvrir le navigateur d'objets" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/hi/LC_MESSAGES/volto.po b/packages/volto/locales/hi/LC_MESSAGES/volto.po index aa80505383..6e674c809f 100644 --- a/packages/volto/locales/hi/LC_MESSAGES/volto.po +++ b/packages/volto/locales/hi/LC_MESSAGES/volto.po @@ -2475,6 +2475,11 @@ msgstr "मेन्यू खोलें" msgid "Open object browser" msgstr "ऑब्जेक्ट ब्राउज़र खोलें" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/it/LC_MESSAGES/volto.po b/packages/volto/locales/it/LC_MESSAGES/volto.po index 856f036ffe..f677738b2b 100644 --- a/packages/volto/locales/it/LC_MESSAGES/volto.po +++ b/packages/volto/locales/it/LC_MESSAGES/volto.po @@ -2475,6 +2475,11 @@ msgstr "Apri menu" msgid "Open object browser" msgstr "Apri object browser" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "Ordine" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/ja/LC_MESSAGES/volto.po b/packages/volto/locales/ja/LC_MESSAGES/volto.po index 2a2da939ab..2189aef254 100644 --- a/packages/volto/locales/ja/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ja/LC_MESSAGES/volto.po @@ -2480,6 +2480,11 @@ msgstr "メニューを開く" msgid "Open object browser" msgstr "オブジェクトブラウザを開く" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/nl/LC_MESSAGES/volto.po b/packages/volto/locales/nl/LC_MESSAGES/volto.po index 01dc20ba85..6b37102de2 100644 --- a/packages/volto/locales/nl/LC_MESSAGES/volto.po +++ b/packages/volto/locales/nl/LC_MESSAGES/volto.po @@ -2479,6 +2479,11 @@ msgstr "" msgid "Open object browser" msgstr "" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/pt/LC_MESSAGES/volto.po b/packages/volto/locales/pt/LC_MESSAGES/volto.po index 7d02e71e72..cc5ca6584a 100644 --- a/packages/volto/locales/pt/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt/LC_MESSAGES/volto.po @@ -2480,6 +2480,11 @@ msgstr "Abrir menu" msgid "Open object browser" msgstr "" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po index f2ea3de844..2a17a9bc2d 100644 --- a/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po +++ b/packages/volto/locales/pt_BR/LC_MESSAGES/volto.po @@ -2481,6 +2481,11 @@ msgstr "Abrir menu" msgid "Open object browser" msgstr "Abrir navegador de objetos" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/ro/LC_MESSAGES/volto.po b/packages/volto/locales/ro/LC_MESSAGES/volto.po index 9481755aa6..811ea97670 100644 --- a/packages/volto/locales/ro/LC_MESSAGES/volto.po +++ b/packages/volto/locales/ro/LC_MESSAGES/volto.po @@ -2475,6 +2475,11 @@ msgstr "Deschideți meniul" msgid "Open object browser" msgstr "Deschideți browserul de obiecte" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/volto.pot b/packages/volto/locales/volto.pot index c28b4ea5dc..f96213d3b0 100644 --- a/packages/volto/locales/volto.pot +++ b/packages/volto/locales/volto.pot @@ -2477,6 +2477,11 @@ msgstr "" msgid "Open object browser" msgstr "" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po index d4511f9c05..fda490e0f4 100644 --- a/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po +++ b/packages/volto/locales/zh_CN/LC_MESSAGES/volto.po @@ -2481,6 +2481,11 @@ msgstr "打开菜单" msgid "Open object browser" msgstr "打开目标浏览器" +#. Default: "Order" +#: components/manage/Sidebar/Sidebar +msgid "Order" +msgstr "" + #. Default: "Ordered" #: components/manage/Blocks/ToC/Schema msgid "Ordered" diff --git a/packages/volto/news/5279.feature b/packages/volto/news/5279.feature deleted file mode 100644 index a93c6febfb..0000000000 --- a/packages/volto/news/5279.feature +++ /dev/null @@ -1 +0,0 @@ -Added object browser icon view @robgietema \ No newline at end of file diff --git a/packages/volto/news/5866.bugfix b/packages/volto/news/5866.bugfix deleted file mode 100644 index dae6cc5bf3..0000000000 --- a/packages/volto/news/5866.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix block chooser search is not focusable when clicked on add button @iRohitSingh \ No newline at end of file diff --git a/packages/volto/news/6020.feature b/packages/volto/news/6020.feature deleted file mode 100644 index c5a57a68f3..0000000000 --- a/packages/volto/news/6020.feature +++ /dev/null @@ -1 +0,0 @@ -Refactor TextWidget. @Tishasoumya-02 \ No newline at end of file diff --git a/packages/volto/news/6027.feature b/packages/volto/news/6027.feature deleted file mode 100644 index 08c7b71647..0000000000 --- a/packages/volto/news/6027.feature +++ /dev/null @@ -1 +0,0 @@ -Refactor IdWidget -@Tishasoumya-02 \ No newline at end of file diff --git a/packages/volto/news/6051.internal b/packages/volto/news/6051.internal deleted file mode 100644 index dbda58fedf..0000000000 --- a/packages/volto/news/6051.internal +++ /dev/null @@ -1 +0,0 @@ -Fix test script in monorepo root @sneridagh diff --git a/packages/volto/news/6063.breaking b/packages/volto/news/6063.breaking deleted file mode 100644 index 985c127151..0000000000 --- a/packages/volto/news/6063.breaking +++ /dev/null @@ -1,4 +0,0 @@ -Breaking from the original slots implementation: -Now `config.getSlots` in the configuration registry takes the argument `location` instead of `pathname`. -This allows to have more expressive conditions, and fulfill the use case of the `Add` form. -@sneridagh diff --git a/packages/volto/news/6063.feature b/packages/volto/news/6063.feature deleted file mode 100644 index d8a2b66f6d..0000000000 --- a/packages/volto/news/6063.feature +++ /dev/null @@ -1,2 +0,0 @@ -The `ContentTypeCondition` now supports the `Add` form, and detects when you create a content type that is set in the condition. @sneridagh -Added a new `BodyClass` helper while adding a new content type of the form `is-adding-contenttype-mycontenttype`. @sneridagh diff --git a/packages/volto/package.json b/packages/volto/package.json index ef3ea8ae14..4228f86581 100644 --- a/packages/volto/package.json +++ b/packages/volto/package.json @@ -9,7 +9,7 @@ } ], "license": "MIT", - "version": "18.0.0-alpha.32", + "version": "18.0.0-alpha.35", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" @@ -281,7 +281,11 @@ "@babel/plugin-syntax-export-namespace-from": "7.8.3", "@babel/runtime": "7.20.6", "@babel/types": "7.20.5", + "@fiverr/afterbuild-webpack-plugin": "^1.0.0", "@jest/globals": "^29.7.0", + "@dnd-kit/core": "6.0.8", + "@dnd-kit/sortable": "7.0.2", + "@dnd-kit/utilities": "3.2.2", "@loadable/babel-plugin": "5.13.2", "@loadable/webpack-plugin": "5.15.2", "@plone/types": "workspace:*", diff --git a/packages/volto/razzle.config.js b/packages/volto/razzle.config.js index 925a9fcae9..204b6607c7 100644 --- a/packages/volto/razzle.config.js +++ b/packages/volto/razzle.config.js @@ -15,6 +15,7 @@ const CircularDependencyPlugin = require('circular-dependency-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); +const AfterBuildPlugin = require('@fiverr/afterbuild-webpack-plugin'); const fileLoaderFinder = makeLoaderFinder('file-loader'); @@ -30,6 +31,7 @@ const defaultModify = ({ webpackConfig: config, webpackObject: webpack, options, + paths, }) => { // Compile language JSON files from po files poToJson({ registry, addonMode: false }); @@ -156,6 +158,66 @@ const defaultModify = ({ placeholders: true, }), ); + + // This copies the publicPath files set in voltoConfigJS with the local `public` + // directory at build time + config.plugins.push( + new AfterBuildPlugin(() => { + const mergeDirectories = (sourceDir, targetDir) => { + const files = fs.readdirSync(sourceDir); + files.forEach((file) => { + const sourcePath = path.join(sourceDir, file); + const targetPath = path.join(targetDir, file); + const isDirectory = fs.statSync(sourcePath).isDirectory(); + if (isDirectory) { + fs.mkdirSync(targetPath, { recursive: true }); + mergeDirectories(sourcePath, targetPath); + } else { + fs.copyFileSync(sourcePath, targetPath); + } + }); + }; + + // If we are in development mode, we copy the public directory to the + // public directory of the setup root, so the files are available + if (dev && !registry.isVoltoProject && registry.addonNames.length > 0) { + const devPublicPath = `${projectRootPath}/../../../public`; + if (!fs.existsSync(devPublicPath)) { + fs.mkdirSync(devPublicPath); + } + mergeDirectories( + path.join(projectRootPath, 'public'), + `${projectRootPath}/../../../public`, + ); + } + + registry.getAddonDependencies().forEach((addonDep) => { + // What comes from getAddonDependencies is in the form of `@package/addon:profile` + const addon = addonDep.split(':')[0]; + // Check if the addon is available in the registry, just in case + if (registry.packages[addon]) { + const p = fs.realpathSync( + `${registry.packages[addon].modulePath}/../.`, + ); + if (fs.existsSync(path.join(p, 'public'))) { + if (!dev) { + mergeDirectories(path.join(p, 'public'), paths.appBuildPublic); + } + if ( + dev && + !registry.isVoltoProject && + registry.addonNames.length > 0 + ) { + mergeDirectories( + path.join(p, 'public'), + `${projectRootPath}/../../../public`, + ); + } + } + } + }); + }), + ); } if (target === 'node') { @@ -387,12 +449,14 @@ module.exports = { webpackConfig, webpackObject, options, + paths, }) => { const defaultConfig = defaultModify({ env: { target, dev }, webpackConfig, webpackObject, options, + paths, }); const res = addonExtenders.reduce( diff --git a/packages/volto/src/actions/form/form.js b/packages/volto/src/actions/form/form.js index 5cc22aabc3..bfbcc75bf4 100644 --- a/packages/volto/src/actions/form/form.js +++ b/packages/volto/src/actions/form/form.js @@ -3,13 +3,16 @@ * @module actions/form/form */ -import { SET_FORM_DATA } from '@plone/volto/constants/ActionTypes'; +import { + SET_FORM_DATA, + SET_UI_STATE, +} from '@plone/volto/constants/ActionTypes'; /** * Set form data function. * @function setFormData * @param {Object} data New form data. - * @returns {Object} Set sidebar action. + * @returns {Object} Set form data action. */ export function setFormData(data) { return { @@ -17,3 +20,16 @@ export function setFormData(data) { data, }; } + +/** + * Set ui state function. + * @function setUIState + * @param {Object} ui New ui state. + * @returns {Object} Set ui state action. + */ +export function setUIState(ui) { + return { + type: SET_UI_STATE, + ui, + }; +} diff --git a/packages/volto/src/actions/index.js b/packages/volto/src/actions/index.js index 21c58372de..ee88c10b60 100644 --- a/packages/volto/src/actions/index.js +++ b/packages/volto/src/actions/index.js @@ -157,7 +157,7 @@ export { resetMetadataFocus, setSidebarTab, } from '@plone/volto/actions/sidebar/sidebar'; -export { setFormData } from '@plone/volto/actions/form/form'; +export { setFormData, setUIState } from '@plone/volto/actions/form/form'; export { deleteLinkTranslation, getTranslationLocator, diff --git a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx index 0424a307bc..a5fb57e217 100644 --- a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.jsx @@ -1,11 +1,14 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; +import { cloneDeep, map } from 'lodash'; import EditBlock from './Edit'; import { DragDropList } from '@plone/volto/components'; import { getBlocks, getBlocksFieldname, + getBlocksLayoutFieldname, applyBlockDefaults, + getBlocksHierarchy, } from '@plone/volto/helpers'; import { addBlock, @@ -13,15 +16,19 @@ import { changeBlock, deleteBlock, moveBlock, + moveBlockEnhanced, mutateBlock, nextBlockId, previousBlockId, } from '@plone/volto/helpers'; import EditBlockWrapper from './EditBlockWrapper'; -import { setSidebarTab } from '@plone/volto/actions'; +import { setSidebarTab, setUIState } from '@plone/volto/actions'; import { useDispatch } from 'react-redux'; import { useDetectClickOutside, useEvent } from '@plone/volto/helpers'; import config from '@plone/volto/registry'; +import { createPortal } from 'react-dom'; + +import Order from './Order/Order'; const BlocksForm = (props) => { const { @@ -53,6 +60,12 @@ const BlocksForm = (props) => { token, } = props; + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); + const blockList = getBlocks(properties); const dispatch = useDispatch(); @@ -183,6 +196,52 @@ const BlocksForm = (props) => { onChangeFormData(newFormData); }; + const onMoveBlockEnhanced = ({ source, destination }) => { + const newFormData = moveBlockEnhanced(cloneDeep(properties), { + source, + destination, + }); + const blocksFieldname = getBlocksFieldname(newFormData); + const blocksLayoutFieldname = getBlocksLayoutFieldname(newFormData); + let error = false; + + const allowedBlocks = Object.keys(blocksConfig); + + map(newFormData[blocksLayoutFieldname].items, (id) => { + const block = newFormData[blocksFieldname][id]; + if (!allowedBlocks.includes(block['@type'])) { + error = true; + } + if (Array.isArray(block[blocksLayoutFieldname]?.items)) { + const size = block[blocksLayoutFieldname].items.length; + const allowedSubBlocks = [ + ...(blocksConfig[block['@type']].allowedBlocks || allowedBlocks), + 'empty', + ] || ['empty']; + if (size < 1 || size > (blocksConfig[block['@type']].maxLength || 4)) { + error = true; + } + map(block[blocksLayoutFieldname].items, (subId) => { + const subBlock = block[blocksFieldname][subId]; + if (!allowedSubBlocks.includes(subBlock['@type'])) { + error = true; + } + }); + } + }); + + if (!error) { + onChangeFormData(newFormData); + dispatch( + setUIState({ + selected: null, + multiSelected: [], + gridSelected: null, + }), + ); + } + }; + const defaultBlockWrapper = ({ draginfo }, editBlock, blockProps) => ( {editBlock} @@ -195,6 +254,7 @@ const BlocksForm = (props) => { // Note they are alreaady filtered by DragDropList, but we also want them // to be removed when the user saves the page next. Otherwise the invalid // blocks would linger for ever. + for (const [n, v] of blockList) { if (!v) { const newFormData = deleteBlock(properties, n); @@ -210,85 +270,101 @@ const BlocksForm = (props) => { }); return ( -
{ - if (stopPropagation) { - e.stopPropagation(); - } - }} - > -
- { - const { source, destination } = result; - if (!destination) { - return; - } - const newFormData = moveBlock( - properties, - source.index, - destination.index, - ); - onChangeFormData(newFormData); - return true; - }} - direction={direction} - > - {(dragProps) => { - const { child, childId, index } = dragProps; - const blockProps = { - allowedBlocks, - showRestricted, - block: childId, - data: child, - handleKeyDown, - id: childId, - formTitle: title, - formDescription: description, - index, - manage, - onAddBlock, - onInsertBlock, - onChangeBlock, - onChangeField, - onChangeFormData, - onDeleteBlock, - onFocusNextBlock, - onFocusPreviousBlock, - onMoveBlock, - onMutateBlock, - onSelectBlock, - pathname, - metadata, - properties, - contentType: type, - navRoot, - blocksConfig, - selected: selectedBlock === childId, - multiSelected: multiSelected?.includes(childId), - type: child['@type'], - editable, - showBlockChooser: selectedBlock === childId, - detached: isContainer, - // Properties to pass to the BlocksForm to match the View ones - content: properties, - history, - location, - token, - }; - return editBlockWrapper( - dragProps, - , - blockProps, - ); - }} - -
-
+ <> + {isMainForm && + isClient && + createPortal( +
+ +
, + document.getElementById('sidebar-order'), + )} +
{ + if (stopPropagation) { + e.stopPropagation(); + } + }} + > +
+ { + const { source, destination } = result; + if (!destination) { + return; + } + const newFormData = moveBlock( + properties, + source.index, + destination.index, + ); + onChangeFormData(newFormData); + return true; + }} + direction={direction} + > + {(dragProps) => { + const { child, childId, index } = dragProps; + const blockProps = { + allowedBlocks, + showRestricted, + block: childId, + data: child, + handleKeyDown, + id: childId, + formTitle: title, + formDescription: description, + index, + manage, + onAddBlock, + onInsertBlock, + onChangeBlock, + onChangeField, + onChangeFormData, + onDeleteBlock, + onFocusNextBlock, + onFocusPreviousBlock, + onMoveBlock, + onMutateBlock, + onSelectBlock, + pathname, + metadata, + properties, + contentType: type, + navRoot, + blocksConfig, + selected: selectedBlock === childId, + multiSelected: multiSelected?.includes(childId), + type: child['@type'], + editable, + showBlockChooser: selectedBlock === childId, + detached: isContainer, + // Properties to pass to the BlocksForm to match the View ones + content: properties, + history, + location, + token, + }; + return editBlockWrapper( + dragProps, + , + blockProps, + ); + }} + +
+
+ ); }; diff --git a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.test.jsx b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.test.jsx index 55958451af..4f53b162e1 100644 --- a/packages/volto/src/components/manage/Blocks/Block/BlocksForm.test.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/BlocksForm.test.jsx @@ -30,6 +30,9 @@ test('Allow override of blocksConfig', () => { locale: 'en', messages: {}, }, + form: { + ui: {}, + }, }); const data = { @@ -68,6 +71,7 @@ test('Allow override of blocksConfig', () => { const { container } = render( + , ); expect(container).toMatchSnapshot(); @@ -79,6 +83,9 @@ test('Removes invalid blocks on saving', () => { locale: 'en', messages: {}, }, + form: { + ui: {}, + }, }); const onChangeFormData = jest.fn(() => {}); @@ -120,6 +127,7 @@ test('Removes invalid blocks on saving', () => { render( + , ); expect(onChangeFormData).toBeCalledWith({ diff --git a/packages/volto/src/components/manage/Blocks/Block/Edit.jsx b/packages/volto/src/components/manage/Blocks/Block/Edit.jsx index cbcfdf527d..4b9c4419a5 100644 --- a/packages/volto/src/components/manage/Blocks/Block/Edit.jsx +++ b/packages/volto/src/components/manage/Blocks/Block/Edit.jsx @@ -9,7 +9,7 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; import cx from 'classnames'; -import { setSidebarTab } from '@plone/volto/actions'; +import { setSidebarTab, setUIState } from '@plone/volto/actions'; import config from '@plone/volto/registry'; import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser'; import { applyBlockDefaults } from '@plone/volto/helpers'; @@ -79,7 +79,11 @@ export class Edit extends Component { this.blockNode.current.focus(); } const tab = this.props.manage ? 1 : blocksConfig?.[type]?.sidebarTab || 0; - if (this.props.selected && this.props.editable) { + if ( + this.props.selected && + this.props.editable && + this.props.sidebarTab !== 2 + ) { this.props.setSidebarTab(tab); } } @@ -105,7 +109,9 @@ export class Edit extends Component { const tab = this.props.manage ? 1 : blocksConfig?.[nextProps.type]?.sidebarTab || 0; - this.props.setSidebarTab(tab); + if (this.props.sidebarTab !== 2) { + this.props.setSidebarTab(tab); + } } } @@ -138,6 +144,21 @@ export class Edit extends Component { {Block !== null ? (
{ + if (this.props.hovered !== this.props.id) { + this.props.setUIState({ hovered: this.props.id }); + } + }} + onFocus={() => { + // TODO: This `onFocus` steals somehow the focus from the slate block + // we have to investigate why this is happening + // Apparently, I can't see any difference in the behavior + // If any, we can fix it in successive iterations + // if (this.props.hovered !== this.props.id) { + // this.props.setUIState({ hovered: this.props.id }); + // } + }} + onMouseLeave={() => this.props.setUIState({ hovered: null })} onClick={(e) => { const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey; !this.props.selected && @@ -161,6 +182,7 @@ export class Edit extends Component { className={cx('block', type, this.props.data.variation, { selected: this.props.selected || this.props.multiSelected, multiSelected: this.props.multiSelected, + hovered: this.props.hovered === this.props.id, })} style={{ outline: 'none' }} ref={this.blockNode} @@ -185,6 +207,11 @@ export class Edit extends Component { ) : (
+ this.props.setUIState({ hovered: this.props.id }) + } + onFocus={() => this.props.setUIState({ hovered: this.props.id })} + onMouseLeave={() => this.props.setUIState({ hovered: null })} onClick={() => !this.props.selected && this.props.onSelectBlock(this.props.id) } @@ -218,5 +245,11 @@ export class Edit extends Component { export default compose( injectIntl, withObjectBrowser, - connect(null, { setSidebarTab }), + connect( + (state, props) => ({ + hovered: state.form?.ui.hovered || null, + sidebarTab: state.sidebar?.tab, + }), + { setSidebarTab, setUIState }, + ), )(Edit); diff --git a/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx b/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx new file mode 100644 index 0000000000..db73f4fbf7 --- /dev/null +++ b/packages/volto/src/components/manage/Blocks/Block/Order/Item.jsx @@ -0,0 +1,122 @@ +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; +import { useDispatch, useSelector } from 'react-redux'; +import { includes } from 'lodash'; + +import { Icon } from '@plone/volto/components'; +import { setUIState } from '@plone/volto/actions'; +import config from '@plone/volto/registry'; + +import deleteSVG from '@plone/volto/icons/delete.svg'; +import dragSVG from '@plone/volto/icons/drag.svg'; + +export const Item = forwardRef( + ( + { + clone, + data, + depth, + disableSelection, + disableInteraction, + ghost, + id, + handleProps, + indentationWidth, + onRemove, + onSelectBlock, + parentId, + style, + value, + wrapperRef, + ...props + }, + ref, + ) => { + const selected = useSelector((state) => state.form.ui.selected); + const hovered = useSelector((state) => state.form.ui.hovered); + const multiSelected = useSelector((state) => state.form.ui.multiSelected); + const gridSelected = useSelector((state) => state.form.ui.gridSelected); + const dispatch = useDispatch(); + return ( +
  • dispatch(setUIState({ hovered: id }))} + onFocus={() => dispatch(setUIState({ hovered: id }))} + onMouseLeave={() => dispatch(setUIState({ hovered: null }))} + onClick={(e) => { + if (depth === 0) { + const isMultipleSelection = e.shiftKey || e.ctrlKey || e.metaKey; + selected !== id && + onSelectBlock( + id, + selected === id ? false : isMultipleSelection, + e, + ); + } else { + dispatch( + setUIState({ + selected: parentId, + multiSelected: [], + gridSelected: id, + }), + ); + } + }} + ref={wrapperRef} + style={{ + '--spacing': `${indentationWidth * depth}px`, + }} + {...props} + > +
    + + + {config.blocks.blocksConfig[data?.['@type']]?.icon && ( + + )}{' '} + {data?.plaintext || + config.blocks.blocksConfig[data?.['@type']]?.title} + + {!clone && onRemove && ( + + )} +
    +
  • + ); + }, +); diff --git a/packages/volto/src/components/manage/Blocks/Block/Order/Order.jsx b/packages/volto/src/components/manage/Blocks/Block/Order/Order.jsx new file mode 100644 index 0000000000..627efacbed --- /dev/null +++ b/packages/volto/src/components/manage/Blocks/Block/Order/Order.jsx @@ -0,0 +1,367 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { find, min } from 'lodash'; + +import { flattenTree, getProjection, removeChildrenOf } from './utilities'; +import SortableItem from './SortableItem'; + +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; + +export function Order({ + items = [], + onMoveBlock, + onDeleteBlock, + onSelectBlock, + indentationWidth = 25, + removable, + dndKitCore, + dndKitSortable, + dndKitUtilities, +}) { + const [activeId, setActiveId] = useState(null); + const [overId, setOverId] = useState(null); + const [offsetLeft, setOffsetLeft] = useState(0); + const [currentPosition, setCurrentPosition] = useState(null); + + const { + DndContext, + closestCenter, + PointerSensor, + useSensor, + useSensors, + DragOverlay, + MeasuringStrategy, + defaultDropAnimation, + } = dndKitCore; + const { SortableContext, arrayMove, verticalListSortingStrategy } = + dndKitSortable; + const { CSS } = dndKitUtilities; + + const measuring = { + droppable: { + strategy: MeasuringStrategy.Always, + }, + }; + + const dropAnimationConfig = { + keyframes({ transform }) { + return [ + { opacity: 1, transform: CSS.Transform.toString(transform.initial) }, + { + opacity: 0, + transform: CSS.Transform.toString({ + ...transform.final, + x: transform.final.x + 5, + y: transform.final.y + 5, + }), + }, + ]; + }, + easing: 'ease-out', + sideEffects({ active }) { + active.node.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: defaultDropAnimation.duration, + easing: defaultDropAnimation.easing, + }); + }, + }; + + const flattenedItems = useMemo( + () => removeChildrenOf(flattenTree(items), activeId ? [activeId] : []), + [activeId, items], + ); + const projected = + activeId && overId + ? getProjection( + flattenedItems, + activeId, + overId, + offsetLeft, + indentationWidth, + arrayMove, + ) + : null; + const sensorContext = useRef({ + items: flattenedItems, + offset: offsetLeft, + }); + const sensors = useSensors(useSensor(PointerSensor)); + + const sortedIds = useMemo( + () => flattenedItems.map(({ id }) => id), + [flattenedItems], + ); + const activeItem = activeId + ? flattenedItems.find(({ id }) => id === activeId) + : null; + + useEffect(() => { + sensorContext.current = { + items: flattenedItems, + offset: offsetLeft, + }; + }, [flattenedItems, offsetLeft]); + + const announcements = { + onDragStart({ active }) { + return `Picked up ${active.id}.`; + }, + onDragMove({ active, over }) { + return getMovementAnnouncement('onDragMove', active.id, over?.id); + }, + onDragOver({ active, over }) { + return getMovementAnnouncement('onDragOver', active.id, over?.id); + }, + onDragEnd({ active, over }) { + return getMovementAnnouncement('onDragEnd', active.id, over?.id); + }, + onDragCancel({ active }) { + return `Moving was cancelled. ${active.id} was dropped in its original position.`; + }, + }; + + return ( + + + {flattenedItems.map(({ id, parentId, depth, data }) => ( + handleRemove(id) : undefined} + onSelectBlock={onSelectBlock} + /> + ))} + {createPortal( + + {activeId && activeItem ? ( + + ) : null} + , + document.body, + )} + + + ); + + function handleDragStart({ active: { id: activeId } }) { + setActiveId(activeId); + setOverId(activeId); + + const activeItem = flattenedItems.find(({ id }) => id === activeId); + + if (activeItem) { + setCurrentPosition({ + parentId: activeItem.parentId, + overId: activeId, + }); + } + + document.body.style.setProperty('cursor', 'grabbing'); + } + + function handleDragMove({ delta }) { + setOffsetLeft(delta.x); + } + + function handleDragOver({ over }) { + setOverId(over?.id ?? null); + } + + function handleDragEnd({ active, over }) { + if (projected && over) { + const { depth, parentId } = projected; + const clonedItems = JSON.parse(JSON.stringify(flattenedItems)); + const overIndex = clonedItems.findIndex(({ id }) => id === over.id); + const activeIndex = clonedItems.findIndex(({ id }) => id === active.id); + const activeTreeItem = clonedItems[activeIndex]; + const oldParentId = activeTreeItem.parentId; + + clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId }; + + // Translate position depending on parent + if (parentId === oldParentId) { + // Move from and to toplevel or move within the same grid block + + let destIndex = clonedItems[overIndex].index; + if (clonedItems[overIndex].depth > clonedItems[activeIndex].depth) { + destIndex = find(clonedItems, { + id: clonedItems[overIndex].parentId, + }).index; + } + onMoveBlock({ + source: { + position: clonedItems[activeIndex].index, + parent: oldParentId, + id: active.id, + }, + destination: { + position: destIndex, + parent: parentId, + }, + }); + } else if (parentId && oldParentId) { + // Move from one gridblock to another + + onMoveBlock({ + source: { + position: clonedItems[activeIndex].index, + parent: oldParentId, + id: active.id, + }, + destination: { + position: + overIndex < activeIndex + ? clonedItems[overIndex - 1].parentId + ? clonedItems[overIndex - 1].index + 1 + : clonedItems[overIndex].index + : overIndex + 1 < clonedItems.length + ? clonedItems[overIndex + 1].index + : clonedItems[overIndex].index + 1, + parent: parentId, + }, + }); + } else if (oldParentId) { + // Moving to the main container from a gridblock + + onMoveBlock({ + source: { + position: clonedItems[activeIndex].index, + parent: oldParentId, + id: active.id, + }, + destination: { + position: + overIndex > activeIndex + ? overIndex + 1 < clonedItems.length + ? clonedItems[overIndex + 1].index + : clonedItems[overIndex].index + 1 + : clonedItems[overIndex].index, + parent: parentId, + }, + }); + } else { + // Moving from the main container to a gridblock + + onMoveBlock({ + source: { + position: clonedItems[activeIndex].index, + parent: oldParentId, + id: active.id, + }, + destination: { + position: + overIndex < activeIndex + ? clonedItems[overIndex - 1].parentId + ? clonedItems[overIndex - 1].index + 1 + : clonedItems[overIndex].index + : overIndex + 1 < clonedItems.length + ? clonedItems[overIndex + 1].index + : clonedItems[overIndex].index + 1, + parent: parentId, + }, + }); + } + } + + resetState(); + } + + function handleDragCancel() { + resetState(); + } + + function resetState() { + setOverId(null); + setActiveId(null); + setOffsetLeft(0); + setCurrentPosition(null); + + document.body.style.setProperty('cursor', ''); + } + + function handleRemove(id) { + onDeleteBlock(id); + } + + function getMovementAnnouncement(eventName, activeId, overId) { + if (overId && projected) { + if (eventName !== 'onDragEnd') { + if ( + currentPosition && + projected.parentId === currentPosition.parentId && + overId === currentPosition.overId + ) { + return; + } else { + setCurrentPosition({ + parentId: projected.parentId, + overId, + }); + } + } + + const clonedItems = JSON.parse(JSON.stringify(flattenTree(items))); + const overIndex = clonedItems.findIndex(({ id }) => id === overId); + const activeIndex = clonedItems.findIndex(({ id }) => id === activeId); + const sortedItems = arrayMove(clonedItems, activeIndex, overIndex); + + const previousItem = sortedItems[overIndex - 1]; + + let announcement; + const movedVerb = eventName === 'onDragEnd' ? 'dropped' : 'moved'; + const nestedVerb = eventName === 'onDragEnd' ? 'dropped' : 'nested'; + + if (!previousItem) { + const nextItem = sortedItems[overIndex + 1]; + announcement = `${activeId} was ${movedVerb} before ${nextItem.id}.`; + } else { + if (projected.depth > previousItem.depth) { + announcement = `${activeId} was ${nestedVerb} under ${previousItem.id}.`; + } else { + let previousSibling = previousItem; + while (previousSibling && projected.depth < previousSibling.depth) { + const parentId = previousSibling.parentId; + previousSibling = sortedItems.find(({ id }) => id === parentId); + } + + if (previousSibling) { + announcement = `${activeId} was ${movedVerb} after ${previousSibling.id}.`; + } + } + } + + return announcement; + } + + return; + } +} + +export default injectLazyLibs([ + 'dndKitCore', + 'dndKitSortable', + 'dndKitUtilities', +])(Order); diff --git a/packages/volto/src/components/manage/Blocks/Block/Order/SortableItem.jsx b/packages/volto/src/components/manage/Blocks/Block/Order/SortableItem.jsx new file mode 100644 index 0000000000..bfa328b1e9 --- /dev/null +++ b/packages/volto/src/components/manage/Blocks/Block/Order/SortableItem.jsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; + +import { Item } from './Item'; + +const animateLayoutChanges = ({ isSorting, wasDragging }) => + isSorting || wasDragging ? false : true; + +export function SortableItem({ + id, + depth, + dndKitSortable, + dndKitUtilities, + ...props +}) { + const { useSortable } = dndKitSortable; + const { CSS } = dndKitUtilities; + const { + attributes, + isDragging, + isSorting, + listeners, + setDraggableNodeRef, + setDroppableNodeRef, + transform, + transition, + } = useSortable({ + id, + animateLayoutChanges, + }); + const style = { + transform: CSS.Translate.toString(transform), + transition, + }; + + return ( + + ); +} + +export default injectLazyLibs(['dndKitSortable', 'dndKitUtilities'])( + SortableItem, +); diff --git a/packages/volto/src/components/manage/Blocks/Block/Order/utilities.js b/packages/volto/src/components/manage/Blocks/Block/Order/utilities.js new file mode 100644 index 0000000000..eaa802ca65 --- /dev/null +++ b/packages/volto/src/components/manage/Blocks/Block/Order/utilities.js @@ -0,0 +1,113 @@ +import { isArray } from 'lodash'; + +import { getBlocksLayoutFieldname } from '@plone/volto/helpers'; + +function getDragDepth(offset, indentationWidth) { + return Math.round(offset / indentationWidth); +} + +export function getProjection( + items, + activeId, + overId, + dragOffset, + indentationWidth, + arrayMove, +) { + const overItemIndex = items.findIndex(({ id }) => id === overId); + const activeItemIndex = items.findIndex(({ id }) => id === activeId); + const activeItem = items[activeItemIndex]; + const newItems = arrayMove(items, activeItemIndex, overItemIndex); + const previousItem = newItems[overItemIndex - 1]; + const nextItem = newItems[overItemIndex + 1]; + const dragDepth = getDragDepth(dragOffset, indentationWidth); + const projectedDepth = activeItem.depth + dragDepth; + const maxDepth = getMaxDepth({ + previousItem, + }); + const minDepth = getMinDepth({ nextItem }); + let depth = projectedDepth; + + if (projectedDepth >= maxDepth) { + depth = maxDepth; + } else if (projectedDepth < minDepth) { + depth = minDepth; + } + + return { depth, maxDepth, minDepth, parentId: getParentId() }; + + function getParentId() { + if (depth === 0 || !previousItem) { + return null; + } + + if (depth <= previousItem.depth) { + return previousItem.parentId; + } + + if (depth > previousItem.depth) { + return previousItem.id; + } + + const newParent = newItems + .slice(0, overItemIndex) + .reverse() + .find((item) => item.depth === depth)?.parentId; + + return newParent ?? null; + } +} + +function getMaxDepth({ previousItem }) { + const blocksLayoutFieldname = getBlocksLayoutFieldname( + previousItem?.data || {}, + ); + if (previousItem) { + return isArray(previousItem.data?.[blocksLayoutFieldname]?.items) + ? previousItem.depth + 1 + : previousItem.depth; + } + + return 0; +} + +function getMinDepth({ nextItem }) { + if (nextItem) { + return nextItem.depth; + } + + return 0; +} + +function flatten(items = [], parentId = null, depth = 0) { + return items.reduce((acc, item, index) => { + return [ + ...acc, + { ...item, parentId, depth, index }, + ...flatten(item.children, item.id, depth + 1), + ]; + }, []); +} + +export function flattenTree(items) { + return flatten(items); +} + +export function findItem(items, itemId) { + return items.find(({ id }) => id === itemId); +} + +export function removeChildrenOf(items, ids) { + const excludeParentIds = [...ids]; + + return items.filter((item) => { + if (item.parentId && excludeParentIds.includes(item.parentId)) { + if (item.children.length) { + excludeParentIds.push(item.id); + } + return false; + } + + return true; + }); +} diff --git a/packages/volto/src/components/manage/Blocks/Block/__snapshots__/BlocksForm.test.jsx.snap b/packages/volto/src/components/manage/Blocks/Block/__snapshots__/BlocksForm.test.jsx.snap index 2197f9332e..15c1cb7e5b 100644 --- a/packages/volto/src/components/manage/Blocks/Block/__snapshots__/BlocksForm.test.jsx.snap +++ b/packages/volto/src/components/manage/Blocks/Block/__snapshots__/BlocksForm.test.jsx.snap @@ -114,5 +114,152 @@ exports[`Allow override of blocksConfig 1`] = `
    + `; diff --git a/packages/volto/src/components/manage/Blocks/Container/Edit.jsx b/packages/volto/src/components/manage/Blocks/Container/Edit.jsx index 6c7fa892cb..7c1ce4d9d4 100644 --- a/packages/volto/src/components/manage/Blocks/Container/Edit.jsx +++ b/packages/volto/src/components/manage/Blocks/Container/Edit.jsx @@ -129,6 +129,7 @@ const ContainerBlockEdit = (props) => { blocksConfig={allowedBlocksConfig} title={data.placeholder} isContainer + isMainForm={false} stopPropagation={selectedBlock} disableAddBlockOnEnterKey onSelectBlock={(id) => { diff --git a/packages/volto/src/components/manage/Blocks/Grid/Edit.jsx b/packages/volto/src/components/manage/Blocks/Grid/Edit.jsx index 713ed9a116..4c15f26881 100644 --- a/packages/volto/src/components/manage/Blocks/Grid/Edit.jsx +++ b/packages/volto/src/components/manage/Blocks/Grid/Edit.jsx @@ -1,14 +1,16 @@ import PropTypes from 'prop-types'; import cx from 'classnames'; -import { useState } from 'react'; import ContainerEdit from '../Container/Edit'; +import { useDispatch, useSelector } from 'react-redux'; +import { setUIState } from '@plone/volto/actions'; const GridBlockEdit = (props) => { const { data } = props; const columnsLength = data?.blocks_layout?.items?.length || 0; - const [selectedBlock, setSelectedBlock] = useState(null); + const selectedBlock = useSelector((state) => state.form.ui.gridSelected); + const dispatch = useDispatch(); return (
    { // This is required to enabling a small "in-between" clickable area // for bringing the Grid sidebar alive once you have selected an inner block onClick={(e) => { - if (!e.block) setSelectedBlock(null); + if (!e.block) dispatch(setUIState({ gridSelected: null })); }} role="presentation" > dispatch(setUIState({ gridSelected: id }))} direction="horizontal" />
    diff --git a/packages/volto/src/components/manage/Blocks/Image/__snapshots__/ImageSidebar.test.jsx.snap b/packages/volto/src/components/manage/Blocks/Image/__snapshots__/ImageSidebar.test.jsx.snap index ebfafbc06e..fef97a63b8 100644 --- a/packages/volto/src/components/manage/Blocks/Image/__snapshots__/ImageSidebar.test.jsx.snap +++ b/packages/volto/src/components/manage/Blocks/Image/__snapshots__/ImageSidebar.test.jsx.snap @@ -84,11 +84,13 @@ Array [ }, \\"align\\": { \\"title\\": \\"Alignment\\", - \\"widget\\": \\"align\\" + \\"widget\\": \\"align\\", + \\"default\\": \\"center\\" }, \\"size\\": { \\"title\\": \\"Image size\\", - \\"widget\\": \\"image_size\\" + \\"widget\\": \\"image_size\\", + \\"default\\": \\"l\\" }, \\"href\\": { \\"title\\": \\"Link to\\", diff --git a/packages/volto/src/components/manage/Blocks/Image/schema.js b/packages/volto/src/components/manage/Blocks/Image/schema.js index 33cb387104..a0f8b0be43 100644 --- a/packages/volto/src/components/manage/Blocks/Image/schema.js +++ b/packages/volto/src/components/manage/Blocks/Image/schema.js @@ -77,10 +77,12 @@ export function ImageSchema({ formData, intl }) { align: { title: intl.formatMessage(messages.Align), widget: 'align', + default: 'center', }, size: { title: intl.formatMessage(messages.size), widget: 'image_size', + default: 'l', }, href: { title: intl.formatMessage(messages.LinkTo), diff --git a/packages/volto/src/components/manage/Controlpanels/Relations/RelationsListing.jsx b/packages/volto/src/components/manage/Controlpanels/Relations/RelationsListing.jsx index b79ade4571..a46fc3cdbc 100644 --- a/packages/volto/src/components/manage/Controlpanels/Relations/RelationsListing.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Relations/RelationsListing.jsx @@ -45,7 +45,7 @@ const RelationsListing = ({ const staticCatalogVocabularyQuery = useSelector( (state) => - state.relations?.relations?.[relationtype] + state.relations?.relations?.data?.[relationtype] ?.staticCatalogVocabularyQuery || {}, ); diff --git a/packages/volto/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx b/packages/volto/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx index b4b4a10ac1..3bc8003b15 100644 --- a/packages/volto/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx +++ b/packages/volto/src/components/manage/Controlpanels/Relations/RelationsMatrix.jsx @@ -414,7 +414,7 @@ const RelationsMatrix = (props) => { diff --git a/packages/volto/src/components/manage/Form/Form.jsx b/packages/volto/src/components/manage/Form/Form.jsx index 1a7d3a072e..b48a1eaffe 100644 --- a/packages/volto/src/components/manage/Form/Form.jsx +++ b/packages/volto/src/components/manage/Form/Form.jsx @@ -52,6 +52,7 @@ import { resetMetadataFocus, setSidebarTab, setFormData, + setUIState, } from '@plone/volto/actions'; import { compose } from 'redux'; import config from '@plone/volto/registry'; @@ -230,13 +231,17 @@ class Form extends Component { this.props.setFormData(formData); } + this.props.setUIState({ + selected: selectedBlock, + multiSelected: [], + hovered: null, + }); + // Set initial state this.state = { formData, initialFormData, errors: {}, - selected: selectedBlock, - multiSelected: [], isClient: false, // Ensure focus remain in field after change inFocus: {}, @@ -263,6 +268,12 @@ class Form extends Component { let errors = {}; let activeIndex = 0; + if (!this.props.isFormSelected && prevProps.isFormSelected) { + this.props.setUIState({ + selected: null, + }); + } + if (requestError && prevProps.requestError !== requestError) { errors = FormValidation.giveServerErrorsToCorrespondingFields(requestError); @@ -376,15 +387,6 @@ class Form extends Component { this.setState({ isClient: true }); } - static getDerivedStateFromProps(props, state) { - let newState = { ...state }; - if (!props.isFormSelected) { - newState.selected = null; - } - - return newState; - } - /** * Change field handler * Remove errors for changed field @@ -439,9 +441,9 @@ class Form extends Component { if (event.shiftKey) { const anchor = - this.state.multiSelected.length > 0 - ? blocks_layout.indexOf(this.state.multiSelected[0]) - : blocks_layout.indexOf(this.state.selected); + this.props.uiState.multiSelected.length > 0 + ? blocks_layout.indexOf(this.props.uiState.multiSelected[0]) + : blocks_layout.indexOf(this.props.uiState.selected); const focus = blocks_layout.indexOf(id); if (anchor === focus) { @@ -451,15 +453,16 @@ class Form extends Component { } else { multiSelected = [...blocks_layout.slice(focus, anchor + 1)]; } + window.getSelection().empty(); } if ((event.ctrlKey || event.metaKey) && !event.shiftKey) { - multiSelected = this.state.multiSelected || []; - if (!this.state.multiSelected.includes(this.state.selected)) { - multiSelected = [...multiSelected, this.state.selected]; + multiSelected = this.props.uiState.multiSelected || []; + if (!this.props.uiState.multiSelected.includes(this.state.selected)) { + multiSelected = [...multiSelected, this.props.uiState.selected]; selected = null; } - if (this.state.multiSelected.includes(id)) { + if (this.props.uiState.multiSelected.includes(id)) { selected = null; multiSelected = without(multiSelected, id); } else { @@ -468,9 +471,10 @@ class Form extends Component { } } - this.setState({ + this.props.setUIState({ selected, multiSelected, + gridSelected: null, }); if (this.props.onSelectForm) { @@ -660,141 +664,143 @@ class Form extends Component { /> - { - const newFormData = { - ...formData, - ...newBlockData, - }; - this.setState({ - formData: newFormData, - }); - if (this.props.global) { - this.props.setFormData(newFormData); - } - }} - onSetSelectedBlocks={(blockIds) => - this.setState({ multiSelected: blockIds }) - } - onSelectBlock={this.onSelectBlock} - /> - { - if (this.props.global) { - this.props.setFormData(state.formData); + <> + { + const newFormData = { + ...formData, + ...newBlockData, + }; + this.setState({ + formData: newFormData, + }); + if (this.props.global) { + this.props.setFormData(newFormData); + } + }} + onSetSelectedBlocks={(blockIds) => + this.props.setUIState({ multiSelected: blockIds }) } - return this.setState(state); - }} - /> - { - const newFormData = { - ...formData, - ...newData, - }; - this.setState({ - formData: newFormData, - }); - if (this.props.global) { - this.props.setFormData(newFormData); - } - }} - onChangeField={this.onChangeField} - onSelectBlock={this.onSelectBlock} - properties={formData} - navRoot={navRoot} - type={type} - pathname={this.props.pathname} - selectedBlock={this.state.selected} - multiSelected={this.state.multiSelected} - manage={this.props.isAdminForm} - allowedBlocks={this.props.allowedBlocks} - showRestricted={this.props.showRestricted} - editable={this.props.editable} - isMainForm={this.props.editable} - // Properties to pass to the BlocksForm to match the View ones - history={this.props.history} - location={this.props.location} - token={this.props.token} - /> - {this.state.isClient && - this.state.sidebarMetadataIsAvailable && - this.props.editable && - createPortal( - 0} - > - {schema && - map(schema.fieldsets, (fieldset) => ( - -
    + { + if (this.props.global) { + this.props.setFormData(state.formData); + } + return this.setState(state); + }} + /> + { + const newFormData = { + ...formData, + ...newData, + }; + this.setState({ + formData: newFormData, + }); + if (this.props.global) { + this.props.setFormData(newFormData); + } + }} + onChangeField={this.onChangeField} + onSelectBlock={this.onSelectBlock} + properties={formData} + navRoot={navRoot} + type={type} + pathname={this.props.pathname} + selectedBlock={this.props.uiState.selected} + multiSelected={this.props.uiState.multiSelected} + manage={this.props.isAdminForm} + allowedBlocks={this.props.allowedBlocks} + showRestricted={this.props.showRestricted} + editable={this.props.editable} + isMainForm={this.props.editable} + // Properties to pass to the BlocksForm to match the View ones + history={this.props.history} + location={this.props.location} + token={this.props.token} + /> + {this.state.isClient && + this.state.sidebarMetadataIsAvailable && + this.props.editable && + createPortal( + 0} + > + {schema && + map(schema.fieldsets, (fieldset) => ( + - - {fieldset.title} - {metadataFieldsets.includes(fieldset.id) ? ( - - ) : ( - - )} - - - - {map(fieldset.fields, (field, index) => ( - - ))} - - -
    -
    - ))} -
    , - document.getElementById('sidebar-metadata'), - )} - - + + {fieldset.title} + {metadataFieldsets.includes(fieldset.id) ? ( + + ) : ( + + )} + + + + {map(fieldset.fields, (field, index) => ( + + ))} + + + + + ))} + , + document.getElementById('sidebar-metadata'), + )} + + +
    ) @@ -964,6 +970,7 @@ export default compose( (state, props) => ({ content: state.content.data, globalData: state.form?.global, + uiState: state.form?.ui, metadataFieldsets: state.sidebar?.metadataFieldsets, metadataFieldFocus: state.sidebar?.metadataFieldFocus, }), @@ -971,6 +978,7 @@ export default compose( setMetadataFieldsets, setSidebarTab, setFormData, + setUIState, resetMetadataFocus, }, null, diff --git a/packages/volto/src/components/manage/Sidebar/Sidebar.jsx b/packages/volto/src/components/manage/Sidebar/Sidebar.jsx index 790cb8bc94..57a7badc7d 100644 --- a/packages/volto/src/components/manage/Sidebar/Sidebar.jsx +++ b/packages/volto/src/components/manage/Sidebar/Sidebar.jsx @@ -34,12 +34,23 @@ const messages = defineMessages({ id: 'Expand sidebar', defaultMessage: 'Expand sidebar', }, + order: { + id: 'Order', + defaultMessage: 'Order', + }, }); const Sidebar = (props) => { const dispatch = useDispatch(); const intl = useIntl(); - const { cookies, content, documentTab, blockTab, settingsTab } = props; + const { + cookies, + content, + documentTab, + blockTab, + settingsTab, + orderTab = true, + } = props; const [expanded, setExpanded] = useState( cookies.get('sidebar_expanded') !== 'false', ); @@ -172,6 +183,22 @@ const Sidebar = (props) => { ), }, + !!orderTab && { + menuItem: intl.formatMessage(messages.order), + pane: ( + + + + ), + }, !!settingsTab && { menuItem: intl.formatMessage(messages.settings), pane: ( diff --git a/packages/volto/src/components/manage/Sidebar/__snapshots__/Sidebar.test.jsx.snap b/packages/volto/src/components/manage/Sidebar/__snapshots__/Sidebar.test.jsx.snap index 67187ba13f..429e9a9d30 100644 --- a/packages/volto/src/components/manage/Sidebar/__snapshots__/Sidebar.test.jsx.snap +++ b/packages/volto/src/components/manage/Sidebar/__snapshots__/Sidebar.test.jsx.snap @@ -53,6 +53,12 @@ Array [ > Block + + Order +
    + ,
    { maxLength, placeholder, isDisabled, + value, } = props; const inputId = `field-${id}`; - const [value, setValue] = useState(flattenToAppURL(props.value)); const [isInvalid, setIsInvalid] = useState(false); + /** * Clear handler * @method clear * @param {Object} value Value - * @returns {undefined} + * @returns {string} Empty string */ const clear = () => { - setValue(''); - onChange(id, undefined); + onChange(id, ''); }; const onChangeValue = (_value) => { @@ -63,8 +63,6 @@ export const InternalUrlWidget = (props) => { } } - setValue(newValue); - newValue = isInternalURL(newValue) ? flattenToAppURL(newValue) : newValue; if (!isInternalURL(newValue) && newValue.length > 0) { @@ -75,7 +73,7 @@ export const InternalUrlWidget = (props) => { } } - onChange(id, newValue === '' ? undefined : newValue); + onChange(id, newValue); }; return ( @@ -89,12 +87,10 @@ export const InternalUrlWidget = (props) => { disabled={isDisabled} placeholder={placeholder} onChange={({ target }) => onChangeValue(target.value)} - onBlur={({ target }) => - onBlur(id, target.value === '' ? undefined : target.value) - } + onBlur={({ target }) => onBlur(id, target.value)} onClick={() => onClick()} - minLength={minLength || null} - maxLength={maxLength || null} + minLength={minLength} + maxLength={maxLength} error={isInvalid} /> {value?.length > 0 ? ( diff --git a/packages/volto/src/components/theme/Footer/Footer.jsx b/packages/volto/src/components/theme/Footer/Footer.jsx index 7e08be8873..6bbabbf87f 100644 --- a/packages/volto/src/components/theme/Footer/Footer.jsx +++ b/packages/volto/src/components/theme/Footer/Footer.jsx @@ -42,6 +42,7 @@ const Footer = ({ intl }) => { textAlign="center" id="footer" aria-label="Footer" + tabIndex="-1" > diff --git a/packages/volto/src/components/theme/FormattedDate/FormattedDate.jsx b/packages/volto/src/components/theme/FormattedDate/FormattedDate.jsx index 0f9c0542cf..81947c87b0 100644 --- a/packages/volto/src/components/theme/FormattedDate/FormattedDate.jsx +++ b/packages/volto/src/components/theme/FormattedDate/FormattedDate.jsx @@ -18,13 +18,25 @@ const FormattedDate = ({ const language = useSelector((state) => locale || state.intl.locale); const toDate = (d) => (typeof d === 'string' ? new Date(d) : d); const args = { date, long, includeTime, format, locale: language }; + const new_date = new Date(toDate(date)); + // Dat check taken from https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript#1353711 + if (Object.prototype.toString.call(new_date) === '[object Date]') { + // it is a date + if (isNaN(new_date)) { + // date object is not valid + return bad date; + } + } else { + // not a date object + return not a date; + } return (