diff --git a/docs/volto_customization/extending_teasers.md b/docs/volto_customization/extending_teasers.md deleted file mode 100644 index 10813202..00000000 --- a/docs/volto_customization/extending_teasers.md +++ /dev/null @@ -1,594 +0,0 @@ ---- -myst: - html_meta: - "description": "How to support extensions per teaser block" - "property=og:description": "How to support extensions per teaser block" - "property=og:title": "Block Extensions" - "keywords": "Volto, Block, Variations" ---- - -# Extending Teasers per type(Advanced topic) - -The basic scenario is to add variations to a block so that it can give control over its look and feel. Sometimes its also possible for a need to have control over individual elements. For instance, Consider we have a teaaser grid in which we can have a base variation of its layout. Then we would left with styling and adjusting individual teasers. This is where extensions come into play. - -In this chapter we will tweak our newly created variation to also support extensions per teaser block and then later we will add grid support to teasers. - -## Block Extensions - -Block extensions are the way to display a new form of your block for a particular block type. For instance if you have a teaserGrid, with block extensions you can control the styling and behaviour of individual teasers. The split of responsibilites is as follows: "the variation will control how the teasers layout and extension will control the individual rendering." - -We already learn about the block variation in the former chapters. We will now add the teaser block extensions the same way we do for variations. - -```js -import TeaserBlockImageDefault from "volto-teaser-tutorial/components/extensions/TeaserBlockImageDefault"; -import TeaserBlockImageRight from "volto-teaser-tutorial/components/extensions/TeaserBlockImageRight"; -import TeaserBlockImageOverlay from "volto-teaser-tutorial/components/extensions/TeaserBlockImageOverlay"; - -config.blocks.blocksConfig.teaser.extensions = { - ...(config.blocks.blocksConfig.teaser.extensions || {}), - cardTemplates: { - items: [ - { - id: "card", - isDefault: true, - title: "Card (default)", - template: TeaserBlockImageDefault, - }, - { - id: "imageOnRight", - isDefault: false, - title: "Image Right", - template: TeaserBlockImageRight, - }, - { - id: "imageOverlay", - isDefault: false, - title: "Image Overlay", - template: TeaserBlockImageOverlay, - }, - ], - }, -}; -``` - -As for the training we created only three extensions namely `TeaserBlockImageDefault`, `TeaserBlockImageRight` and `TeaserBlockImageOverlay` into our addons `components/extensions` folder. - -In order to support these extension first we need to add a special fieldset to our variation schema so that we can seperate the concerns about individual teasers and put these extensions under it. - -Luckily, In order to do that we have a helper in volto which automatic adds a select field where we want in the schema to display variations/extensions from a particualar block. As its a standalone method we can also add a variation from a completely different block like we do in SearchBlock in volto. - -Go ahead and register that in index.js in our variation schemaEnhancer: - -```jsx -import { addExtensionFieldToSchema } from '@plone/volto/helpers/Extensions/withBlockSchemaEnhancer'; - -config.blocks.blocksConfig.teaser.variations = [ - ...config.blocks.blocksConfig.teaser.variations, - { - id: 'image-top-variation', - title: 'Image(Top) variation', - template: TeaserBlockImageVariation, - isDefault: false, - schemaEnhancer: ({ schema, FormData, intl }) => { - const extension = 'cardTemplates'; - schema.fieldsets.push({ - id: 'Cards', - title: 'Cards', - fields: [], - }); - addExtensionFieldToSchema({ - schema, - name: extension, - items: config.blocks.blocksConfig.teaser.extensions[extension]?.items, - intl, - title: { id: 'Card Type' }, - insertFieldToOrder: (schema, extension) => { - const cardFieldSet = schema.fieldsets.find( - (item) => item.id === 'Cards', - ).fields; - if (cardFieldSet.indexOf(extension) === -1) - cardFieldSet.unshift(extension); - }, - }); - ... -``` - -Notice first we added a new fieldSet where our extensions will recide and the method `addExtensionFieldToSchema` imported volto core. This method as mentioned above adds a new field in the given fieldSet with the given extenionName `cardTemplates`. - -```{note} -By default `addExtensionFieldToSchema` adds the extewnsion field to default fieldSet, in order to ovverride that you can pass `insertFieldToOrder` method to specifiy where it should be added. - -``` - -Woot. We will now have our extensions loaded into the schema. We will have to refactor our intial Variation code to adapt the extension now. - -```{note} -The concept of extensions are only possible if the code of your block allows it. That is the reason why we created a variation out of a teaser block at the first place. -``` - -So our `TeaserBlockImageVariation` can be simplified now as: - -```jsx -import React from "react"; -import PropTypes from "prop-types"; -import { useIntl } from "react-intl"; -import cloneDeep from "lodash/cloneDeep"; -import config from "@plone/volto/registry"; - -const TeaserBlockImageVariation = (props) => { - const { data, extension = "cardTemplates" } = props; - const intl = useIntl(); - - const teaserExtenstions = - config.blocks.blocksConfig?.teaser?.extensions[extension].items; - let activeItem = teaserExtenstions.find( - (item) => item.id === data[extension] - ); - const extenionSchemaEnhancer = activeItem?.schemaEnhancer; - if (extenionSchemaEnhancer) - extenionSchemaEnhancer({ - schema: cloneDeep(config.blocks.blocksConfig?.teaser?.blockSchema), - data, - intl, - }); - const ExtensionToRender = activeItem?.template; - - return ExtensionToRender ? ( -
- -
- ) : null; -}; - -TeaserBlockImageVariation.propTypes = { - data: PropTypes.objectOf(PropTypes.any).isRequired, - isEditMode: PropTypes.bool, -}; - -export default TeaserBlockImageVariation; -``` - -We have added the "active extension" logic to this code and removed the templating code to their individual components under `extensions/` folder. -Notice that we can also add more schema Enhancers to our base variation schema if each extensions can provide it. - -The `ExtensionToRender` will be selected extensions from `extensions/` folder. - -Create a folder named `extensions/` within `components` and add three components provided below: - -TeaserBlockImageDefault: - -```jsx -import React from "react"; -import PropTypes from "prop-types"; -import { Message } from "semantic-ui-react"; -import { defineMessages, useIntl } from "react-intl"; - -import imageBlockSVG from "@plone/volto/components/manage/Blocks/Image/block-image.svg"; - -import { - flattenToAppURL, - isInternalURL, - addAppURL, -} from "@plone/volto/helpers"; -import { MaybeWrap } from "@plone/volto/components"; -import { formatDate } from "@plone/volto/helpers/Utils/Date"; -import { UniversalLink } from "@plone/volto/components"; -import cx from "classnames"; -import config from "@plone/volto/registry"; - -const messages = defineMessages({ - PleaseChooseContent: { - id: "Please choose an existing content as source for this element", - defaultMessage: - "Please choose an existing content as source for this element", - }, -}); - -const DefaultImage = (props) => {props.alt; - -const TeaserBlockImageDefault = (props) => { - const { className, data, isEditMode } = props; - const locale = config.settings.dateLocale || "en"; - const intl = useIntl(); - const href = data.href?.[0]; - const image = data.preview_image?.[0]; - const align = data?.styles?.align; - const creationDate = data.href?.[0]?.CreationDate; - const formattedDate = formatDate({ - date: creationDate, - format: { - year: "numeric", - month: "short", - day: "2-digit", - }, - locale: locale, - }); - - const Image = config.getComponent("Image").component || DefaultImage; - const { openExternalLinkInNewTab } = config.settings; - - return ( -
- <> - {!href && isEditMode && ( - -
- -

{intl.formatMessage(messages.PleaseChooseContent)}

-
-
- )} - {href && ( - -
- {(href.hasPreviewImage || href.image_field || image) && ( -
- -
- )} -
- {data?.head_title && ( -
{data.head_title}
- )} -

{data?.title}

- {data.creationDate &&

{formattedDate}

} - {!data.hide_description &&

{data?.description}

} -
-
-
- )} - -
- ); -}; - -TeaserBlockImageDefault.propTypes = { - data: PropTypes.objectOf(PropTypes.any).isRequired, - isEditMode: PropTypes.bool, -}; - -export default TeaserBlockImageDefault; -``` - -TeaserBlockImageRight: - -```jsx -import React from "react"; -import PropTypes from "prop-types"; -import { Message } from "semantic-ui-react"; -import { defineMessages, useIntl } from "react-intl"; - -import imageBlockSVG from "@plone/volto/components/manage/Blocks/Image/block-image.svg"; - -import { - flattenToAppURL, - isInternalURL, - addAppURL, -} from "@plone/volto/helpers"; -import { MaybeWrap } from "@plone/volto/components"; -import { formatDate } from "@plone/volto/helpers/Utils/Date"; -import { UniversalLink } from "@plone/volto/components"; -import cx from "classnames"; -import config from "@plone/volto/registry"; - -const messages = defineMessages({ - PleaseChooseContent: { - id: "Please choose an existing content as source for this element", - defaultMessage: - "Please choose an existing content as source for this element", - }, -}); - -const DefaultImage = (props) => {props.alt; - -const TeaserBlockImageRight = (props) => { - const { className, data, isEditMode } = props; - const locale = config.settings.dateLocale || "en"; - const intl = useIntl(); - const href = data.href?.[0]; - const image = data.preview_image?.[0]; - const align = data?.styles?.align; - const creationDate = data.href?.[0]?.CreationDate; - const formattedDate = formatDate({ - date: creationDate, - format: { - year: "numeric", - month: "short", - day: "2-digit", - }, - locale: locale, - }); - - const Image = config.getComponent("Image").component || DefaultImage; - const { openExternalLinkInNewTab } = config.settings; - - return ( -
- <> - {!href && isEditMode && ( - -
- -

{intl.formatMessage(messages.PleaseChooseContent)}

-
-
- )} - {href && ( - -
-
- {data?.head_title && ( -
{data.head_title}
- )} -

{data?.title}

- {data.creationDate &&

{formattedDate}

} - {!data.hide_description &&

{data?.description}

} -
- {(href.hasPreviewImage || href.image_field || image) && ( -
- -
- )} -
-
- )} - -
- ); -}; - -TeaserBlockImageRight.propTypes = { - data: PropTypes.objectOf(PropTypes.any).isRequired, - isEditMode: PropTypes.bool, -}; - -export default TeaserBlockImageRight; -``` - -TeaserBlockImageOverlay: - -```jsx -import React from "react"; -import PropTypes from "prop-types"; -import { Message } from "semantic-ui-react"; -import { defineMessages, useIntl } from "react-intl"; -import cloneDeep from "lodash/cloneDeep"; -import imageBlockSVG from "@plone/volto/components/manage/Blocks/Image/block-image.svg"; -import { - flattenToAppURL, - isInternalURL, - addAppURL, -} from "@plone/volto/helpers"; -import { MaybeWrap } from "@plone/volto/components"; -import { formatDate } from "@plone/volto/helpers/Utils/Date"; -import { UniversalLink } from "@plone/volto/components"; -import cx from "classnames"; -import config from "@plone/volto/registry"; - -const messages = defineMessages({ - PleaseChooseContent: { - id: "Please choose an existing content as source for this element", - defaultMessage: - "Please choose an existing content as source for this element", - }, -}); - -const DefaultImage = (props) => {props.alt; - -const TeaserBlockImageOverlay = (props) => { - const { className, data, isEditMode, extension = "cardTemplates" } = props; - const locale = config.settings.dateLocale || "en"; - const intl = useIntl(); - const href = data.href?.[0]; - const image = data.preview_image?.[0]; - const align = data?.styles?.align; - const creationDate = data.href?.[0]?.CreationDate; - const formattedDate = formatDate({ - date: creationDate, - format: { - year: "numeric", - month: "short", - day: "2-digit", - }, - locale: locale, - }); - - const teaserExtenstions = - config.blocks.blocksConfig?.teaser?.extensions[extension].items; - let activeItem = teaserExtenstions.find( - (item) => item.id === data[extension] - ); - const extenionSchemaEnhancer = activeItem?.schemaEnhancer; - if (extenionSchemaEnhancer) - extenionSchemaEnhancer({ - schema: cloneDeep(config.blocks.blocksConfig?.teaser?.blockSchema), - data, - intl, - }); - - const Image = config.getComponent("Image").component || DefaultImage; - const { openExternalLinkInNewTab } = config.settings; - - return ( -
- <> - {!href && isEditMode && ( - -
- -

{intl.formatMessage(messages.PleaseChooseContent)}

-
-
- )} - {href && ( - -
- {(href.hasPreviewImage || href.image_field || image) && ( -
- -
- )} - -
- {data?.head_title && ( -
{data.head_title}
- )} -
-

{data?.title}

- {!data.hide_description &&

{data?.description}

} - {data?.creationDate && ( -

{formattedDate}

- )} -
-
-
-
- )} - -
- ); -}; - -TeaserBlockImageOverlay.propTypes = { - data: PropTypes.objectOf(PropTypes.any).isRequired, - isEditMode: PropTypes.bool, -}; - -export default TeaserBlockImageOverlay; -``` - -The styles have to be added as well: - -```{code-block} less -:force: true -.gradiant { - h2 { - color: white; - } - position: absolute; - bottom: 30px; - display: flex; - width: 100%; - height: 200px; - align-items: flex-end; - padding: 1.5rem; - background-image: linear-gradient( - 13.39deg, - rgba(46, 62, 76, 0.65) 38.6%, - rgba(46, 62, 76, 0.169) 59.52%, - rgba(69, 95, 106, 0) 79.64% - ); -} - -.teaser-item.overlay { - display: flex; - - .image-wrapper { - width: 100%; - } -} - -.has--objectFit--contain { - img { - object-fit: contain !important; - } -} - -.has--objectFit--cover { - img { - object-fit: cover !important; - } -} - -.has--objectFit--fill { - img { - object-fit: fill !important; - } -} - -.has--objectFit--scale-down { - img { - object-fit: scale-down !important; - } -} -``` - -Great. We now have extension per teaser in our block which controls each item individually. - -## Grid support to Teasers - -As mentioned before we will configure the grid-block to also use our extended teaser-block. -We need to override the grid-block configuration with our teaser block. - -In your volto-teaser-tutorial addon's `index.js`: - -```js -//This ensures that grid block uses our overridden teaser -config.blocks.blocksConfig.gridBlock.blocksConfig.teaser = - config.blocks.blocksConfig.teaser; -``` - -Woot. We will now have a grid block with our teaser variations so that each teaser can now have its own set of extensions. diff --git a/docs/volto_customization/index.md b/docs/volto_customization/index.md index e8c55a48..b30c67b3 100644 --- a/docs/volto_customization/index.md +++ b/docs/volto_customization/index.md @@ -55,8 +55,6 @@ schema teaser_variations data_adapters styling -extending_teasers -listing_block ``` diff --git a/docs/volto_customization/installation.md b/docs/volto_customization/installation.md index 9f1c64de..7e6d1309 100644 --- a/docs/volto_customization/installation.md +++ b/docs/volto_customization/installation.md @@ -38,7 +38,7 @@ They assume you have a macOS/Linux machine. ### Python -Installing Python is beyond the scope of this documentation. However, it is recommended to use a Python version manager, pyenv, that allows you to install multiple versions of Python on your development environment without destroying your system's Python. +Installing Python is beyond the scope of this training. However, it is recommended to use a Python version manager, pyenv, that allows you to install multiple versions of Python on your development environment without destroying your system's Python. Plone requires Python version 3.8, 3.9, 3.10, 3.11, or 3.12. @@ -81,10 +81,10 @@ To bootstrap a new Plone project(with both backend and frontend), you can use [C You can use pipx to run cookieplone (see instructions for installation): ```shell -pipx run cookieplone tutorial-project +pipx run cookieplone project ``` -You will be presented with a series of prompts. You can also specify the addons you want to install along with the project. You can type `volto-teaser-tutorial` when prompted for the addons. +You will be presented with a series of prompts. You can also specify the addons you want to install along with the project. You can type the name of the project as `tutorial-project` and name of the addon as `volto-teaser-tutorial` when prompted for the addons. ```shell [11/17] Volto Addon Name (volto-project-title): volto-teaser-tutorial @@ -93,10 +93,6 @@ You will be presented with a series of prompts. You can also specify the addons You can accept the rest of the default values in square brackets (`[default-option]`) by hitting the {kbd}`Enter` key, or enter your preferred values. For the training, we will use the default values for the rest of the prompts. -```{tip} -See the cookiecutter's README for how to [Use options to avoid prompts](https://github.com/collective/cookieplone/?tab=readme-ov-file#use-options-to-avoid-prompts). -``` - ## Install the project To work on your project, you need to install both the frontend and backend. diff --git a/docs/volto_customization/listing_block.md b/docs/volto_customization/listing_block.md deleted file mode 100644 index 7ee3b265..00000000 --- a/docs/volto_customization/listing_block.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -myst: - html_meta: - "description": "How to create a block variation" - "property=og:description": "How to create a block variation" - "property=og:title": "Create a simple listing block variation" - "keywords": "Volto, Training, Block variation" ---- - -# Create a simple listing block variation - -We will create a variation for listing block, the approach is the same we did for teasers. A new variation which will control the layout and extensions which will take care of individual items styling. - -First of all let's add a styling fieldset in the current schema of volto's default listing block . - -In your addon config: - -```js -import { addStylingFieldset } from "volto-teaser-tutorial/components/helpers"; - -if (config.blocks.blocksConfig.listing) { - config.blocks.blocksConfig.listing.title = "Listing (Tutorial)"; - config.blocks.blocksConfig.listing.schemaEnhancer = addStylingFieldset; -} -``` - -Create a file named `helpers.js` inside `components/` folder and add the relevant schema enhancer for it: - -```js -import { cloneDeep } from "lodash"; -import imageNarrowSVG from "@plone/volto/icons/image-narrow.svg"; -import imageFitSVG from "@plone/volto/icons/image-fit.svg"; -import imageWideSVG from "@plone/volto/icons/image-wide.svg"; -import imageFullSVG from "@plone/volto/icons/image-full.svg"; - -export const ALIGN_INFO_MAP = { - narrow_width: [imageNarrowSVG, "Narrow width"], - container_width: [imageFitSVG, "Container width"], - wide_width: [imageWideSVG, "Wide width"], - full: [imageFullSVG, "Full width"], -}; - -export const addStylingFieldset = ({ schema }) => { - const applied = schema?.properties?.styles; - - if (!applied) { - const resSchema = cloneDeep(schema); - - resSchema.fieldsets.push({ - id: "styling", - fields: ["styles"], - title: "Styling", - }); - resSchema.properties.styles = { - widget: "object", - title: "Styling", - schema: { - fieldsets: [ - { - id: "default", - title: "Default", - fields: ["size"], - }, - ], - properties: { - size: { - widget: "align", - title: "Section size", - actions: Object.keys(ALIGN_INFO_MAP), - actionsInfoMap: ALIGN_INFO_MAP, - }, - }, - required: [], - }, - }; - return resSchema; - } - - return schema; -}; -``` - -This function will inject styles field into the schema if isn't present already. We can add relevant styling here. Volto will build classNames based on the styles as mentioned in the earlier chapters. We will have to provide our own css for the generated classNames. - -```less -:force: true - -#main .has--size--narrow_width, -#main .narrow_width, -[class~="narrow_view"] [id="page-document"] > * { - max-width: var(--narrow-text-width, 500px) !important; -} - -#main .container_width, -#main .has--size--container_width, -.view-wrapper > *, -[class~="view-defaultview"] [id="page-document"] > *, -[class~="view-viewview"] [id="page-document"] > * { - max-width: var(--container-text-width, 1120px) !important; -} -``` - -In order to have a control over individual items in the listing let's create a sample variation of listing block. - -```js -import ListingVariation from "volto-teaser-tutorial/components/ListingBlockVariation"; - -config.blocks.blocksConfig.listing.variations = [ - ...(config.blocks.blocksConfig.listing.variations || []), - { - id: "tutorial", - isDefault: false, - title: "Sample Variation", - template: ListingVariation, - schemaEnhancer: ({ schema, FormData, intl }) => { - const extension = "cardTemplates"; - schema.fieldsets.push({ - id: "Cards", - title: "Cards", - fields: [], - }); - addExtensionFieldToSchema({ - schema, - name: extension, - items: config.blocks.blocksConfig.teaser.extensions[extension]?.items, - intl, - title: { id: "Card Type" }, - insertFieldToOrder: (schema, extension) => { - const cardFieldSet = schema.fieldsets.find( - (item) => item.id === "Cards" - ).fields; - if (cardFieldSet.indexOf(extension) === -1) - cardFieldSet.unshift(extension); - }, - }); - return schema; - }, - }, -]; -``` - -Notice that here we will keep the schemaEnhancer configuration of teaser extensions. For better readability we can also move these lines of code into a `baseSchemaEnhancer` which will serve for both listing and teaser block extensions. But we can leave it up to the user for now. - -Finally we write our own variation for ListingBlock: - -ListingBlockVariation.jsx - -```jsx -import React from "react"; -import PropTypes from "prop-types"; -import cloneDeep from "lodash/cloneDeep"; -import { useIntl } from "react-intl"; -import { ConditionalLink, UniversalLink } from "@plone/volto/components"; -import { flattenToAppURL } from "@plone/volto/helpers"; -import config from "@plone/volto/registry"; - -import { isInternalURL } from "@plone/volto/helpers/Url/Url"; - -const ListingVariation = (props) => { - const { - items, - linkTitle, - linkHref, - isEditMode, - data, - extension = "cardTemplates", - } = props; - let link = null; - let href = linkHref?.[0]?.["@id"] || ""; - - if (isInternalURL(href)) { - link = ( - - {linkTitle || href} - - ); - } else if (href) { - link = {linkTitle || href}; - } - - const intl = useIntl(); - - const teaserExtenstions = - config.blocks.blocksConfig?.teaser?.extensions[extension].items; - let activeItem = teaserExtenstions.find( - (item) => item.id === props?.[extension] - ); - const extenionSchemaEnhancer = activeItem?.schemaEnhancer; - if (extenionSchemaEnhancer) - extenionSchemaEnhancer({ - schema: cloneDeep(config.blocks.blocksConfig?.teaser?.blockSchema), - data: data || props, - intl, - }); - const ExtensionToRender = activeItem?.template; - - return ( - <> -
- {items.map((item) => ( -
- -
- ))} -
- - {link &&
{link}
} - - ); -}; - -ListingVariation.propTypes = { - items: PropTypes.arrayOf(PropTypes.any).isRequired, - linkMore: PropTypes.any, - isEditMode: PropTypes.bool, -}; -export default ListingVariation; -``` - -We will now have the per listing item styling support like we have for teaser blocks. We can also add more styling schema with the help of its individual schema extenders. diff --git a/docs/volto_customization/schema.md b/docs/volto_customization/schema.md index cc36b30c..ca12b433 100644 --- a/docs/volto_customization/schema.md +++ b/docs/volto_customization/schema.md @@ -137,7 +137,14 @@ const TeaserBlockImageDefault = (props) => { : null } > -
+
{(href.hasPreviewImage || href.image_field || image) && (
Body component in Teaser block also supports adding variations from component registry. You can read more about component registry in following chapters. diff --git a/docs/volto_customization/shadowing.md b/docs/volto_customization/shadowing.md index e43cbd4c..f09503cb 100644 --- a/docs/volto_customization/shadowing.md +++ b/docs/volto_customization/shadowing.md @@ -29,7 +29,7 @@ For example when we customize the News Item View instead of adding the override we add it as -`src/addon/volto-teaser-tutorial/src/customizations/volto/components/theme/View/NewsItemView.jsx`. +`packages/volto-teaser-tutorial/src/customizations/volto/components/theme/View/NewsItemView.jsx`. Both paths work fine though, we just want to go all-in with the addon-approach. diff --git a/docs/volto_customization/teaser_variations.md b/docs/volto_customization/teaser_variations.md index f22cf739..1f4f1ae9 100644 --- a/docs/volto_customization/teaser_variations.md +++ b/docs/volto_customization/teaser_variations.md @@ -130,7 +130,14 @@ const TeaserBlockImageDefault = (props) => { : null } > -
+
{(href.hasPreviewImage || href.image_field || image) && (
config/index.js of Volto itself which is available in your projects in `frontend/omelette/src/config/index.js`. +You can find all existing options in the file config/index.js of Volto itself which is available in your projects in `frontend/core/packages/volto/src/config/index.js`. ```{seealso} Many options are explained in the {doc}`plone6docs:volto/configuration/settings-reference`