Compare commits

...
Author SHA1 Message Date
Kenn JacobsenandGitHub e5458e7e88 Merge branch 'v19/dev' into v19/feature/rethink-segment-model 2026-06-24 08:03:50 +02:00
b57867ab70 Global Elements: Reusable Content of Blocks (#22448)
* Slice 1: Reference Model

A block layout item's contentKey can point to either local inline
content or a library element. For shared content, the `isSharedContent`
flag is set to `true`.

* Slice 2: Insert Block from Library

* Slice 3: Transfer to Library

* Slice 4: Disconnect from Library

* Slice 5: Inline Element Editing from Block Context

* [WIP] Slice 6: Publish Awareness

* fix(block): address code review findings

Critical:
- disconnectFromLibrary now sets initial expose for new local content
  and cleans up resolved variant state entry
- Extract #updateExposedState() in entry elements, called from all three
  observers (hasExpose, isLibraryElement, sharedContentVariantState) to
  prevent stale unpublished state on library blocks

Important:
- Guard #fetchLibraryElement against already-resolved elements to prevent
  redundant server requests
- Hoist UmbElementDetailRepository to class field in entry elements to
  avoid accumulating dead controllers

Suggestion:
- Fix umb-localize key attributes to use literal keys instead of
  resolved strings from localize.term()

* Enable reusable elements in block editors, including indexing for search and output rendering

* Update cache levels for property value converters.

* Add keys to block layout items

* fix(block): address review findings for reusable block content

- Use DocumentVariantStateModel.DRAFT enum instead of magic string
  in both block-list and block-grid entry elements
- Strip isSharedContent from layout during clipboard write to prevent
  pasted blocks from incorrectly appearing as library references
- Guard #setInitialBlockExpose in disconnectFromLibrary against missing
  content type structure
- Store all element variants and resolve against active variantId for
  correct multi-culture state display
- Add already-resolved guard to #fetchLibraryElement
- Add JSDoc on isLibraryElement and sharedContentVariantState observables
- Add .trim() to transfer modal name validation

* feat(block): add layout key and migrate identity from contentKey to key

BREAKING: UmbBlockLayoutBaseModel now requires a `key: string` property.
Plugin code that creates layout objects without `key` will get a compile
error.

- Change UmbArrayState identity functions to use `(x) => x.key`
- Add `layout` setter on entry elements (list, grid, single, rte) that
  extracts both layoutKey and contentKey from the layout object
- Deprecate `contentKey` setter on entry elements (use `layout` instead)
- Add `layoutKey` read-only getter for sorter identity
- Add `setLayoutKey()` / `layoutByKey()` / `getLayoutByKey()` methods
- Update `transferToLibrary` and `disconnectFromLibrary` to take layoutKey
- Update delete operations to find by layout key, only remove shared
  content/settings/exposes if no other layout references the same contentKey
- Migrate grid recursive area operations to use key for identity
- Update `unique` observable on entry context to derive from layout key
- Generate new key on property value clone
- Backwards compat: `setLayouts` assigns `key ??= contentKey` for
  persisted data without key
- Strip `isSharedContent` from clipboard layout clone
- Update sorter configs and repeat key functions

* Add tests proving that reusable content can work with RTEs too

* i18n: capitalize Element/Library in disconnect-from-library strings

Per Niels' feedback on PR #22448 — Element and Library are product nouns
and should be capitalized to distinguish from generic uses.

* refactor(block): rename layout/library APIs for consistency

- `setLayoutKey`/`getLayoutKey` → `setKey`/`getKey` on entry context
- `layoutByKey`/`getLayoutByKey` → `byKey`/`getByKey` on entries context
- `layoutKey` property → `key` on block entry elements (list/grid/single/rte)
- `data-layout-key` attribute → `data-key` on `umb-rte-block`
- `insertLibraryElementReference` → `insertLibraryElement` on manager
- `transferToLibrary`/`disconnectFromLibrary` `layoutKey` param → `key`
- `delete(layoutKey)` param → `delete(key)` on entries context
- `allowedLibraryElementTypeKeys` → `libraryAllowedElementTypeKeys` on catalogue modal data

* refactor(block): convert catalogue modal value to discriminated union

`UmbBlockCatalogueModalValue` was a single object with optional `create`,
`clipboard`, and `library` fields, which let invalid combinations type-check.
Convert it to a true discriminated union so consumers must narrow with `'in'`
before accessing the variant payload.

Update all four entries contexts (block-list, block-grid, block-rte, block-single)
to use `value && 'create' in value` style narrowing in their `onSubmit` handlers.

* refactor(block): move library transfer/disconnect handlers to Block Manager

The block entry elements (`umb-block-list-entry`, `umb-block-grid-entry`) each
duplicated the orchestration for transferring a local block's content to the
Element Library and disconnecting a referenced library element back to local
content. The handlers opened modals, scaffolded element data, called the
element repository, and finally mutated manager state — all from the UI element.

Move that logic to the manager as `requestTransferToLibrary(key)` and
`requestDisconnectFromLibrary(key)`. The "request" prefix marks the
user-confirmed flows; the bare `transferToLibrary` / `disconnectFromLibrary`
methods remain as the pure state mutations.

Entry elements now delegate to the manager, which also lets us drop the
per-element `UmbElementDetailRepository` field and the modal-related imports
from the elements.

Confirm modal headlines/labels are now passed as localization keys, letting
the modal handle its own string resolution (per Niels' review feedback).

* refactor(block): deprecation hygiene around contentKey setters

- Stop calling the (deprecated) `setContentKey()` from `set layout` on the
  entry elements. The layout already carries the contentKey, so internal flows
  no longer need the fallback path.
- Add `UmbDeprecation` runtime warnings to all four `set contentKey` element
  setters (list/grid/single/rte) and to `UmbBlockEntryContext.setContentKey`.
  JSDoc `@deprecated` alone is not enough — runtime warnings are required per
  the Web.UI.Client deprecation policy.

* fix(block): preserve isSharedContent through clipboard copy/paste

When copying a block that references a library Element, we were stripping
`isSharedContent` from the cloned layout so that pasting always produced a
local copy. Per Niels' review feedback, the expected behaviour is the inverse:
a copied library-referencing block should paste as a reference. If the user
wants a local copy after paste, they explicitly disconnect from the library.

- Remove the `delete clonedLayout.isSharedContent` in `#copyToClipboard` for
  both block-list and block-grid.
- Branch in `_insertBlockFromPropertyValue` so layouts with `isSharedContent`
  route through the manager's `insertLibraryElement(contentKey, originData)`
  flow rather than expecting matching `contentData` (which the clipboard
  payload deliberately doesn't carry for references).

* refactor(block): centralise library element resolution in the Block Manager

Per Niels' review: the entry context shouldn't be the place where the safety
fetch for library element content lives — there could be other call sites,
and the manager already owns the resolved-elements state.

Add a layouts observer in `UmbBlockManagerContext` that watches `_layouts`
and, for any layout where `isSharedContent` is set, kicks off
`#fetchLibraryElement(contentKey)`. The fetch already dedupes, so this is
safe to call repeatedly.

In return, drop both `_manager.ensureContentResolved(contentKey)` calls from
`UmbBlockEntryContext` (`setContentKey` and `#observeContentData`). Also
derive `#contentKey` from the observed layout so internal flows have access
to it without callers having to push it through the deprecated setter.

* docs(block): add follow-up TODOs from review

- Mark `#fetchLibraryElement` for `@madsrasmussen` to replace with a batching
  manager that bundles multiple element requests into a single round-trip.
  Today's per-key fetch becomes N+1 on pages with many shared blocks.
- Update the catalogue modal TODO to reflect that the catalogue is conceptually
  a Modal/Flow extension point — not a Workspace as the previous comment
  implied. Captures the open question about an extensible "Library tab"
  surface for other content sources.

* fix(block): break circular dep between manager context and modals barrel

`UmbBlockManagerContext` imported `UMB_BLOCK_TRANSFER_TO_LIBRARY_MODAL` from
`../modals/index.js` (the barrel). That barrel transitively pulled in the
catalogue modal element, which sits downstream of the manager — creating:

  context/index → block-manager.context → modals/index
    → modals/block-catalogue/index → block-catalogue-modal.element

Import the token directly from `transfer-to-library-modal.token.ts` instead.

* Elements: Contextualize variant blocks rendering for invariant content (#22790)

* Contextualize variant blocks rendering for invariant content

* Initialize local language variables in a more readable way

* Also filter out whitespace cultures

* Add XML docs.

* feat(block): migrate library transfer/disconnect to blockAction extensions

The `#renderTransferToLibraryAction()` / `#renderDisconnectFromLibraryAction()`
render methods (and their handlers) were commented out when `main` was merged
in, leaving these flows unrendered. Migrate them to the new `blockAction`
extension type (PR #22459) so they:

- Render through `<umb-block-action-list>` like the other common actions.
- Reuse automatically across Block List, Block Grid, Single Block, and RTE
  Block editors — no per-editor code.
- Honour visibility via manifest conditions, not inline state branches.

Additions:
- `UMB_BLOCK_ENTRY_IS_LIBRARY_ELEMENT_CONDITION` — boolean-match condition
  observing the existing `context.isLibraryElement` observable. Used inverted
  by the two new actions.
- `Umb.BlockAction.TransferToLibrary` (weight 250, `icon-link`) — visible when
  `isLibraryElement` is false and the entry is not read-only.
- `Umb.BlockAction.DisconnectFromLibrary` (weight 250, `icon-unlink`) — visible
  when `isLibraryElement` is true and the entry is not read-only. The two
  actions are mutually exclusive so sharing a weight is safe.
- Two thin proxy methods on `UmbBlockEntryContext`
  (`requestTransferToLibrary()` / `requestDisconnectFromLibrary()`) mirroring
  the established `requestDelete()` pattern — actions consume only the entry
  context and call into the manager via these proxies.

Removals:
- The commented-out `#renderTransferToLibraryAction` /
  `#renderDisconnectFromLibraryAction` blocks and their handlers in
  `block-list-entry.element.ts` and `block-grid-entry.element.ts`.

* Renamed `sharedContentVariantStateOf` to `elementStateOf`

* TODO comments and prettify

* Removed `ensureContentResolved`

turns out it was redundant.

* Inlined `transferToLibrary` and `disconnectFromLibrary`

Both were single-use imperative helpers called only by their respective
`request*` counterparts in the same file, with no external callers. The
"request" / "do" split was speculative; folding them in reduces surface
area and matches the recent `ensureContentResolved` cleanup.

* Lifted library-allowed element-type fetch to base entries context

All four block variants (list, grid, rte, single) had the same six-line
block fetching the element-type uniques that overlap with the block
types. Moved into a protected helper `_getLibraryAllowedElementTypeKeys`
on UmbBlockEntriesContext so each variant just calls it.

* Removed `@property` decorator from `layout` setter

The setter had no matching getter, which Lit warns about (and will error
on in a future version) for reactive properties. Since no render template
reads `this.layout` and the setter's effects flow through the entry
context's own observables, the reactive tracking is unused — dropping the
decorator silences the warning without behaviour change.

Consumers using `.layout=${x}` in Lit templates are unaffected; that's
property assignment, not attribute reflection, and doesn't require the
property to be reactive.

* feat(components): adds `umb-entity-frame` component + Storybook stories

Cherry picked from PR https://github.com/umbraco/Umbraco-CMS/pull/22844

* Fixed block delete passing contentKey where layout key is required

`UmbBlockEntriesContext.delete()` was changed earlier on this branch to
take the layout `key` (so that multiple layouts referencing one shared
contentKey can be deleted independently). Two callers still passed
`contentKey`, which made `delete` throw "Cannot delete block, missing
layout for X" the moment a user tried to remove a block:

- `UmbBlockEntryContext.delete()` — fires on user delete from the UI.
- `block-workspace.context.ts` modal-rejected handler — fires when
  cancelling a brand-new block in live-editing mode.

Both now pass the layout key.

* Added `umb-entity-frame` to Block editor entry UI

Adds `--umb-color-reference` and `--umb-color-reference-contrast` CSS variables

* 🧹 Linting

* Block Single: derive `_exposed` from library element variant state

Aligns block-single-entry with block-list-entry and block-grid-entry:
library-element references now compute their unpublished/draft state
from the shared element's variant state instead of the (always-missing)
expose entry. Without this, inserted Library Elements always appeared
as Draft in single-block editors.

Also sets the `is-reference` attribute when the block is a library
reference, which activates the existing `:host([is-reference])` styles.

* Block entries: collapse `_isReferenceAttr` into `_isLibraryElement`

The two fields were always set together to the same value across all
three entry elements. `_isReferenceAttr` existed only because `@state`
doesn't reflect to an HTML attribute. Decorating the existing
`_isLibraryElement` field with `@property({ attribute: 'is-reference',
reflect: true })` covers both jobs — it reflects to the attribute (for
the existing `:host([is-reference])` CSS) and is still read from JS by
`#updateExposedState()`.

* Fix build errors after merges

* Refine block-catalogue-modal Library tab

- Convert _hasLibraryElements from @state() to native private field
  (set once in connectedCallback before first render; no reactivity needed)
- Promote inline .props object to #libraryTreeProps class field
  (stable reference avoids re-setting umb-tree props on every render)
- Remove self-documenting comment from #librarySelectableFilter
- Remove stale TODO comment

* Wire Library tab search in block-catalogue-modal

- Route tree selection through pickerContext.selection (unified path with
  search-result selections; removes direct writes to this.value from tree handlers)
- Observe pickerContext.selection.selection to drive this.value
- Observe pickerContext.search.query to hide tree while a search is active
- Configure selection as single-select (setMultiple(false))
- Pass selectionManager to tree props for visual selection state
- Add Umb.PickerSearchResultItem.Element manifest and element under
  src/packages/elements/picker/ so search results render correctly

* Backoffice: Simplify insertLibraryElement in block-manager.context

Library elements do not need an expose entry — exposure is derived from
the element's own variant state. Remove the redundant fetchLibraryElement
call and expose-setting logic; the layout observer already handles the
fetch automatically when the layout is appended.

* Backoffice: Rename transfer-to-library to transfer-to-element-library

* Sets the Entity Frame color for non-references

* "Transfer to Library" modal updates

Pre-populates the name field.

* Backoffice: Rename disconnect-from-library to disconnect-from-element-library

* Checks published visibility for Block entry items

+ markup tweaks

* Backoffice: Fix block showing as unsupported after Transfer to Element Library

After a transfer the manager assigns a new UUID (created.unique) to the
layout's contentKey. The entry context was not re-observing content for
the new key, leaving it permanently watching the old (now-gone) content.

Two interacting issues:

1. #observeContentData() was never re-called when layout.contentKey
   changed — only when the layout key itself changed or the manager
   first connected.  A new observer on this.contentKey now re-calls it
   on every contentKey change, with this.#contentKey synced first
   (because #observeLayout() assigns it AFTER _layout.setValue() emits,
   so downstream callbacks would otherwise read the stale value).

2. The guard 'if (unsupported !== true)' permanently locked the flag once
   it was set by the transient {content:undefined, isLibrary:false}
   emission during the transfer.  Replaced with #structurallyUnsupported
   — only set by #getContentStructure / #observeBlockType when the block
   type or element type is genuinely absent — so the content observer can
   freely reset the flag for all other transitions including transfer.

* Block Single: CSS selector fix

* Backoffice: Show link icon in block entry tabs for library elements

When a block is transferred to the Element Library (a shared element), add a
<uui-icon name="link"> to the entity-frame tab to make the library/shared
status visually clearer alongside the existing purple colour theme.

Applies to block-list, block-grid, and block-single entry components.
The icon is shown conditionally when _isLibraryElement is true.

* `requestTransferToElementLibrary` removed the `name` parameter

as can be retrieved from the context itself.

* fix(block): make block action href and validation data path reactive

`umb-block-action.element.ts` previously resolved `getHref()` and
`getValidationDataPath()` once in the `api` setter via `.then()`,
freezing the values for the lifetime of the action component. When a
block's `contentKey` changes at runtime (e.g. after disconnecting from
the Element Library), the edit button kept navigating to the stale path.

Add optional `hrefObservable` and `validationDataPathObservable` to
`UmbBlockAction`. When an action provides these observables the element
subscribes to them reactively; otherwise it falls back to the existing
one-shot promise path (non-breaking for third-party actions).

`UmbEditContentBlockAction` now observes `workspaceEditContentPath` and
`contentKey` from the block entry context and pushes updates into states,
resolving the stale-href bug on disconnect.

Resolves the [LK] TODO in block-action.element.ts.

* fix(block): refresh expose observer after disconnect from element library

After "Disconnect from Element Library" the block's layout.contentKey
changes from the shared element's UUID to a fresh local content key. The
expose observer ('observeExpose' in #gotVariantId) was bound to the old
key and never re-bound because #gotVariantId only runs when variantId
changes, not when contentKey changes — leaving _hasExpose false and the
block showing a stale "Draft"/unpublished badge.

Re-running #gotVariantId alongside #observeContentData in the contentKey
observer ensures the expose subscription always targets the current key,
mirroring the existing pattern already applied for the content observer.

* fix(block): correct workspace tabs and submit label for library elements

When a block references a Library Element (isSharedContent: true), opening
the block workspace via the "Edit Settings" action now shows only the
"Settings" tab. The "Content" tab is hidden because the content is owned
by the shared element and is not editable in the local block workspace.

Surfaces `hasContent` on the block workspace context, and gates
the Content workspace view on a new `Umb.Condition.BlockWorkspaceHasContent`
condition — mirroring the existing `Umb.Condition.BlockWorkspaceHasSettings`
pattern. Also removes the dead `TODO_conditions` block from the Content
view manifest.

* refactor(block): rename LibraryElement to SharedContent for naming consistency

Aligns block symbols that describe a block's content being shared/referenced
with the existing 'isSharedContent' layout flag and 'sharedContentVariantState',
retiring the inconsistent 'LibraryElement' naming for that concept.

- Entry state: isLibraryElement -> isSharedContent; #libraryElementWorkspacePath
  -> #sharedContentWorkspacePath; the three entry elements' _isLibraryElement
  -> _isSharedContent.
- Manager: insertLibraryElement -> insertSharedContent; #fetchLibraryElement
  -> #fetchSharedContent; #resolvedLibraryElements(Variants)
  -> #resolvedSharedContent(Variants).
- Condition: UmbBlockEntryIsLibraryElementCondition
  -> UmbBlockEntryHasSharedContentCondition (alias 'Umb.Condition.BlockEntryHasSharedContent').
- Route segment 'library-element' -> 'library'.

Genuine Element Library feature references are intentionally kept: the
transfer/disconnect actions and modals, and the catalogue picker UI
(#hasLibraryElements, #renderLibrary, blockEditor_tabLibrary, the
{ library: { elementKey } } modal value, libraryAllowedElementTypeKeys).

Pure rename, no behaviour change.

* fix(block): address PR review feedback on client-side files

- block-catalogue-modal: add UmbDeselectedEvent import; correctly type
  #onLibraryElementDeselected parameter (was UmbSelectedEvent)
- block-catalogue-modal: fix #librarySelectableFilter to handle
  undefined documentType.unique via nullish coalesce
- block-manager: setLayouts no longer mutates incoming layout objects
  in-place; uses map+spread to ensure backwards-compat key backfill
  without side effects on the caller's array
- block-grid-to-block-copy-translator: clipboard layout key now uses
  gridLayout.key (layout identity) rather than gridLayout.contentKey,
  which would break when the same shared-content element appears in
  multiple layout entries

* Fix low-hanging PR review comments

* Clarify why top-level aggregation works in effect

* Rename IsSharedContent (server-side)

* Rename IsSharedContent (client-side)

to `IsExternalContent`

* Added comments to clarify retries in tests

* Replace "isSharedContent" with "isExternalContent"

* Block: address review feedback — naming, comments, and small refactors

- requestTransferToElementLibrary / requestDisconnectFromElementLibrary → requestTransferToExternalContent / requestDisconnectFromExternalContent (manager + entry context + action callers)
- .addAdditionalPath('library') → 'element'
- #resolvedExternalContent / #resolvedExternalContentVariants → #externalContentValues / #externalContentVariants
- elementStateOf → externalContentStateOf
- hrefObservable / validationDataPathObservable → href / validationDataPath (interface + action impls + default kind element)
- _hasExpose → _localExpose (grid, list, single entry elements)
- BlockWorkspaceHasContentConditionConfig / BlockEntryHasSettingsConditionConfig: type alias → interface
- Remove implementation-specific / AI-ish comments from block-entry, block-manager, action files, block-workspace
- Reuse #elementRepository field in requestTransfer/Disconnect; remove local instantiations
- #fetchExternalContent now accepts an array — one call per layout-state update instead of N
- getHref / getValidationDataPath in edit-content/edit-settings actions now resolve via the observable

* Block: fix CI lint errors — remove unused #context fields and suppress empty-interface rule

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
2026-06-23 15:35:35 +01:00
kjac d39ad2c124 Add test for automatic fallback for missing segment values 2026-06-03 19:16:18 +02:00
kjac 597371e213 Remove ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch as it no longer plays a role in validation 2026-06-03 17:33:56 +02:00
kjac 6cbb6e1c83 Remove Segment from BlockItemVariation 2026-06-03 17:09:46 +02:00
kjac 247e1eb513 Remove Segment from API endpoint models (content level) 2026-06-03 14:38:11 +02:00
kjac a94b5e37ec Remove Segment from VariantModel 2026-06-03 12:30:36 +02:00
kjac 533f42d093 Ignore VariantModel.Segment in all implementation 2026-06-03 12:27:16 +02:00
leekelleher f365493f0b Bump version to 19.0.0-beta1. 2026-06-01 18:08:32 +01:00
178 changed files with 3856 additions and 1154 deletions
@@ -31,10 +31,6 @@ public abstract class ContentControllerBase : ManagementApiControllerBase
.WithTitle("Content type culture variance mismatch")
.WithDetail("The content type variance did not match that of the passed content data.")
.Build()),
ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch => BadRequest(problemDetailsBuilder
.WithTitle("Content type segment variance mismatch")
.WithDetail("The content type variance did not match that of the passed content data.")
.Build()),
ContentEditingOperationStatus.NotFound => NotFound(problemDetailsBuilder
.WithTitle("The content could not be found")
.Build()),
@@ -21,7 +21,7 @@ internal abstract class ContentEditingPresentationFactory<TValueModel, TVariantM
.Variants
.Select(variant => new VariantModel
{
Culture = variant.Culture, Segment = variant.Segment, Name = variant.Name
Culture = variant.Culture, Name = variant.Name
})
};
}
@@ -125,19 +125,14 @@ internal sealed class DocumentEditingPresentationFactory : ContentEditingPresent
private DocumentVariantRequestModel[] MapVariantsToRequestModel(IContent content)
{
IPropertyValue[] propertyValues = content.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
var cultures = content.AvailableCultures.DefaultIfEmpty(null).ToArray();
// The default segment (null) must always be included
var segments = propertyValues.Select(property => property.Segment).Union([null]).Distinct().ToArray();
return cultures
.SelectMany(culture => segments.Select(segment => new DocumentVariantRequestModel
.Select(culture => new DocumentVariantRequestModel
{
Culture = culture,
Segment = segment,
Name = content.GetCultureName(culture) ?? string.Empty,
}))
})
.ToArray();
}
@@ -30,7 +30,7 @@ public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantVi
protected delegate void ValueViewModelMapping(IDataEditor propertyEditor, TValueViewModel variantViewModel);
protected delegate void VariantViewModelMapping(string? culture, string? segment, TVariantViewModel variantViewModel);
protected delegate void VariantViewModelMapping(string? culture, TVariantViewModel variantViewModel);
protected IEnumerable<TValueViewModel> MapValueViewModels(
IEnumerable<IProperty> properties,
@@ -81,27 +81,23 @@ public abstract class ContentMapDefinition<TContent, TValueViewModel, TVariantVi
protected IEnumerable<TVariantViewModel> MapVariantViewModels(TContent source, VariantViewModelMapping? additionalVariantMapping = null)
{
IPropertyValue[] propertyValues = source.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray();
var cultures = source.AvailableCultures.DefaultIfEmpty(null).ToArray();
// the default segment (null) must always be included in the view model - both for variant and invariant documents
var segments = propertyValues.Select(property => property.Segment).Union([null]).Distinct().ToArray();
return cultures
.SelectMany(culture => segments.Select(segment =>
.Select(culture =>
{
var variantViewModel = new TVariantViewModel
{
Culture = culture,
Segment = segment,
Name = source.GetCultureName(culture) ?? string.Empty,
CreateDate = source.CreateDate, // apparently there is no culture specific creation date
UpdateDate = culture == null
? source.UpdateDate
: source.GetUpdateDate(culture) ?? source.UpdateDate,
};
additionalVariantMapping?.Invoke(culture, segment, variantViewModel);
additionalVariantMapping?.Invoke(culture, variantViewModel);
return variantViewModel;
}))
})
.ToArray();
}
@@ -56,7 +56,7 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.State = PublishableVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
@@ -74,7 +74,7 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
target.Values = MapValueViewModels(source.Properties, published: true);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.Name = source.GetPublishName(culture) ?? documentVariantViewModel.Name;
PublishableVariantState variantState = PublishableVariantStateHelper.GetState(source, culture);
@@ -112,7 +112,7 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
target.Values = MapValueViewModels(properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.State = PublishableVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
@@ -130,7 +130,7 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.State = PublishableVariantState.Draft;
});
@@ -44,7 +44,7 @@ public class DocumentVersionMapDefinition : ContentMapDefinition<IContent, Docum
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.State = PublishableVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
@@ -42,7 +42,7 @@ public class ElementMapDefinition : ContentMapDefinition<IElement, ElementValueR
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.State = PublishableVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
@@ -40,7 +40,7 @@ public class ElementVersionMapDefinition : ContentMapDefinition<IElement, Elemen
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
(culture, documentVariantViewModel) =>
{
documentVariantViewModel.State = PublishableVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
+1 -57
View File
@@ -43141,13 +43141,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -43214,13 +43207,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -43813,13 +43799,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -43886,13 +43865,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -46253,13 +46225,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -46289,13 +46254,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -46946,13 +46904,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -46982,13 +46933,6 @@
],
"description": "Gets or sets the culture code for this variant, or `null` for invariant content."
},
"segment": {
"type": [
"null",
"string"
],
"description": "Gets or sets the segment identifier for this variant, or `null` for non-segmented content."
},
"name": {
"type": "string",
"description": "Gets or sets the name of the content for this variant."
@@ -53509,4 +53453,4 @@
"name": "oEmbed"
}
]
}
}
@@ -96,7 +96,7 @@ public abstract class BlockEditorDataConverter<TValue, TLayout>
// this method is only meant to have any effect when migrating block editor values
// from the original format to the new, variant enabled format
private static void AmendExpose(TValue value)
=> value.Expose = value.ContentData.ConvertAll(cd => new BlockItemVariation(cd.Key, null, null));
=> value.Expose = value.ContentData.ConvertAll(cd => new BlockItemVariation(cd.Key, null));
// this method is only meant to have any effect when migrating block editor values
// from the original format to the new, variant enabled format
@@ -64,4 +64,8 @@ public class BlockGridLayoutItem : BlockLayoutItemBase
/// <inheritdoc />
public override bool ReferencesSetting(Guid key)
=> SettingsKey == key || Areas.Any(area => area.ContainsSetting(key));
/// <inheritdoc />
public override IEnumerable<IBlockLayoutItem> GetContainedLayouts()
=> Areas.SelectMany(area => area.Items);
}
@@ -1,7 +1,7 @@
namespace Umbraco.Cms.Core.Models.Blocks;
/// <summary>
/// Represents a block item variation for culture and segment.
/// Represents a block item variation for culture.
/// </summary>
public class BlockItemVariation
{
@@ -17,12 +17,10 @@ public class BlockItemVariation
/// </summary>
/// <param name="contentKey">The content key.</param>
/// <param name="culture">The culture.</param>
/// <param name="segment">The segment.</param>
public BlockItemVariation(Guid contentKey, string? culture, string? segment)
public BlockItemVariation(Guid contentKey, string? culture)
{
ContentKey = contentKey;
Culture = culture;
Segment = segment;
}
/// <summary>
@@ -40,12 +38,4 @@ public class BlockItemVariation
/// The culture.
/// </value>
public string? Culture { get; set; }
/// <summary>
/// Gets or sets the segment.
/// </summary>
/// <value>
/// The segment.
/// </value>
public string? Segment { get; set; }
}
@@ -5,12 +5,18 @@ namespace Umbraco.Cms.Core.Models.Blocks;
/// </summary>
public abstract class BlockLayoutItemBase : IBlockLayoutItem
{
/// <inheritdoc />
public Guid Key { get; set; }
/// <inheritdoc />
public Guid ContentKey { get; set; }
/// <inheritdoc />
public Guid? SettingsKey { get; set; }
/// <inheritdoc />
public bool IsExternalContent { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="BlockLayoutItemBase" /> class.
/// </summary>
@@ -44,4 +50,7 @@ public abstract class BlockLayoutItemBase : IBlockLayoutItem
/// <inheritdoc />
public virtual bool ReferencesSetting(Guid key)
=> SettingsKey == key;
/// <inheritdoc />
public virtual IEnumerable<IBlockLayoutItem> GetContainedLayouts() => [];
}
@@ -8,6 +8,18 @@ namespace Umbraco.Cms.Core.Models.Blocks;
/// </summary>
public interface IBlockLayoutItem
{
/// <summary>
/// Gets or sets the layout item key.
/// </summary>
/// <value>
/// The layout item key.
/// </value>
/// <remarks>
/// Uniquely identifies a layout item. Previously the <see cref="ContentKey"/> could be used for this, but
/// with reusable elements, the same <see cref="ContentKey"/> can appear multiple times in one layout.
/// </remarks>
public Guid Key { get; set; }
/// <summary>
/// Gets or sets the content key.
/// </summary>
@@ -24,6 +36,11 @@ public interface IBlockLayoutItem
/// </value>
public Guid? SettingsKey { get; set; }
/// <summary>
/// Indicates if the content source is local or originates from the element service.
/// </summary>
public bool IsExternalContent { get; set; }
/// <summary>
/// Determines whether this layout item references the specified content key.
/// </summary>
@@ -41,4 +58,10 @@ public interface IBlockLayoutItem
/// <c>true</c> if this layout item references the specified settings key; otherwise, <c>false</c>.
/// </returns>
public bool ReferencesSetting(Guid key) => SettingsKey == key;
/// <summary>
/// Returns any nested layouts for this layout (e.g. area layouts for the Block Grid).
/// </summary>
/// <returns>The nested layouts.</returns>
public IEnumerable<IBlockLayoutItem> GetContainedLayouts();
}
@@ -10,11 +10,6 @@ public class VariantModel
/// </summary>
public string? Culture { get; set; }
/// <summary>
/// Gets or sets the segment identifier for this variant, or <c>null</c> for non-segmented content.
/// </summary>
public string? Segment { get; set; }
/// <summary>
/// Gets or sets the name of the content for this variant.
/// </summary>
@@ -5,18 +5,13 @@ namespace Umbraco.Cms.Core.Models.ContentEditing;
/// <summary>
/// Represents the base model for content variants with culture and segment support.
/// </summary>
public abstract class VariantModelBase : IHasCultureAndSegment
public abstract class VariantModelBase
{
/// <summary>
/// Gets or sets the culture code for this variant, or <c>null</c> for invariant content.
/// </summary>
public string? Culture { get; set; }
/// <summary>
/// Gets or sets the segment identifier for this variant, or <c>null</c> for non-segmented content.
/// </summary>
public string? Segment { get; set; }
/// <summary>
/// Gets or sets the name of the content for this variant.
/// </summary>
@@ -20,18 +20,13 @@ public sealed class PropertyValidationContext
/// </summary>
public required IEnumerable<string> CulturesBeingValidated { get; init; }
/// <summary>
/// Gets the collection of segments being validated.
/// </summary>
public required IEnumerable<string?> SegmentsBeingValidated { get; init; }
/// <summary>
/// Creates an empty property validation context with no culture or segment.
/// </summary>
/// <returns>An empty property validation context.</returns>
public static PropertyValidationContext Empty() => new()
{
Culture = null, Segment = null, CulturesBeingValidated = [], SegmentsBeingValidated = []
Culture = null, Segment = null, CulturesBeingValidated = [],
};
/// <summary>
@@ -42,6 +37,6 @@ public sealed class PropertyValidationContext
/// <returns>A property validation context for the specified culture and segment.</returns>
public static PropertyValidationContext CultureAndSegment(string? culture, string? segment) => new()
{
Culture = culture, Segment = segment, CulturesBeingValidated = [], SegmentsBeingValidated = []
Culture = culture, Segment = segment, CulturesBeingValidated = [],
};
}
@@ -1,12 +1,11 @@
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a property index value factory specifically for block-based property values.
/// Represents a property index value factory specifically for block grid properties.
/// </summary>
/// <remarks>
/// This marker interface allows for specialized indexing of block content,
/// such as Block List, Block Grid, and Rich Text block values.
/// This marker interface allows for specialized indexing of block grid content.
/// </remarks>
public interface IBlockValuePropertyIndexValueFactory : IPropertyIndexValueFactory
public interface IBlockGridPropertyIndexValueFactory : IPropertyIndexValueFactory
{
}
@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a property index value factory specifically for block list properties.
/// </summary>
/// <remarks>
/// This marker interface allows for specialized indexing of block list content.
/// </remarks>
public interface IBlockListPropertyIndexValueFactory : IPropertyIndexValueFactory
{
}
@@ -0,0 +1,11 @@
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a property index value factory specifically for single block properties.
/// </summary>
/// <remarks>
/// This marker interface allows for specialized indexing of single block content.
/// </remarks>
public interface ISingleBlockPropertyIndexValueFactory : IPropertyIndexValueFactory
{
}
@@ -3,7 +3,17 @@ using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Cms.Core.PublishedCache;
/// <summary>
/// A service for converting <see cref="BlockItemData"/> into <see cref="IPublishedElement"/>.
/// </summary>
public interface IBlockElementService
{
Task<IPublishedElement?> BuildElementAsync(BlockItemData blockItemData, bool? preview = null);
/// <summary>
/// Creates an <see cref="IPublishedElement"/> instance from <see cref="BlockItemData"/>.
/// </summary>
/// <param name="owner">The <see cref="IPublishedElement"/> that contains the block property which is the origin to the <see cref="BlockItemData"/>.</param>
/// <param name="blockItemData">The <see cref="BlockItemData"/> containing the data to convert into an <see cref="IPublishedElement"/>.</param>
/// <param name="preview">Whether to perform the conversion for preview.</param>
/// <returns>The created <see cref="IPublishedElement"/>, or null if an element could not be created from the <see cref="BlockItemData"/>.</returns>
Task<IPublishedElement?> BuildElementAsync(IPublishedElement owner, BlockItemData blockItemData, bool? preview = null);
}
@@ -523,7 +523,7 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
return null;
}
if (contentType.VariesByNothing() && contentEditingModelBase.Variants.Any(v => v.Culture is null && v.Segment is null) is false)
if (contentType.VariesByNothing() && contentEditingModelBase.Variants.Any(v => v.Culture is null) is false)
{
// does not vary by anything and is missing the invariant name = invalid
operationStatus = ContentEditingOperationStatus.ContentTypeCultureVarianceMismatch;
@@ -537,13 +537,6 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
return null;
}
if (contentType.VariesBySegment() && contentEditingModelBase.Variants.Any(v => v.Segment is null) is false)
{
// varies by segment with no default segment variants = invalid
operationStatus = ContentEditingOperationStatus.ContentTypeSegmentVarianceMismatch;
return null;
}
var propertyTypesByAlias = contentType.CompositionPropertyTypes.ToDictionary(pt => pt.Alias);
var propertyValuesAndVariance = contentEditingModelBase
.Properties
@@ -669,7 +662,6 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
// as each culture can have several segments. we'll prioritize the segment-less names
var variantNamesByCulture = contentEditingModelBase.Variants
.Where(v => v.Culture.IsNullOrWhiteSpace() == false)
.OrderBy(v => v.Segment.IsNullOrWhiteSpace() ? 0 : 1)
.GroupBy(v => v.Culture!)
.ToDictionary(g => g.Key, g => g.First().Name);
@@ -679,16 +671,10 @@ internal abstract class ContentEditingServiceBase<TContent, TContentType, TConte
content.SetCultureName(name, culture);
}
}
else if (contentType.VariesBySegment())
{
// this should be validated already so it's OK to throw an exception here
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Segment is null)?.Name
?? throw new ArgumentException("Could not find the default segment variant", nameof(contentEditingModelBase));
}
else
{
// this should be validated already so it's OK to throw an exception here
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name
content.Name = contentEditingModelBase.Variants.FirstOrDefault(v => v.Culture is null)?.Name
?? throw new ArgumentException("Could not find a culture invariant variant", nameof(contentEditingModelBase));
}
}
@@ -255,8 +255,7 @@ internal abstract class ContentPublishingServiceBase<TContent, TContentService>
Variants = cultures.Select(culture => new VariantModel()
{
Name = content.GetPublishName(culture) ?? string.Empty,
Culture = culture,
Segment = null
Culture = culture
}).ToArray()
};
@@ -66,20 +66,12 @@ internal abstract class ContentValidationServiceBase<TContentType>
cultures = await GetCultureCodes();
}
// We don't have managed segments, so we have to make do with the ones passed in the model.
var segments =
new string?[] { null }
.Union(contentEditingModelBase.Variants
.Where(variant => variant.Culture is null || cultures.Contains(variant.Culture))
.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment)
.WhereNotNull())
.ToArray();
foreach (IPropertyType propertyType in invariantPropertyTypes)
{
var validationContext = new PropertyValidationContext
{
Culture = null, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
Culture = null, Segment = null, CulturesBeingValidated = cultures
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
@@ -94,7 +86,7 @@ internal abstract class ContentValidationServiceBase<TContentType>
{
var validationContext = new PropertyValidationContext
{
Culture = culture, Segment = null, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
Culture = culture, Segment = null, CulturesBeingValidated = cultures
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
@@ -106,51 +98,46 @@ internal abstract class ContentValidationServiceBase<TContentType>
foreach (IPropertyType propertyType in segmentVariantPropertyTypes)
{
foreach (var segment in segments)
PropertyValueModel[] propertyValuesToValidate = contentEditingModelBase
.Properties
.Where(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture is null)
.ToArray();
var segmentsToValidate = propertyValuesToValidate.Select(pv => pv.Segment).Union([null]).Distinct().ToArray();
foreach (var segment in segmentsToValidate)
{
var validationContext = new PropertyValidationContext
{
Culture = null, Segment = segment, CulturesBeingValidated = cultures, SegmentsBeingValidated = segments
Culture = null, Segment = segment, CulturesBeingValidated = cultures
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture is null && propertyValue.Segment.InvariantEquals(segment));
PropertyValueModel? propertyValueModel = propertyValuesToValidate.FirstOrDefault(pv => pv.Segment.InvariantEquals(segment));
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
if (cultureAndSegmentVariantPropertyTypes.Length > 0)
{
// Get a mapping of segments to their associated cultures based on the variants and properties provided in the model.
// Without managed segments again we need to rely on the model data.
Dictionary<string, HashSet<string>> segmentCultures = GetPopulatedSegmentCultures(contentEditingModelBase, cultures);
foreach (IPropertyType propertyType in cultureAndSegmentVariantPropertyTypes)
{
foreach (var culture in cultures)
{
foreach (var segment in segments.DefaultIfEmpty(null))
{
// Skip validation if the segment has cultures defined and the current culture is not included.
if (segment is not null &&
segmentCultures.TryGetValue(segment, out HashSet<string>? associatedCultures) &&
associatedCultures.Contains(culture) is false)
{
continue;
}
PropertyValueModel[] propertyValuesToValidate = contentEditingModelBase
.Properties
.Where(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture))
.ToArray();
var segmentsToValidate = propertyValuesToValidate.Select(pv => pv.Segment).Union([null]).Distinct().ToArray();
foreach (var segment in segmentsToValidate)
{
var validationContext = new PropertyValidationContext
{
Culture = culture,
Segment = segment,
CulturesBeingValidated = cultures,
SegmentsBeingValidated = segments,
};
PropertyValueModel? propertyValueModel = contentEditingModelBase
.Properties
.FirstOrDefault(propertyValue => propertyValue.Alias == propertyType.Alias && propertyValue.Culture.InvariantEquals(culture) && propertyValue.Segment.InvariantEquals(segment));
PropertyValueModel? propertyValueModel = propertyValuesToValidate.FirstOrDefault(pv => pv.Segment.InvariantEquals(segment));
validationErrors.AddRange(ValidateProperty(propertyType, propertyValueModel, validationContext));
}
}
@@ -178,31 +165,6 @@ internal abstract class ContentValidationServiceBase<TContentType>
private async Task<string[]> GetCultureCodes() => (await _languageService.GetAllIsoCodesAsync()).ToArray();
/// <summary>
/// Gets a dictionary of segments along with the cultures they are associated with.
/// </summary>
/// <param name="contentEditingModel">The content editing model.</param>
/// <param name="cultures">The cultures to consider when finding associated cultures for each segment.</param>
/// <returns>
/// A dictionary where the key is a unique segment from <see cref="ContentEditingModelBase.Variants"/> and the value is
/// the set of cultures that have at least one property defined for that segment in <see cref="ContentEditingModelBase.Properties"/>.
/// </returns>
/// <remarks>
/// Internal to support unit testing.
/// </remarks>
internal static Dictionary<string, HashSet<string>> GetPopulatedSegmentCultures(ContentEditingModelBase contentEditingModel, string[] cultures)
{
IEnumerable<string> uniqueSegments = contentEditingModel.Variants.Select(variant => variant.Segment).WhereNotNull().Distinct();
return uniqueSegments.ToDictionary(
segment => segment,
segment => contentEditingModel.Properties
.Where(property => property.Segment.InvariantEquals(segment))
.Where(property => property.Culture is not null && cultures.Contains(property.Culture))
.Select(property => property.Culture!)
.ToHashSet());
}
private IEnumerable<PropertyValidationError> ValidateProperty(IPropertyType propertyType, PropertyValueModel? propertyValueModel, PropertyValidationContext validationContext)
{
ValidationResult[] validationResults = _propertyValidationService
@@ -25,11 +25,6 @@ public enum ContentEditingOperationStatus
/// </summary>
ContentTypeCultureVarianceMismatch,
/// <summary>
/// The content's segment variance does not match the content type's segment variance setting.
/// </summary>
ContentTypeSegmentVarianceMismatch,
/// <summary>
/// The specified content item was not found.
/// </summary>
@@ -193,8 +193,7 @@ public class PropertyValidationService : IPropertyValidationService
{
Culture = null,
Segment = null,
CulturesBeingValidated = [impact.Culture!],
SegmentsBeingValidated = []
CulturesBeingValidated = [impact.Culture!]
});
}
@@ -218,8 +217,7 @@ public class PropertyValidationService : IPropertyValidationService
{
Culture = validationContext.Culture?.NullOrWhiteSpaceAsNull(),
Segment = validationContext.Segment?.NullOrWhiteSpaceAsNull(),
CulturesBeingValidated = validationContext.CulturesBeingValidated,
SegmentsBeingValidated = validationContext.SegmentsBeingValidated
CulturesBeingValidated = validationContext.CulturesBeingValidated
};
var culture = validationContext.Culture;
@@ -285,7 +285,9 @@ public static partial class UmbracoBuilderExtensions
/// <returns>The same <see cref="Umbraco.Cms.Core.DependencyInjection.IUmbracoBuilder"/> instance so that multiple calls can be chained.</returns>
public static IUmbracoBuilder AddPropertyIndexValueFactories(this IUmbracoBuilder builder)
{
builder.Services.AddSingleton<IBlockValuePropertyIndexValueFactory, BlockValuePropertyIndexValueFactory>();
builder.Services.AddSingleton<IBlockListPropertyIndexValueFactory, BlockListPropertyIndexValueFactory>();
builder.Services.AddSingleton<IBlockGridPropertyIndexValueFactory, BlockGridPropertyIndexValueFactory>();
builder.Services.AddSingleton<ISingleBlockPropertyIndexValueFactory, SingleBlockPropertyIndexValueFactory>();
builder.Services.AddSingleton<ITagPropertyIndexValueFactory, TagPropertyIndexValueFactory>();
builder.Services.AddSingleton<IRichTextPropertyIndexValueFactory, RichTextPropertyIndexValueFactory>();
builder.Services.AddSingleton<IDateOnlyPropertyIndexValueFactory, DateOnlyPropertyIndexValueFactory>();
@@ -76,7 +76,7 @@ public class MigrateSingleBlockList : AsyncMigrationBase
SingleBlockListConfigurationCache blockListConfigurationCache,
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
ISingleBlockPropertyIndexValueFactory blockValuePropertyIndexValueFactory,
IBlockEditorElementTypeCache elementTypeCache,
AppCaches appCaches)
: base(context)
@@ -110,10 +110,10 @@ public abstract class BlockEditorPropertyNotificationHandlerBase<TBlockLayoutIte
private void TraverseObject(JsonObject obj)
{
// we'll assume that the object is a data representation of a block based editor if it contains "contentData" and "settingsData".
if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData)
// we'll assume that the object is a data representation of a block based editor if it contains "contentData", "settingsData" and "layout".
if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData && obj["layout"] is JsonObject layoutData)
{
ParseKeys(contentData, settingsData);
ParseKeys(contentData, settingsData, layoutData);
return;
}
@@ -123,12 +123,46 @@ public abstract class BlockEditorPropertyNotificationHandlerBase<TBlockLayoutIte
}
}
private void ParseKeys(JsonArray contentData, JsonArray settingsData)
private void ParseKeys(JsonArray contentData, JsonArray settingsData, JsonObject layoutData)
{
// grab all keys from the objects of contentData and settingsData
var keys = contentData.Select(c => c?["key"])
.Union(settingsData.Select(s => s?["key"]))
.Select(keyToken => keyToken?.GetValue<string>().NullOrWhiteSpaceAsNull())
// recurse a JSON object to find all contained block editor layouts
List<JsonObject> GetLayoutItemsRecursively(JsonObject jsonObject)
{
var layoutItems = new List<JsonObject>();
if (jsonObject.ContainsKey("key") && jsonObject.ContainsKey("contentKey"))
{
// assume it's a layout if it has "key" and "contentKey"
layoutItems.Add(jsonObject);
}
foreach (JsonNode property in jsonObject.Select(v => v.Value).WhereNotNull())
{
IEnumerable<JsonObject> childrenToRecurse = property is JsonObject jsonObjectChild
? [jsonObjectChild]
: property is JsonArray jsonArrayChild
? jsonArrayChild.OfType<JsonObject>()
: [];
layoutItems.AddRange(childrenToRecurse.SelectMany(GetLayoutItemsRecursively));
}
return layoutItems;
}
// grab keys applicable for replacement from all the layouts - that is:
// - the key of the layout itself ("key").
// - the key of the content item ("contentKey").
// - ONLY for local content; do NOT replace content item keys for shared content.
// - the key of the settings item ("settingsKey") if present.
List<JsonObject> layoutItems = GetLayoutItemsRecursively(layoutData);
var keys = layoutItems.SelectMany(layoutItem => new[]
{
layoutItem["key"]?.GetValue<string>(),
layoutItem["isExternalContent"]?.GetValue<bool>() is not true
? layoutItem["contentKey"]?.GetValue<string>()
: null,
layoutItem["settingsKey"]?.GetValue<string>(),
})
.WhereNotNull()
.ToArray();
// the following is solely for avoiding functionality wise breakage. we should consider removing it eventually, but for the time being it's harmless.
@@ -127,7 +127,7 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
}
private static bool IsBlockEditorDataEmpty([NotNullWhen(false)] BlockEditorData<TValue, TLayout>? editorData)
=> editorData is null || editorData.BlockValue.ContentData.Count == 0;
=> editorData is null || editorData.BlockValue.Layout.Count == 0;
// We don't throw on error here because we want to be able to parse what we can, even if some of the data is invalid. In cases where migrating
// from nested content to blocks, we don't want to trigger a fatal error for retrieving references, as this isn't vital to the operation.
@@ -28,6 +28,18 @@ public abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorV
var validationContextCulture = isWildcardCulture ? null : validationContext.Culture.NullOrWhiteSpaceAsNull();
elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, validationContextCulture, validationContext.Segment));
// We don't have managed segments, so we will simply trust the ones present in the model.
var segmentsByCulture = blockEditorData
.BlockValue.ContentData.SelectMany(cd => cd.Values)
.Union(blockEditorData.BlockValue.SettingsData.SelectMany(cd => cd.Values))
.GroupBy(v => v.Culture)
.Select(g => new
{
Culture = g.Key,
Segments = g.Select(v => v.Segment).WhereNotNull().Distinct().Union([null]).ToArray(),
})
.ToArray();
if (validationContextCulture is null)
{
// make sure we extend validation to variant block value (element level variation)
@@ -36,7 +48,8 @@ public abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorV
: validationContext.CulturesBeingValidated;
foreach (var culture in validationContextCulturesBeingValidated)
{
foreach (var segment in validationContext.SegmentsBeingValidated.DefaultIfEmpty(null))
var segmentsToValidate = segmentsByCulture.FirstOrDefault(s => s.Culture.InvariantEquals(culture))?.Segments ?? [];
foreach (var segment in segmentsToValidate.DefaultIfEmpty(null))
{
elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, culture, segment));
}
@@ -45,7 +58,8 @@ public abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorV
else
{
// make sure we extend validation to invariant block values (no element level variation)
foreach (var segment in validationContext.SegmentsBeingValidated.DefaultIfEmpty(null))
var segmentsToValidate = segmentsByCulture.SelectMany(s => s.Segments).Distinct().ToArray();
foreach (var segment in segmentsToValidate.DefaultIfEmpty(null))
{
elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, null, segment));
}
@@ -124,7 +138,12 @@ public abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorV
if (segment != "*")
{
if (propertyType.VariesBySegment() != (segment is not null) || blockPropertyValue.Segment.InvariantEquals(segment) is false)
if (propertyType.VariesBySegment() is false && segment is not null)
{
continue;
}
if (propertyType.VariesBySegment() && blockPropertyValue.Segment.InvariantEquals(segment) is false)
{
continue;
}
@@ -63,13 +63,20 @@ public class BlockEditorValues<TValue, TLayout>
private BlockEditorData<TValue, TLayout>? Clean(BlockEditorData<TValue, TLayout> blockEditorData)
{
if (blockEditorData.BlockValue.ContentData.Count == 0)
if (blockEditorData.BlockValue.Layout.Count == 0)
{
// if there's no content ensure there's no settings too
blockEditorData.BlockValue.SettingsData.Clear();
return null;
}
if (blockEditorData.BlockValue.ContentData.Count == 0
&& blockEditorData.BlockValue.SettingsData.Count == 0)
{
// no local content or settings; the block editor must contain only global elements
return blockEditorData;
}
var contentTypePropertyTypes = new Dictionary<string, Dictionary<string, IPropertyType>>();
// filter out any content that isn't referenced in the layout references
@@ -26,7 +26,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase
public BlockGridPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
IBlockGridPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory)
=> _ioHelper = ioHelper;
@@ -25,9 +25,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public abstract class BlockGridPropertyEditorBase : DataEditor, IValueSchemaProvider
{
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly IBlockGridPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockGridPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
: base(dataValueEditorFactory)
{
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
@@ -0,0 +1,23 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors;
internal sealed class BlockGridPropertyIndexValueFactory
: BlockValuePropertyIndexValueFactoryBase<BlockGridValue>, IBlockGridPropertyIndexValueFactory
{
public BlockGridPropertyIndexValueFactory(
PropertyEditorCollection propertyEditorCollection,
IElementService elementService,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
{
}
protected override IEnumerable<RawDataItem> GetDataItems(BlockGridValue input, bool published)
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
}
@@ -25,7 +25,7 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
public BlockListPropertyEditor(
IDataValueEditorFactory dataValueEditorFactory,
IIOHelper ioHelper,
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory,
IBlockListPropertyIndexValueFactory blockValuePropertyIndexValueFactory,
IJsonSerializer jsonSerializer)
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory, jsonSerializer)
=> _ioHelper = ioHelper;
@@ -21,13 +21,13 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public abstract class BlockListPropertyEditorBase : DataEditor, IValueSchemaProvider
{
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly IBlockListPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly IJsonSerializer _jsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyEditorBase"/> class.
/// </summary>
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
protected BlockListPropertyEditorBase(IDataValueEditorFactory dataValueEditorFactory, IBlockListPropertyIndexValueFactory blockValuePropertyIndexValueFactory, IJsonSerializer jsonSerializer)
: base(dataValueEditorFactory)
{
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
@@ -0,0 +1,26 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors;
internal sealed class BlockListPropertyIndexValueFactory
: BlockValuePropertyIndexValueFactoryBase<BlockListValue>, IBlockListPropertyIndexValueFactory
{
public BlockListPropertyIndexValueFactory(
PropertyEditorCollection propertyEditorCollection,
IElementService elementService,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
{
}
protected override IEnumerable<RawDataItem> GetDataItems(BlockListValue input, bool published)
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
}
@@ -1,45 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Core.PropertyEditors;
internal sealed class BlockValuePropertyIndexValueFactory :
BlockValuePropertyIndexValueFactoryBase<BlockValuePropertyIndexValueFactory.IndexValueFactoryBlockValue>,
IBlockValuePropertyIndexValueFactory
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockValuePropertyIndexValueFactory"/> class.
/// </summary>
/// <param name="propertyEditorCollection">The <see cref="PropertyEditorCollection"/> containing available property editors.</param>
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for serializing and deserializing JSON values.</param>
/// <param name="indexingSettings">The <see cref="IOptionsMonitor{IndexingSettings}"/> providing access to indexing configuration settings.</param>
public BlockValuePropertyIndexValueFactory(
PropertyEditorCollection propertyEditorCollection,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(propertyEditorCollection, jsonSerializer, indexingSettings)
{
}
protected override IEnumerable<RawDataItem> GetDataItems(IndexValueFactoryBlockValue input, bool published)
=> GetDataItems(input.ContentData, input.Expose, published);
// we only care about the content data when extracting values for indexing - not the layouts nor the settings
internal sealed class IndexValueFactoryBlockValue
{
/// <summary>
/// Gets or sets the list of content block item data.
/// </summary>
public List<BlockItemData> ContentData { get; set; } = new();
/// <summary>
/// Gets or sets the collection of <see cref="BlockItemVariation"/> instances that should be exposed by the index value factory.
/// </summary>
public List<BlockItemVariation> Expose { get; set; } = new();
}
}
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
@@ -12,14 +13,17 @@ namespace Umbraco.Cms.Core.PropertyEditors;
internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : JsonPropertyIndexValueFactoryBase<TSerialized>
{
private readonly PropertyEditorCollection _propertyEditorCollection;
private readonly IElementService _elementService;
protected BlockValuePropertyIndexValueFactoryBase(
PropertyEditorCollection propertyEditorCollection,
IElementService elementService,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(jsonSerializer, indexingSettings)
{
_propertyEditorCollection = propertyEditorCollection;
_elementService = elementService;
}
protected override IEnumerable<IndexValue> Handle(
@@ -106,37 +110,74 @@ internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : J
/// <summary>
/// Unwraps block item data as data items.
/// </summary>
protected IEnumerable<RawDataItem> GetDataItems(IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
protected IEnumerable<RawDataItem> GetDataItems(IEnumerable<IBlockLayoutItem> layouts, IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
{
List<RawDataItem> indexData;
if (published is false)
{
return contentData.Select(ToRawData);
indexData = contentData.Select(ToRawData).ToList();
}
else
{
indexData = new();
foreach (BlockItemData blockItemData in contentData)
{
var exposedCultures = expose
.Where(e => e.ContentKey == blockItemData.Key)
.Select(e => e.Culture)
.ToArray();
if (exposedCultures.Any() is false)
{
continue;
}
if (exposedCultures.Contains(null)
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
{
indexData.Add(ToRawData(blockItemData));
continue;
}
indexData.Add(
ToRawData(
blockItemData.ContentTypeKey,
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))));
}
}
var indexData = new List<RawDataItem>();
foreach (BlockItemData blockItemData in contentData)
IBlockLayoutItem[] layoutsAsArray = layouts as IBlockLayoutItem[] ?? layouts.ToArray();
// Get the shared element keys from all layouts.
// NOTE: While the Grid areas are modeled to contain areas within areas, in reality it cannot be configured as
// such, so this "top-level aggregation" of shared content keys works in effect.
Guid[] sharedElementKeys = layoutsAsArray
.Union(layoutsAsArray.SelectMany(l => l.GetContainedLayouts()))
.Where(l => l.IsExternalContent)
.Select(l => l.ContentKey)
.ToArray();
if (sharedElementKeys.Length > 0)
{
var exposedCultures = expose
.Where(e => e.ContentKey == blockItemData.Key)
.Select(e => e.Culture)
.ToArray();
if (exposedCultures.Any() is false)
{
continue;
}
if (exposedCultures.Contains(null)
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
{
indexData.Add(ToRawData(blockItemData));
continue;
}
indexData.Add(
ToRawData(
blockItemData.ContentTypeKey,
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))));
IEnumerable<IElement> elements = _elementService.GetByIds(sharedElementKeys);
indexData.AddRange(
elements.Select(element => new RawDataItem
{
ContentTypeKey = element.ContentType.Key,
Properties = element
.Properties
.SelectMany(property => property
.Values
.Select(value => new RawPropertyData
{
Alias = property.Alias,
Culture = value.Culture,
Value = published
? value.PublishedValue
: value.EditedValue,
}))
.ToArray(),
}));
}
return indexData;
@@ -287,11 +287,29 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment)
{
EnsureLayoutItemKeys(blockValue);
MapBlockItemDataToEditor(property, blockValue.ContentData, culture, segment);
MapBlockItemDataToEditor(property, blockValue.SettingsData, culture, segment);
_blockEditorVarianceHandler.AlignExposeVariance(blockValue);
}
// Ensures that all layout items have a key (for backwards data format compatibility).
private static void EnsureLayoutItemKeys(TValue blockValue)
{
if (!blockValue.Layout.TryGetValue(blockValue.PropertyEditorAlias, out IEnumerable<IBlockLayoutItem>? layout))
{
return;
}
// All layout items with an empty key will be assigned the content key of the layout item.
// This ensures data consistency across multiple sessions.
foreach (IBlockLayoutItem layoutItem in layout.Where(layoutItem => layoutItem.Key == Guid.Empty))
{
layoutItem.Key = layoutItem.ContentKey;
}
}
protected IEnumerable<Guid> ConfiguredElementTypeKeys(IBlockConfiguration configuration)
{
yield return configuration.ContentElementTypeKey;
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
@@ -19,14 +20,16 @@ internal sealed class RichTextPropertyIndexValueFactory : BlockValuePropertyInde
/// </summary>
/// <param name="propertyEditorCollection">A collection containing all available property editors.</param>
/// <param name="jsonSerializer">The serializer used for handling JSON data.</param>
/// <param name="elementService">Service for accessing elements.</param>
/// <param name="indexingSettings">The monitor providing current indexing settings.</param>
/// <param name="logger">The logger used for logging diagnostic information.</param>
public RichTextPropertyIndexValueFactory(
PropertyEditorCollection propertyEditorCollection,
IElementService elementService,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings,
ILogger<RichTextPropertyIndexValueFactory> logger)
: base(propertyEditorCollection, jsonSerializer, indexingSettings)
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
{
_jsonSerializer = jsonSerializer;
_logger = logger;
@@ -156,7 +159,7 @@ internal sealed class RichTextPropertyIndexValueFactory : BlockValuePropertyInde
}
protected override IEnumerable<RawDataItem> GetDataItems(RichTextEditorValue input, bool published)
=> GetDataItems(input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
=> GetDataItems(input.Blocks?.GetLayouts() ?? [], input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
/// <summary>
/// Strips HTML tags from content, replacing them with spaces to preserve word boundaries for indexing.
@@ -27,7 +27,7 @@ public class SingleBlockPropertyEditor : DataEditor
{
private readonly IJsonSerializer _jsonSerializer;
private readonly IIOHelper _ioHelper;
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
private readonly ISingleBlockPropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
/// <summary>
/// Initializes a new instance of the <see cref="SingleBlockPropertyEditor"/> class.
@@ -40,7 +40,7 @@ public class SingleBlockPropertyEditor : DataEditor
IDataValueEditorFactory dataValueEditorFactory,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
ISingleBlockPropertyIndexValueFactory blockValuePropertyIndexValueFactory)
: base(dataValueEditorFactory)
{
_jsonSerializer = jsonSerializer;
@@ -0,0 +1,26 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Core.PropertyEditors;
internal sealed class SingleBlockPropertyIndexValueFactory
: BlockValuePropertyIndexValueFactoryBase<SingleBlockValue>, ISingleBlockPropertyIndexValueFactory
{
public SingleBlockPropertyIndexValueFactory(
PropertyEditorCollection propertyEditorCollection,
IElementService elementService,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(propertyEditorCollection, elementService, jsonSerializer, indexingSettings)
{
}
protected override IEnumerable<RawDataItem> GetDataItems(SingleBlockValue input, bool published)
=> GetDataItems(input.GetLayouts() ?? [], input.ContentData, input.Expose, published);
}
@@ -94,7 +94,7 @@ public sealed class BlockEditorConverter
Key = data.Key,
};
return _blockElementService.BuildElementAsync(alignedData, preview).GetAwaiter().GetResult();
return _blockElementService.BuildElementAsync(owner, alignedData, preview).GetAwaiter().GetResult();
}
/// <summary>
@@ -145,7 +145,7 @@ public sealed class BlockEditorVarianceHandler
if (exposeVariation.VariesByCulture() && blockVariations.All(v => v.Culture is null))
{
var defaultCulture = await _languageService.GetDefaultIsoCodeAsync();
return blockVariations.Select(v => new BlockItemVariation(v.ContentKey, defaultCulture, v.Segment));
return blockVariations.Select(v => new BlockItemVariation(v.ContentKey, defaultCulture));
}
if (exposeVariation.VariesByCulture() is false && blockVariations.All(v => v.Culture is not null))
@@ -153,7 +153,7 @@ public sealed class BlockEditorVarianceHandler
var defaultCulture = await _languageService.GetDefaultIsoCodeAsync();
return blockVariations
.Where(v => v.Culture == defaultCulture)
.Select(v => new BlockItemVariation(v.ContentKey, null, v.Segment))
.Select(v => new BlockItemVariation(v.ContentKey, null))
.ToList();
}
@@ -220,14 +220,14 @@ public sealed class BlockEditorVarianceHandler
var omitNullCulture = contentData.Values.Any(v => v.Culture is not null);
foreach (BlockPropertyValue value in contentData.Values
.Where(v => omitNullCulture is false || v.Culture is not null)
.DistinctBy(v => v.Culture + v.Segment))
.DistinctBy(v => v.Culture))
{
blockValue.Expose.Add(new BlockItemVariation(contentData.Key, value.Culture, value.Segment));
blockValue.Expose.Add(new BlockItemVariation(contentData.Key, value.Culture));
}
}
}
blockValue.Expose = blockValue.Expose.DistinctBy(e => $"{e.ContentKey}.{e.Culture}.{e.Segment}").ToList();
blockValue.Expose = blockValue.Expose.DistinctBy(e => $"{e.ContentKey}.{e.Culture}").ToList();
}
private static bool VariesByCulture(BlockPropertyValue blockPropertyValue)
@@ -91,8 +91,7 @@ internal static class BlockExposeFallbackHelper
string? segment)
=> expose.Any(v =>
v.ContentKey == elementKey &&
v.Culture.InvariantEquals(culture) &&
v.Segment == segment);
v.Culture.InvariantEquals(culture));
/// <summary>
/// Walks the language fallback chain and returns the culture that the block is exposed for,
@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -30,10 +31,21 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
private readonly ILanguageService _languageService;
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
private readonly IElementCacheService _elementCacheService;
/// <summary>
/// Initializes a new instance of the <see cref="BlockGridPropertyValueConverter"/> class.
/// </summary>
/// <param name="proflog">The logger used for profiling and diagnostics.</param>
/// <param name="blockConverter">The converter responsible for handling block editor values.</param>
/// <param name="jsonSerializer">The serializer used for JSON serialization and deserialization.</param>
/// <param name="apiElementBuilder">The builder for creating API elements from block data.</param>
/// <param name="constructorCache">The cache for block grid property value constructors.</param>
/// <param name="variationContextAccessor">Provides access to the current variation context.</param>
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
/// <param name="languageService">Service for accessing all languages.</param>
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
/// <param name="elementCacheService">The cache for elements.</param>
public BlockGridPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
@@ -43,7 +55,8 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
IElementCacheService elementCacheService)
{
_proflog = proflog;
_blockConverter = blockConverter;
@@ -54,10 +67,11 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
_blockEditorVarianceHandler = blockEditorVarianceHandler;
_languageService = languageService;
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
_elementCacheService = elementCacheService;
}
/// <inheritdoc cref="BlockGridPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IJsonSerializer, IApiElementBuilder, BlockGridPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 19.")]
[Obsolete("Please use the constructor with all parameters. Scheduled for removal in Umbraco 20.")]
public BlockGridPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
@@ -70,6 +84,31 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
{
}
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
public BlockGridPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
IJsonSerializer jsonSerializer,
IApiElementBuilder apiElementBuilder,
BlockGridPropertyValueConstructorCache constructorCache,
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
: this(
proflog,
blockConverter,
jsonSerializer,
apiElementBuilder,
constructorCache,
variationContextAccessor,
blockEditorVarianceHandler,
languageService,
propertyRenderingContextAccessor,
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
{
}
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockGrid);
@@ -80,7 +119,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
=> PropertyCacheLevel.Elements;
/// <inheritdoc />
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
@@ -155,7 +194,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
return null;
}
var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns).GetAwaiter().GetResult();
}
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
@@ -16,19 +17,22 @@ internal sealed class BlockGridPropertyValueCreator : BlockPropertyValueCreatorB
/// </summary>
/// <param name="blockEditorConverter">The service used to convert block editor data into strongly typed objects.</param>
/// <param name="variationContextAccessor">Provides access to the current variation context for content.</param>
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors, such as culture or segment variations.</param>
/// <param name="jsonSerializer">The serializer used to handle JSON data for block grid properties.</param>
/// <param name="constructorCache">A cache for constructors used when creating block grid property values, improving performance.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
/// <param name="elementCacheService">The cache for elements.</param>
public BlockGridPropertyValueCreator(
BlockEditorConverter blockEditorConverter,
IVariationContextAccessor variationContextAccessor,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
IElementCacheService elementCacheService,
IJsonSerializer jsonSerializer,
BlockGridPropertyValueConstructorCache constructorCache,
ILanguageService languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
{
_jsonSerializer = jsonSerializer;
_constructorCache = constructorCache;
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Extensions;
@@ -34,6 +35,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
private readonly ILanguageService _languageService;
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
private readonly IElementCacheService _elementCacheService;
/// <summary>
/// Initializes a new instance of the <see cref="BlockListPropertyValueConverter"/> class.
@@ -48,6 +50,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
/// <param name="blockEditorVarianceHandler">Handles variance for block editors.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
/// <param name="elementCacheService">The cache for elements.</param>
public BlockListPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
@@ -58,7 +61,8 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
IElementCacheService elementCacheService)
{
_proflog = proflog;
_blockConverter = blockConverter;
@@ -70,6 +74,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
_blockEditorVarianceHandler = blockEditorVarianceHandler;
_languageService = languageService;
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
_elementCacheService = elementCacheService;
}
/// <inheritdoc cref="BlockListPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IContentTypeService, IApiElementBuilder, IJsonSerializer, BlockListPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
@@ -87,6 +92,33 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
{
}
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
public BlockListPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
IContentTypeService contentTypeService,
IApiElementBuilder apiElementBuilder,
IJsonSerializer jsonSerializer,
BlockListPropertyValueConstructorCache constructorCache,
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
: this(
proflog,
blockConverter,
contentTypeService,
apiElementBuilder,
jsonSerializer,
constructorCache,
variationContextAccessor,
blockEditorVarianceHandler,
languageService,
propertyRenderingContextAccessor,
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
{
}
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
@@ -128,7 +160,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
=> PropertyCacheLevel.Elements;
/// <inheritdoc />
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
@@ -196,7 +228,7 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli
return null;
}
var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks).GetAwaiter().GetResult();
}
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -15,19 +16,22 @@ internal sealed class BlockListPropertyValueCreator : BlockPropertyValueCreatorB
/// </summary>
/// <param name="blockEditorConverter">The service used to convert block editor data into strongly typed objects.</param>
/// <param name="variationContextAccessor">Provides access to the current variation context, used for handling content variations.</param>
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors, determining how values vary by culture or segment.</param>
/// <param name="jsonSerializer">The serializer used for serializing and deserializing JSON data related to block list properties.</param>
/// <param name="constructorCache">A cache that stores constructors for block list property values to improve performance.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
/// <param name="elementCacheService">The cache for elements.</param>
public BlockListPropertyValueCreator(
BlockEditorConverter blockEditorConverter,
IVariationContextAccessor variationContextAccessor,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
IElementCacheService elementCacheService,
IJsonSerializer jsonSerializer,
BlockListPropertyValueConstructorCache constructorCache,
ILanguageService languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
{
_jsonSerializer = jsonSerializer;
_constructorCache = constructorCache;
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -21,6 +22,7 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
private readonly ILanguageService _languageService;
private readonly IElementCacheService _elementCacheService;
/// <summary>
/// Creates a specific data converter for the block property implementation.
@@ -64,13 +66,14 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
/// <returns></returns>
protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator);
protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, IPropertyRenderingContextAccessor propertyRenderingContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService)
protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, IPropertyRenderingContextAccessor propertyRenderingContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, ILanguageService languageService, IElementCacheService elementCacheService)
{
BlockEditorConverter = blockEditorConverter;
_variationContextAccessor = variationContextAccessor;
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
_blockEditorVarianceHandler = blockEditorVarianceHandler;
_languageService = languageService;
_elementCacheService = elementCacheService;
}
protected BlockEditorConverter BlockEditorConverter { get; }
@@ -121,17 +124,14 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
CreateBlockModelFromItems createModelFromItems,
EnrichBlockItemModelFromConfiguration? enrichBlockItem = null)
{
if (converted.BlockValue.ContentData.Count == 0)
if (converted.Layout is null || converted.Layout.Any() is false)
{
return createEmptyModel();
}
if (converted.Layout is null)
{
return createEmptyModel();
}
var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey);
TBlockConfiguration[] blockConfigurationsAsArray = blockConfigurations as TBlockConfiguration[] ?? blockConfigurations.ToArray();
var blockConfigMap = blockConfigurationsAsArray.ToDictionary(bc => bc.ContentElementTypeKey);
var blockContentDataMap = converted.BlockValue.ContentData.ToDictionary(b => b.Key);
VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext();
var languagesByIsoCode = (await _languageService.GetAllAsync())
.ToDictionary(l => l.IsoCode, StringComparer.OrdinalIgnoreCase);
@@ -139,15 +139,33 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
// Convert the content data
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
foreach (BlockItemData data in converted.BlockValue.ContentData)
// Get all layouts.
// NOTE: While the Grid areas are modeled to contain areas within areas, in reality it cannot be configured as
// such, so this "top-level aggregation" of layouts works in effect.
IBlockLayoutItem[] allLayouts = converted
.Layout
.SelectMany(layout => new[] { layout }.Union(layout.GetContainedLayouts()))
.ToArray();
foreach (var layout in allLayouts)
{
if (!blockConfigMap.ContainsKey(data.ContentTypeKey))
IPublishedElement? element = null;
BlockItemData? data = null;
if (layout.IsExternalContent)
{
continue;
element = await _elementCacheService.GetByKeyAsync(layout.ContentKey, preview);
if (preview is false && element?.IsPublished(variationContext.Culture) is false)
{
element = null;
}
}
else if (blockContentDataMap.TryGetValue(layout.ContentKey, out data))
{
element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview);
}
IPublishedElement? element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview);
if (element == null)
if (element is null)
{
continue;
}
@@ -162,15 +180,19 @@ internal abstract class BlockPropertyValueCreatorBase<TBlockModel, TBlockItemMod
? variationContext.Segment.NullOrWhiteSpaceAsNull()
: null;
Fallback fallback = _propertyRenderingContextAccessor.PropertyRenderingContext?.Fallback ?? default;
if (BlockExposeFallbackHelper.IsBlockExposed(expose, element.Key, expectedBlockVariationCulture, expectedBlockVariationSegment, fallback, languagesByIsoCode, defaultIsoCode, out var resolvedCulture) is false)
string? resolvedCulture = null;
if (layout.IsExternalContent is false)
{
continue;
Fallback fallback = _propertyRenderingContextAccessor.PropertyRenderingContext?.Fallback ?? default;
if (BlockExposeFallbackHelper.IsBlockExposed(expose, element.Key, expectedBlockVariationCulture, expectedBlockVariationSegment, fallback, languagesByIsoCode, defaultIsoCode, out resolvedCulture) is false)
{
continue;
}
}
// If the block was exposed via fallback to a different culture, recreate the element
// with that culture's variation context so its property values come from the resolved culture.
if (resolvedCulture is not null && resolvedCulture.InvariantEquals(expectedBlockVariationCulture) is false)
if (resolvedCulture is not null && resolvedCulture.InvariantEquals(expectedBlockVariationCulture) is false && data is not null)
{
VariationContext? originalContext = _variationContextAccessor.VariationContext;
try
@@ -3,6 +3,7 @@
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -18,7 +19,9 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
/// </summary>
/// <param name="blockEditorConverter">The <see cref="BlockEditorConverter"/> used to convert block editor values.</param>
/// <param name="variationContextAccessor">The <see cref="IVariationContextAccessor"/> providing access to the variation context.</param>
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
/// <param name="blockEditorVarianceHandler">The <see cref="BlockEditorVarianceHandler"/> that handles block editor variance.</param>
/// <param name="elementCacheService">The cache for elements.</param>
/// <param name="jsonSerializer">The <see cref="IJsonSerializer"/> used for JSON serialization and deserialization.</param>
/// <param name="constructorCache">The <see cref="RichTextBlockPropertyValueConstructorCache"/> used to cache rich text block property value constructors.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
@@ -27,10 +30,11 @@ internal sealed class RichTextBlockPropertyValueCreator : BlockPropertyValueCrea
IVariationContextAccessor variationContextAccessor,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
IElementCacheService elementCacheService,
IJsonSerializer jsonSerializer,
RichTextBlockPropertyValueConstructorCache constructorCache,
ILanguageService languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
{
_jsonSerializer = jsonSerializer;
_constructorCache = constructorCache;
@@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
@@ -46,6 +47,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
private readonly ILanguageService _languageService;
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
private readonly IElementCacheService _elementCacheService;
private DeliveryApiSettings _deliveryApiSettings;
private readonly IDisposable? _deliveryApiSettingsChangeSubscription;
@@ -69,6 +71,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
/// <param name="deliveryApiSettingsMonitor">Monitors settings for the Delivery API.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
/// <param name="elementCacheService">The cache for elements.</param>
public RteBlockRenderingValueConverter(
HtmlLocalLinkParser linkParser,
HtmlUrlParser urlParser,
@@ -85,7 +88,8 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
BlockEditorVarianceHandler blockEditorVarianceHandler,
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
IElementCacheService elementCacheService)
{
_linkParser = linkParser;
_urlParser = urlParser;
@@ -102,6 +106,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
_blockEditorVarianceHandler = blockEditorVarianceHandler;
_languageService = languageService;
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
_elementCacheService = elementCacheService;
_deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue;
_deliveryApiSettingsChangeSubscription = deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings);
@@ -128,6 +133,45 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
{
}
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
public RteBlockRenderingValueConverter(
HtmlLocalLinkParser linkParser,
HtmlUrlParser urlParser,
HtmlImageSourceParser imageSourceParser,
IApiRichTextElementParser apiRichTextElementParser,
IApiRichTextMarkupParser apiRichTextMarkupParser,
IPartialViewBlockEngine partialViewBlockEngine,
BlockEditorConverter blockEditorConverter,
IJsonSerializer jsonSerializer,
IApiElementBuilder apiElementBuilder,
RichTextBlockPropertyValueConstructorCache constructorCache,
ILogger<RteBlockRenderingValueConverter> logger,
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
: this(
linkParser,
urlParser,
imageSourceParser,
apiRichTextElementParser,
apiRichTextMarkupParser,
partialViewBlockEngine,
blockEditorConverter,
jsonSerializer,
apiElementBuilder,
constructorCache,
logger,
variationContextAccessor,
blockEditorVarianceHandler,
deliveryApiSettingsMonitor,
languageService,
propertyRenderingContextAccessor,
StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
{
}
/// <summary>
/// Gets the cache level for the property.
/// </summary>
@@ -328,7 +372,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
return null;
}
var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, blocks, preview, configuration.Blocks).GetAwaiter().GetResult();
}
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Extensions;
@@ -36,6 +37,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
private readonly ILanguageService _languageService;
private readonly IPropertyRenderingContextAccessor _propertyRenderingContextAccessor;
private readonly IElementCacheService _elementCacheService;
/// <summary>
/// Initializes a new instance of the <see cref="SingleBlockPropertyValueConverter"/> class.
@@ -49,6 +51,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
/// <param name="propertyRenderingContextAccessor">Accessor for the current property rendering context.</param>
/// <param name="elementCacheService">The cache for elements.</param>
public SingleBlockPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
@@ -58,7 +61,8 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
IElementCacheService elementCacheService)
{
_proflog = proflog;
_blockConverter = blockConverter;
@@ -69,6 +73,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
_blockEditorVarianceHandler = blockEditorVarianceHandler;
_languageService = languageService;
_propertyRenderingContextAccessor = propertyRenderingContextAccessor;
_elementCacheService = elementCacheService;
}
/// <inheritdoc cref="SingleBlockPropertyValueConverter(IProfilingLogger, BlockEditorConverter, IApiElementBuilder, IJsonSerializer, BlockListPropertyValueConstructorCache, IVariationContextAccessor, BlockEditorVarianceHandler, ILanguageService, IPropertyRenderingContextAccessor)"/>
@@ -85,6 +90,21 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
{
}
[Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V20.")]
public SingleBlockPropertyValueConverter(
IProfilingLogger proflog,
BlockEditorConverter blockConverter,
IApiElementBuilder apiElementBuilder,
IJsonSerializer jsonSerializer,
BlockListPropertyValueConstructorCache constructorCache,
IVariationContextAccessor variationContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
ILanguageService languageService,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor)
: this(proflog, blockConverter, apiElementBuilder, jsonSerializer, constructorCache, variationContextAccessor, blockEditorVarianceHandler, languageService, propertyRenderingContextAccessor, StaticServiceProvider.Instance.GetRequiredService<IElementCacheService>())
{
}
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.SingleBlock);
@@ -94,7 +114,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Element;
=> PropertyCacheLevel.Elements;
/// <inheritdoc />
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
@@ -149,7 +169,7 @@ public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDe
}
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache, _languageService);
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _propertyRenderingContextAccessor, _blockEditorVarianceHandler, _elementCacheService, _jsonSerializer, _constructorCache, _languageService);
return creator.CreateBlockModelAsync(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks).GetAwaiter().GetResult();
}
}
@@ -1,5 +1,6 @@
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
@@ -16,7 +17,9 @@ internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreato
/// </summary>
/// <param name="blockEditorConverter">The service used to convert block editor data.</param>
/// <param name="variationContextAccessor">Provides access to the current variation context.</param>
/// <param name="propertyRenderingContextAccessor">Provides access to the current rendering context.</param>
/// <param name="blockEditorVarianceHandler">Handles variance logic for block editors.</param>
/// <param name="elementCacheService">The cache for elements.</param>
/// <param name="jsonSerializer">The serializer used for JSON serialization and deserialization.</param>
/// <param name="constructorCache">A cache for constructors used in block list property value creation.</param>
/// <param name="languageService">Service used to retrieve language information for fallback resolution.</param>
@@ -25,10 +28,11 @@ internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreato
IVariationContextAccessor variationContextAccessor,
IPropertyRenderingContextAccessor propertyRenderingContextAccessor,
BlockEditorVarianceHandler blockEditorVarianceHandler,
IElementCacheService elementCacheService,
IJsonSerializer jsonSerializer,
BlockListPropertyValueConstructorCache constructorCache,
ILanguageService languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService)
: base(blockEditorConverter, variationContextAccessor, propertyRenderingContextAccessor, blockEditorVarianceHandler, languageService, elementCacheService)
{
_jsonSerializer = jsonSerializer;
_constructorCache = constructorCache;
@@ -129,7 +129,7 @@ internal sealed class MemberEditingService : IMemberEditingService
}
// this should be validated already so it's OK to throw an exception here
var memberName = createModel.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name
var memberName = createModel.Variants.FirstOrDefault(v => v.Culture is null)?.Name
?? throw new ArgumentException("Expected an invariant variant for the member name.", nameof(createModel));
var identityMember = MemberIdentityUser.CreateNew(
@@ -391,7 +391,7 @@ internal sealed class MemberEditingService : IMemberEditingService
private async Task<MemberEditingOperationStatus> ValidateMemberDataAsync(MemberEditingModelBase model, Guid? memberKey, string? password)
{
if (model.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name.IsNullOrWhiteSpace() is not false)
if (model.Variants.FirstOrDefault(v => v.Culture is null)?.Name.IsNullOrWhiteSpace() is not false)
{
return MemberEditingOperationStatus.InvalidName;
}
@@ -1,45 +1,84 @@
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Factories;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.HybridCache.Services;
/// <inheritdoc/>
internal class BlockElementService : IBlockElementService
{
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
private readonly IPublishedContentFactory _publishedContentFactory;
private readonly IPublishedModelFactory _publishedModelFactory;
private readonly ILanguageService _languageService;
public BlockElementService(
IPublishedContentTypeCache publishedContentTypeCache,
IPublishedContentFactory publishedContentFactory,
IPublishedModelFactory publishedModelFactory)
IPublishedModelFactory publishedModelFactory,
ILanguageService languageService)
{
_publishedContentTypeCache = publishedContentTypeCache;
_publishedContentFactory = publishedContentFactory;
_publishedModelFactory = publishedModelFactory;
_languageService = languageService;
}
public Task<IPublishedElement?> BuildElementAsync(BlockItemData blockItemData, bool? preview = null)
/// <inheritdoc/>
public async Task<IPublishedElement?> BuildElementAsync(IPublishedElement owner, BlockItemData blockItemData, bool? preview = null)
{
ILanguage[]? allLanguages = null;
ILanguage? defaultLanguage = null;
// Only convert element types - content types will cause an exception when PublishedModelFactory creates the model
IPublishedContentType? publishedContentType = _publishedContentTypeCache.Get(PublishedItemType.Element, blockItemData.ContentTypeKey);
if (publishedContentType is null || publishedContentType.IsElement is false)
{
return Task.FromResult<IPublishedElement?>(null);
return null;
}
var propertyData = new Dictionary<string, PropertyData[]>();
foreach (IGrouping<string, BlockPropertyValue> properties in blockItemData.Values.GroupBy(value => value.Alias))
{
propertyData[properties.Key] = properties.Select(property => new PropertyData
IPublishedPropertyType? propertyType = publishedContentType.GetPropertyType(properties.Key);
if (propertyType is null)
{
Culture = property.Culture ?? string.Empty, // throws an error if there is no value
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
Value = property.Value,
}).ToArray();
continue;
}
if (propertyType.VariesByCulture() && owner.ContentType.VariesByCulture() is false)
{
// Special case:
// The element property type varies by culture, but the owner element (e.g. the page) content type does not
// vary by culture. Since the created element is fully culture aware at render time, we need to replicate
// property values across all available languages, to make them available for rendering.
allLanguages ??= (await _languageService.GetAllAsync()).ToArray();
defaultLanguage ??= allLanguages.SingleOrDefault(l => l.IsDefault)
?? throw new InvalidOperationException("Could not find the default language.");
BlockPropertyValue property = properties.FirstOrDefault(p => p.Culture.InvariantEquals(defaultLanguage.IsoCode))
?? properties.First();
propertyData[properties.Key] = allLanguages.Select(language => new PropertyData
{
Culture = language.IsoCode,
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
Value = property.Value,
}).ToArray();
}
else
{
propertyData[properties.Key] = properties.Select(property => new PropertyData
{
Culture = property.Culture ?? string.Empty, // throws an error if there is no value
Segment = property.Segment ?? string.Empty, // throws an error if there is no value
Value = property.Value,
}).ToArray();
}
}
var published = preview is not true;
@@ -47,9 +86,15 @@ internal class BlockElementService : IBlockElementService
const string name = "n/a";
var cultureInfos = (publishedContentType.VariesByCulture()
? blockItemData.Values.Select(value => value.Culture).WhereNotNull().Distinct()
: []).ToDictionary(
IEnumerable<string> cultures = publishedContentType.VariesByCulture()
? propertyData
.SelectMany(p => p.Value.Select(v => v.Culture))
.Where(c => c.IsNullOrWhiteSpace() is false)
.OfType<string>()
.Distinct()
: [];
var cultureInfos = cultures.ToDictionary(
culture => culture,
_ => new CultureVariation
{
@@ -83,6 +128,6 @@ internal class BlockElementService : IBlockElementService
};
var result = _publishedContentFactory.ToIPublishedElement(contentCacheNode, draft);
return Task.FromResult(result.CreateModel(_publishedModelFactory));
return result.CreateModel(_publishedModelFactory);
}
}
@@ -670,7 +670,7 @@ export const data: Array<UmbMockDataTypeModel> = [
alias: 'blocks',
value: [
{
label: 'Mocked Block Type for Block List',
label: 'Mocked Block Type for Block List: ${elementProperty}',
contentElementTypeKey: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c',
settingsElementTypeKey: 'all-property-editors-document-type-id',
iconColor: '#F5C1BC',
@@ -1062,13 +1062,21 @@ export const data: Array<UmbMockDocumentModel> = [
layout: {
'Umbraco.BlockList': [
{
key: '1234',
contentKey: '1234',
settingsKey: '5678',
},
{
key: '1234-headline',
contentKey: '1234-headline',
settingsKey: '1234-headline-settings',
},
{
key: '45110b54-764e-4198-858d-6bf8f51589b6',
contentKey: 'simple-element-id',
settingsKey: null,
isExternalContent: true,
},
],
},
contentData: [
@@ -0,0 +1,30 @@
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const blockReferenceHandlers = [
// GET /document/:id/referenced-elements-with-pending-changes
// Returns library elements referenced by a document that have unpublished draft changes.
http.get(umbracoPath('/document/:id/referenced-elements-with-pending-changes'), () => {
return HttpResponse.json({
total: 2,
items: [
{
id: 'simple-element-id',
name: 'Simple Element',
documentType: { id: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c', icon: 'icon-lab' },
state: 'PublishedPendingChanges',
publishDate: '2024-02-01T10:00:00.000Z',
scheduledPublishDate: null,
},
{
id: 'element-in-folder-id',
name: 'Element In Folder',
documentType: { id: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c', icon: 'icon-lab' },
state: 'PublishedPendingChanges',
publishDate: '2024-01-17T08:00:00.000Z',
scheduledPublishDate: '2026-05-01T00:00:00.000Z',
},
],
});
}),
];
@@ -5,6 +5,7 @@ import { publishingHandlers } from './publishing.handlers.js';
import { detailHandlers } from './detail.handlers.js';
import { folderHandlers } from './folder.handlers.js';
import { moveCopyHandlers } from './move-copy.handlers.js';
import { blockReferenceHandlers } from './block-reference.handlers.js';
export const handlers = [
...recycleBinHandlers,
@@ -14,4 +15,5 @@ export const handlers = [
...detailHandlers,
...folderHandlers,
...moveCopyHandlers,
...blockReferenceHandlers,
];
@@ -9,7 +9,12 @@ export const treeHandlers = [
const url = new URL(request.url);
const skip = Number(url.searchParams.get('skip'));
const take = Number(url.searchParams.get('take'));
const foldersOnly = url.searchParams.get('foldersOnly') === 'true';
const response = umbElementMockDb.tree.getRoot({ skip, take });
if (foldersOnly) {
response.items = response.items.filter((item: any) => item.isFolder);
response.total = response.items.length;
}
return HttpResponse.json(response);
}),
@@ -19,7 +24,12 @@ export const treeHandlers = [
if (!parentId) return;
const skip = Number(url.searchParams.get('skip'));
const take = Number(url.searchParams.get('take'));
const foldersOnly = url.searchParams.get('foldersOnly') === 'true';
const response = umbElementMockDb.tree.getChildrenOf({ parentId, skip, take });
if (foldersOnly) {
response.items = response.items.filter((item: any) => item.isFolder);
response.total = response.items.length;
}
return HttpResponse.json(response);
}),
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@umbraco-cms/backoffice",
"version": "18.1.0-rc",
"version": "19.0.0-beta1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umbraco-cms/backoffice",
"version": "18.1.0-rc",
"version": "19.0.0-beta1",
"license": "MIT",
"workspaces": [
"./src/libs/*",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@umbraco-cms/backoffice",
"license": "MIT",
"version": "18.1.0-rc",
"version": "19.0.0-beta1",
"type": "module",
"exports": {
".": null,
@@ -2936,6 +2936,12 @@ export default {
unsupportedBlockName: 'Unsupported',
unsupportedBlockDescription:
'This content is no longer supported in this Editor. If you are missing this content, please contact your administrator. Otherwise delete it.',
tabLibrary: 'Library',
transferToElementLibrary: 'Transfer to Library',
disconnectFromElementLibrary: 'Disconnect from Library',
disconnectFromElementLibraryConfirm:
'This will create a local copy of the Element content. The Library Element will not be affected.',
elementUsedByCount: (count: number) => `This Element is referenced by ${count} item(s).`,
blockVariantConfigurationNotSupported:
'One or more Block Types of this Block Editor is using a Element-Type that is configured to Vary By Culture or Vary By Segment. This is not supported on a Content item that does not vary by Culture or Segment.',
},
@@ -4,6 +4,8 @@
--umb-section-sidebar-width: 300px;
--umb-card-medium-min-width: 160px;
--umb-card-large-min-width: 250px;
--umb-color-reference: #7532c8;
--umb-color-reference-contrast: var(--uui-color-surface, #fff);
}
@font-face {
@@ -154,14 +154,14 @@ export class UmbBlockGridManagerContext<
while (i--) {
const currentEntry = entries[i];
// Lets check if we found the right parent layout entry:
if (currentEntry.contentKey === parentId) {
if (currentEntry.key === parentId) {
// Append the layout entry to be inserted and unfreeze the rest of the data:
const areas =
currentEntry.areas?.map((x) =>
x.key === areaKey
? {
...x,
items: pushAtToUniqueArray([...x.items], insert, (x) => x.contentKey === insert.contentKey, index),
items: pushAtToUniqueArray([...x.items], insert, (x) => x.key === insert.key, index),
}
: x,
) ?? [];
@@ -171,7 +171,7 @@ export class UmbBlockGridManagerContext<
...currentEntry,
areas,
},
(x) => x.contentKey === currentEntry.contentKey,
(x) => x.key === currentEntry.key,
);
}
// Otherwise check if any items of the areas are the parent layout entry we are looking for. We do so based on parentId, recursively:
@@ -199,7 +199,7 @@ export class UmbBlockGridManagerContext<
(z) => z.key === area.key,
),
},
(x) => x.contentKey === currentEntry.contentKey,
(x) => x.key === currentEntry.key,
);
}
}
@@ -32,6 +32,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
layout: {
[UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
{
key: 'contentKey',
columnSpan: 12,
rowSpan: 1,
areas: [],
@@ -54,6 +55,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
contentData: blockGridPropertyValue.contentData,
layout: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -62,6 +62,7 @@ export class UmbBlockGridToBlockClipboardCopyPropertyValueTranslator
}
return {
key: gridLayout.key,
contentKey: gridLayout.contentKey,
settingsKey: gridLayout.settingsKey,
};
@@ -36,6 +36,7 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => {
columnSpan: 12,
rowSpan: 1,
areas: [],
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -49,6 +50,7 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => {
contentData: blockGridPropertyValue.contentData,
layout: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -40,6 +40,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
columnSpan: 12,
rowSpan: 1,
areas: [],
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -34,6 +34,7 @@ describe('UmbGridBlockToBlockGridClipboardPastePropertyValueTranslator', () => {
columnSpan: 12,
rowSpan: 1,
areas: [],
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -183,6 +183,7 @@ export class UmbBlockGridEntriesContext
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
const blockTypes = this.#allowedBlockTypes.getValue();
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
const configuredSize = this._manager
.getEditorConfiguration()
@@ -200,6 +201,7 @@ export class UmbBlockGridEntriesContext
blocks: blockTypes,
blockGroups: this._manager.getBlockGroups() ?? [],
openClipboard: routingInfo.view === 'clipboard',
libraryAllowedElementTypeKeys,
clipboardFilter: async (clipboardEntryDetail) => {
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
pasteTranslatorManifests,
@@ -237,7 +239,7 @@ export class UmbBlockGridEntriesContext
};
})
.onSubmit(async (value, data) => {
if (value?.create && data) {
if (value && 'create' in value && data) {
const created = await this.create(
value.create.contentElementTypeKey,
// We can parse an empty object, cause the rest will be filled in by others.
@@ -254,7 +256,9 @@ export class UmbBlockGridEntriesContext
} else {
throw new Error('Failed to create block');
}
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
} else if (value && 'library' in value) {
this._manager?.insertExternalContent(value.library.elementKey);
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
if (!clipboardContext) {
throw new Error('Clipboard context not available');
@@ -498,23 +502,23 @@ export class UmbBlockGridEntriesContext
}
// create Block?
override async delete(contentKey: string) {
override async delete(key: string) {
// TODO: Loop through children and delete them as well?
// Find layout entry:
const layout = this._layoutEntries.getValue().find((x) => x.contentKey === contentKey);
const layout = this._layoutEntries.getValue().find((x) => x.key === key);
if (!layout) {
throw new Error(`Cannot delete block, missing layout for ${contentKey}`);
throw new Error(`Cannot delete block, missing layout for ${key}`);
}
// The following loop will only delete the referenced data of sub Layout Entries, as the Layout entry is part of the main Layout Entry they will go away when that is removed. [NL]
forEachBlockLayoutEntryOf(layout, async (entry) => {
if (entry.settingsKey) {
this._manager!.removeOneSettings(entry.settingsKey);
}
this._manager!.removeOneContent(contentKey);
this._manager!.removeExposesOf(contentKey);
this._manager!.removeOneContent(entry.contentKey);
this._manager!.removeExposesOf(entry.contentKey);
});
await super.delete(contentKey);
await super.delete(key);
}
protected async _insertFromPropertyValue(value: UmbBlockGridValueModel, originData: UmbBlockGridWorkspaceOriginData) {
@@ -112,10 +112,10 @@ function resolvePlacementAsBlockGrid(
const SORTER_CONFIG: UmbSorterConfig<UmbBlockGridLayoutModel, UmbBlockGridEntryElement> = {
getUniqueOfElement: (element) => {
return element.contentKey!;
return element.key!;
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.contentKey;
return modelEntry.key;
},
resolvePlacement: resolvePlacementAsBlockGrid,
identifier: 'block-grid-editor',
@@ -400,12 +400,11 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen
<div class="umb-block-grid__layout-container" data-area-length=${this._layoutEntries.length}>
${repeat(
this._layoutEntries,
(layout) => layout.contentKey,
(layout) => layout.key,
(layout, index) =>
html`<umb-block-grid-entry
class="umb-block-grid__layout-item"
index=${index}
.contentKey=${layout.contentKey}
.layout=${layout}>
</umb-block-grid-entry>
`,
@@ -2,9 +2,10 @@ import type { UmbBlockGridLayoutModel } from '../../types.js';
import { UMB_BLOCK_GRID } from '../../constants.js';
import { UmbBlockGridEntryContext } from './block-grid-entry.context.js';
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
import { umbDestroyOnDisconnect, UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
@@ -30,15 +31,61 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
this.#context.setIndex(value);
}
/**
* Set the layout entry for this block.
*/
public set layout(value: UmbBlockGridLayoutModel | undefined) {
if (!value) return;
const key = value.key;
const contentKey = value.contentKey;
if (key && key !== this._key) {
this._key = key;
this.#context.setKey(key);
}
if (contentKey && contentKey !== this._contentKey) {
this._contentKey = contentKey;
this._blockViewProps.contentKey = contentKey;
this.setAttribute('data-element-key', contentKey);
new UmbObserveValidationStateController(
this,
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`,
(hasMessages) => {
this._contentInvalid = hasMessages;
this._blockViewProps.contentInvalid = hasMessages;
},
'observeMessagesForContent',
);
}
}
public get key(): string | undefined {
return this._key;
}
private _key?: string | undefined;
/**
* @deprecated Use the `layout` property instead. Will be removed in Umbraco 20.
*/
@property({ attribute: false })
public get contentKey(): string | undefined {
return this._contentKey;
}
public set contentKey(key: string | undefined) {
if (!key || key === this._contentKey) return;
new UmbDeprecation({
deprecated: 'umb-block-grid-entry.contentKey property',
solution: 'Use the `layout` property instead.',
removeInVersion: '20.0.0',
}).warn();
this._contentKey = key;
this._blockViewProps.contentKey = key;
this.setAttribute('data-element-key', key);
if (!this._key) {
this.#context.setKey(key);
}
this.#context.setContentKey(key);
new UmbObserveValidationStateController(
@@ -88,6 +135,8 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
@state()
private _exposed?: boolean;
private _localExpose?: boolean;
// Unsupported is triggered if the Block Type is not recognized, it can also be triggered by the Content Element Type not existing any longer. [NL]
@state()
private _unsupported?: boolean;
@@ -134,10 +183,11 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
config: { showContentEdit: false, showSettingsEdit: false },
}; // Set to undefined cause it will be set before we render.
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockGridLayoutModel>>) {
this._blockViewProps = { ...this._blockViewProps, ...incoming };
this.requestUpdate('_blockViewProps');
}
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
private _isExternalContent = false;
@state()
private _externalContentVariantState: string | null | undefined;
@state()
private _isReadOnly = false;
@@ -193,8 +243,8 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
this.observe(
this.#context.hasExpose,
(exposed) => {
this.#updateBlockViewProps({ unpublished: !exposed });
this._exposed = exposed;
this._localExpose = exposed;
this.#updateExposedState();
},
null,
);
@@ -211,6 +261,22 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
this.observe(this.#context.actionsVisibility, (showActions) => (this._showActions = showActions), null);
this.observe(this.#context.inlineEditingMode, (mode) => (this._inlineEditingMode = mode), null);
this.observe(this.#context.isSortMode, (isSortMode) => (this._isSortMode = isSortMode), null);
this.observe(
this.#context.isExternalContent,
(isExternalContent) => {
this._isExternalContent = isExternalContent;
this.#updateExposedState();
},
null,
);
this.observe(
this.#context.externalContentVariantState,
(state) => {
this._externalContentVariantState = state;
this.#updateExposedState();
},
null,
);
// Data:
this.observe(
@@ -299,6 +365,11 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
);
}
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockGridLayoutModel>>) {
this._blockViewProps = { ...this._blockViewProps, ...incoming };
this.requestUpdate('_blockViewProps');
}
override connectedCallback(): void {
super.connectedCallback();
// element styling:
@@ -374,6 +445,16 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
this.#context.expose();
};
#updateExposedState() {
// External content blocks use the element's variant state; local blocks use the expose entry
const isExposed = this._isExternalContent
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
: this._localExpose;
this.#updateBlockViewProps({ unpublished: !isExposed });
this._exposed = isExposed;
}
#callUpdateInlineCreateButtons() {
clearTimeout(this.#renderTimeout);
this.#renderTimeout = setTimeout(this.#updateInlineCreateButtons, 100) as unknown as number;
@@ -458,6 +539,9 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
this._isSortMode,
() => this.#renderRefBlock(),
() => html`
<umb-entity-frame>
${when(this._isExternalContent, () => html`<uui-icon name="link"></uui-icon>`)} ${this._label}
</umb-entity-frame>
<umb-extension-slot
single
type="blockEditorCustomView"
@@ -580,7 +664,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
#renderActionBar() {
if (this._isSortMode) return nothing;
if (!this._showActions) return nothing;
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_GRID}></umb-block-action-list>`;
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_GRID}></umb-block-action-list>`;
}
static override styles = [
@@ -659,10 +743,6 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
right: calc(1px - (var(--umb-block-grid--column-gap, 0px) * 0.5));
}
.umb-block-grid__block {
height: 100%;
}
:host(:hover):not(:drop)::after {
display: block;
border-color: var(--uui-color-interactive-emphasis);
@@ -696,6 +776,23 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
uui-badge {
z-index: 2;
}
:host([is-reference]) .umb-block-grid__block {
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
}
.umb-block-grid__block {
--umb-entity-frame-opacity: 0;
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
height: 100%;
&:hover,
&:focus-within {
--umb-entity-frame-opacity: 1;
}
}
`,
];
}
@@ -39,7 +39,7 @@ export async function forEachBlockLayoutEntryOf(
callback: (entry: UmbBlockGridLayoutModel, parentUnique: string, areaKey: string) => PromiseLike<void>,
): Promise<void> {
if (entry.areas) {
const parentUnique = entry.contentKey;
const parentUnique = entry.key;
await Promise.all(
entry.areas.map(async (area) => {
const areaKey = area.key;
@@ -32,6 +32,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
layout: {
[UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -51,6 +52,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => {
contentData: blockListPropertyValue.contentData,
layout: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -32,6 +32,7 @@ describe('UmbBlockToBlockListClipboardPastePropertyValueTranslator', () => {
layout: {
[UMB_BLOCK_LIST_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -45,6 +46,7 @@ describe('UmbBlockToBlockListClipboardPastePropertyValueTranslator', () => {
contentData: blockListPropertyValue.contentData,
layout: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -3,8 +3,9 @@ import type { UmbBlockListLayoutModel } from '../../types.js';
import { UMB_BLOCK_LIST } from '../../constants.js';
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
import type {
@@ -32,10 +33,54 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
return this.#context.getIndex();
}
/**
* Set the layout entry for this block.
*/
public set layout(value: UmbBlockListLayoutModel | undefined) {
if (!value) return;
const key = value.key;
const contentKey = value.contentKey;
if (key && key !== this._key) {
this._key = key;
this.#context.setKey(key);
}
if (contentKey && contentKey !== this._contentKey) {
this._contentKey = contentKey;
new UmbObserveValidationStateController(
this,
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`,
(hasMessages) => {
this._contentInvalid = hasMessages;
this._blockViewProps.contentInvalid = hasMessages;
},
'observeMessagesForContent',
);
}
}
public get key(): string | undefined {
return this._key;
}
private _key?: string | undefined;
/**
* @deprecated Use the `layout` property instead. Will be removed in Umbraco 20.
*/
@property({ attribute: false })
public set contentKey(value: string | undefined) {
if (!value) return;
new UmbDeprecation({
deprecated: 'umb-block-list-entry.contentKey property',
solution: 'Use the `layout` property instead.',
removeInVersion: '20.0.0',
}).warn();
this._contentKey = value;
if (!this._key) {
this.#context.setKey(value);
}
this.#context.setContentKey(value);
new UmbObserveValidationStateController(
@@ -73,6 +118,8 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
@state()
private _exposed?: boolean;
private _localExpose?: boolean;
@state()
private _unsupported?: boolean;
@@ -99,10 +146,11 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
config: { showContentEdit: false, showSettingsEdit: false },
}; // Set to undefined cause it will be set before we render.
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockListLayoutModel>>) {
this._blockViewProps = { ...this._blockViewProps, ...incoming };
this.requestUpdate('_blockViewProps');
}
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
private _isExternalContent = false;
@state()
private _externalContentVariantState: string | null | undefined;
@state()
private _isReadOnly = false;
@@ -155,8 +203,8 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
this.observe(
this.#context.hasExpose,
(exposed) => {
this.#updateBlockViewProps({ unpublished: !exposed });
this._exposed = exposed;
this._localExpose = exposed;
this.#updateExposedState();
},
null,
);
@@ -173,6 +221,22 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
this.observe(this.#context.actionsVisibility, (showActions) => (this._showActions = showActions), null);
this.observe(this.#context.inlineEditingMode, (mode) => (this._inlineEditingMode = mode), null);
this.observe(this.#context.isSortMode, (isSortMode) => (this._isSortMode = isSortMode), null);
this.observe(
this.#context.isExternalContent,
(isExternalContent) => {
this._isExternalContent = isExternalContent;
this.#updateExposedState();
},
null,
);
this.observe(
this.#context.externalContentVariantState,
(state) => {
this._externalContentVariantState = state;
this.#updateExposedState();
},
null,
);
// Data props:
this.observe(
@@ -244,6 +308,11 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
);
}
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockListLayoutModel>>) {
this._blockViewProps = { ...this._blockViewProps, ...incoming };
this.requestUpdate('_blockViewProps');
}
override connectedCallback(): void {
super.connectedCallback();
// element styling:
@@ -279,6 +348,16 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
this.#context.expose();
};
#updateExposedState() {
// External content blocks use the element's variant state; local blocks use the expose entry
const isExposed = this._isExternalContent
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
: this._localExpose;
this.#updateBlockViewProps({ unpublished: !isExposed });
this._exposed = isExposed;
}
#extensionSlotFilterMethod = (manifest: ManifestBlockEditorCustomView) => {
if (this._unsupported) {
// If the block is unsupported, we should not allow any custom views to render.
@@ -302,12 +381,14 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
if (this._exposed || this._isReadOnly) {
return ext.component;
} else {
return html`<div style="min-height: var(--uui-size-16);">
${ext.component}
<umb-block-overlay-expose-button
.contentTypeName=${this._contentTypeName}
@click=${this.#expose}></umb-block-overlay-expose-button>
</div>`;
return html`
<div style="min-height: var(--uui-size-16);">
${ext.component}
<umb-block-overlay-expose-button
.contentTypeName=${this._contentTypeName}
@click=${this.#expose}></umb-block-overlay-expose-button>
</div>
`;
}
};
@@ -328,23 +409,29 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
}
#renderInlineBlock() {
return html`<umb-inline-list-block
.label=${this._label}
.icon=${this._icon}
.index=${this._blockViewProps.index}
.unpublished=${!this._exposed}
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}></umb-inline-list-block>`;
return html`
<umb-inline-list-block
.label=${this._label}
.icon=${this._icon}
.index=${this._blockViewProps.index}
.unpublished=${!this._exposed}
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}>
</umb-inline-list-block>
`;
}
#renderUnsupportedBlock() {
return html`<umb-unsupported-list-block
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}></umb-unsupported-list-block>`;
return html`
<umb-unsupported-list-block
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}>
</umb-unsupported-list-block>
`;
}
#renderBuiltinBlockView = () => {
@@ -366,6 +453,9 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
this._isSortMode,
() => this.#renderRefBlock(),
() => html`
<umb-entity-frame>
${when(this._isExternalContent, () => html`<uui-icon name="link"></uui-icon>`)} ${this._label}
</umb-entity-frame>
<umb-extension-slot
single
type="blockEditorCustomView"
@@ -388,7 +478,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
#renderActionBar() {
if (this._isSortMode) return nothing;
if (!this._showActions) return nothing;
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_LIST}></umb-block-action-list>`;
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_LIST}></umb-block-action-list>`;
}
override render() {
@@ -484,6 +574,21 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
transition: opacity 50ms 16ms;
opacity: 0;
}
:host([is-reference]) .umb-block-list__block {
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
}
.umb-block-list__block {
--umb-entity-frame-opacity: 0;
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
&:hover,
&:focus-within {
--umb-entity-frame-opacity: 1;
}
}
`,
];
}
@@ -494,4 +599,4 @@ declare global {
interface HTMLElementTagNameMap {
'umb-block-list-entry': UmbBlockListEntryElement;
}
}
}
@@ -56,6 +56,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
const blockTypes = this._manager.getBlockTypes();
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
const configuredSize = this._manager
.getEditorConfiguration()
@@ -73,6 +74,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
blocks: blockTypes,
blockGroups: [],
openClipboard: routingInfo.view === 'clipboard',
libraryAllowedElementTypeKeys,
clipboardFilter: async (clipboardEntryDetail) => {
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
pasteTranslatorManifests,
@@ -104,7 +106,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
};
})
.onSubmit(async (value, data) => {
if (value?.create && data) {
if (value && 'create' in value && data) {
const created = await this.create(
value.create.contentElementTypeKey,
{},
@@ -120,7 +122,9 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
} else {
throw new Error('Failed to create block');
}
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
} else if (value && 'library' in value) {
this._manager?.insertExternalContent(value.library.elementKey);
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
if (!clipboardContext) {
throw new Error('Clipboard context not found');
@@ -196,7 +200,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext<
async create(
contentElementTypeKey: string,
partialLayoutEntry?: Omit<UmbBlockListLayoutModel, 'contentKey'>,
partialLayoutEntry?: Omit<UmbBlockListLayoutModel, 'contentKey' | 'key'>,
originData?: UmbBlockListWorkspaceOriginData,
) {
await this._retrieveManager;
@@ -52,7 +52,7 @@ export class UmbBlockListManagerContext<
*/
async createWithPresets(
contentElementTypeKey: string,
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
// This property is used by some implementations, but not used in this. Do not remove. [NL]
_originData?: UmbBlockListWorkspaceOriginData,
@@ -40,10 +40,10 @@ import '../../components/block-list-entry/index.js';
const SORTER_CONFIG: UmbSorterConfig<UmbBlockListLayoutModel, UmbBlockListEntryElement> = {
getUniqueOfElement: (element) => {
return element.contentKey!;
return element.key!;
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.contentKey;
return modelEntry.key;
},
//identifier: 'block-list-editor',
itemSelector: 'umb-block-list-entry',
@@ -406,15 +406,10 @@ export class UmbPropertyEditorUIBlockListElement
${this.#renderSortModeToolbar()}
${repeat(
this._layouts,
(layout) => layout.contentKey,
(layout) => layout.key,
(layout, index) => html`
${this.#renderInlineCreateButton(index)}
<umb-block-list-entry
index=${index}
.contentKey=${layout.contentKey}
.layout=${layout}
${umbDestroyOnDisconnect()}>
</umb-block-list-entry>
<umb-block-list-entry index=${index} .layout=${layout} ${umbDestroyOnDisconnect()}></umb-block-list-entry>
`,
)}
${this.#renderCreateButtonGroup()}
@@ -2,7 +2,7 @@ import type { UmbBlockRteLayoutModel } from '../../types.js';
import { UMB_BLOCK_RTE } from '../../constants.js';
import { UmbBlockRteEntryContext } from '../../context/block-rte-entry.context.js';
import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
@@ -22,10 +22,35 @@ import '../../../block/action/block-action-list.element.js';
*/
@customElement('umb-rte-block')
export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropertyEditorUiElement {
/**
* The unique key of this block layout entry.
*/
@property({ type: String, attribute: 'data-key', reflect: true })
public set key(value: string | undefined) {
if (!value) return;
this._key = value;
this.#context.setKey(value);
}
public get key(): string | undefined {
return this._key;
}
private _key?: string | undefined;
/**
* @deprecated Use `key` instead. Will be removed in Umbraco 20.
*/
@property({ type: String, attribute: 'data-content-key', reflect: true })
public set contentKey(value: string | undefined) {
if (!value) return;
new UmbDeprecation({
deprecated: 'umb-rte-block.contentKey property',
solution: 'Use the `key` property instead.',
removeInVersion: '20.0.0',
}).warn();
this._contentKey = value;
if (!this._key) {
this.#context.setKey(value);
}
this.#context.setContentKey(value);
new UmbObserveValidationStateController(
@@ -255,19 +280,22 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
if (this._exposed || this._isReadOnly) {
return ext.component;
} else {
return html`<div>
${ext.component}
<umb-block-overlay-expose-button
.contentTypeName=${this._contentTypeName}
@click=${this.#expose}></umb-block-overlay-expose-button>
</div>`;
return html`
<div>
${ext.component}
<umb-block-overlay-expose-button
.contentTypeName=${this._contentTypeName}
@click=${this.#expose}></umb-block-overlay-expose-button>
</div>
`;
}
};
#renderBlock() {
return this.contentKey && this._contentTypeAlias
? html`
<div class="uui-text uui-font">
<div class="umb-block-rte__block uui-text uui-font">
<umb-entity-frame .label=${this._label}></umb-entity-frame>
<umb-extension-slot
type="blockEditorCustomView"
default-element="umb-ref-rte-block"
@@ -287,7 +315,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
#renderActionBar() {
if (!this._showActions) return nothing;
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_RTE}></umb-block-action-list>`;
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_RTE}></umb-block-action-list>`;
}
#renderBuiltinBlockView = () => {
@@ -299,14 +327,17 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
};
#renderRefBlock() {
return html`<umb-ref-rte-block
.label=${this._label}
.icon=${this._icon}
.index=${this._blockViewProps.index}
.unpublished=${!this._exposed}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
.config=${this._blockViewProps.config}></umb-ref-rte-block>`;
return html`
<umb-ref-rte-block
.label=${this._label}
.icon=${this._icon}
.index=${this._blockViewProps.index}
.unpublished=${!this._exposed}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
.config=${this._blockViewProps.config}>
</umb-ref-rte-block>
`;
}
override render() {
@@ -355,6 +386,21 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
:host([drag-placeholder]) {
opacity: 0.2;
}
:host([is-reference]) .umb-block-rte__block {
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
}
.umb-block-rte__block {
--umb-entity-frame-opacity: 0;
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
&:hover,
&:focus-within {
--umb-entity-frame-opacity: 1;
}
}
`,
];
}
@@ -77,12 +77,15 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
const config = propertyContext.getConfig();
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
return {
modal: { size: modalSize },
data: {
blocks: blockTypes,
blockGroups: [],
openClipboard: routingInfo.view === 'clipboard',
libraryAllowedElementTypeKeys,
clipboardFilter: async (clipboardEntryDetail) => {
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
pasteTranslatorManifests,
@@ -114,7 +117,7 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
};
})
.onSubmit(async (value, data) => {
if (value?.create && data) {
if (value && 'create' in value && data) {
const created = await this.create(
value.create.contentElementTypeKey,
{},
@@ -130,7 +133,12 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
} else {
throw new Error('Failed to create block');
}
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
} else if (value && 'library' in value && data) {
await this._manager?.insertExternalContent(
value.library.elementKey,
data.originData as UmbBlockRteWorkspaceOriginData,
);
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
if (!clipboardContext) {
throw new Error('Clipboard context not found');
@@ -193,7 +201,7 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext<
async create(
contentElementTypeKey: string,
partialLayoutEntry?: Omit<UmbBlockRteLayoutModel, 'contentKey'>,
partialLayoutEntry?: Omit<UmbBlockRteLayoutModel, 'contentKey' | 'key'>,
originData?: UmbBlockRteWorkspaceOriginData,
) {
await this._retrieveManager;
@@ -3,6 +3,7 @@ import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js';
import type { UmbBlockDataModel } from '../../block/types.js';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block';
import { UmbId } from '@umbraco-cms/backoffice/id';
import '../components/block-rte-entry/index.js';
@@ -53,7 +54,7 @@ export class UmbBlockRteManagerContext<
*/
async createWithPresets(
contentElementTypeKey: string,
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
// This property is used by some implementations, but not used in this, do not remove. [NL]
_originData?: UmbBlockRteWorkspaceOriginData,
@@ -82,6 +83,14 @@ export class UmbBlockRteManagerContext<
return true;
}
override async insertExternalContent(elementKey: string, originData?: UmbBlockRteWorkspaceOriginData) {
await super.insertExternalContent(elementKey, originData);
if (originData) {
const layout = { key: UmbId.new(), contentKey: elementKey, isExternalContent: true } as BlockLayoutType;
this.notifyBlockInserted(layout, originData);
}
}
/**
* @param contentKey
* @internal
@@ -32,6 +32,7 @@ describe('UmbBlockSingleToBlockClipboardCopyPropertyValueTranslator', () => {
layout: {
[UMB_BLOCK_SINGLE_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -51,6 +52,7 @@ describe('UmbBlockSingleToBlockClipboardCopyPropertyValueTranslator', () => {
contentData: blockSinglePropertyValue.contentData,
layout: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -32,6 +32,7 @@ describe('UmbBlockToBlockSingleClipboardPastePropertyValueTranslator', () => {
layout: {
[UMB_BLOCK_SINGLE_PROPERTY_EDITOR_SCHEMA_ALIAS]: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -45,6 +46,7 @@ describe('UmbBlockToBlockSingleClipboardPastePropertyValueTranslator', () => {
contentData: blockSinglePropertyValue.contentData,
layout: [
{
key: 'contentKey',
contentKey: 'contentKey',
settingsKey: null,
},
@@ -1,10 +1,11 @@
import { UmbBlockSingleEntryContext } from '../../context/block-single-entry.context.js';
import type { UmbBlockSingleLayoutModel } from '../../types.js';
import { UMB_BLOCK_SINGLE } from '../../constants.js';
import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element';
import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils';
import { stringOrStringArrayContains, UmbDeprecation } from '@umbraco-cms/backoffice/utils';
import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block';
import { UmbElementVariantState } from '@umbraco-cms/backoffice/element';
import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation';
import { UUIBlinkAnimationValue, UUIBlinkKeyframes } from '@umbraco-cms/backoffice/external/uui';
import type {
@@ -33,13 +34,57 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
this.#context.setIndex(value);
}
/**
* Set the layout entry for this block.
*/
public set layout(value: UmbBlockSingleLayoutModel | undefined) {
if (!value) return;
const key = value.key;
const contentKey = value.contentKey;
if (key && key !== this._key) {
this._key = key;
this.#context.setKey(key);
}
if (contentKey && contentKey !== this._contentKey) {
this._contentKey = contentKey;
new UmbObserveValidationStateController(
this,
`$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`,
(hasMessages) => {
this._contentInvalid = hasMessages;
this._blockViewProps.contentInvalid = hasMessages;
},
'observeMessagesForContent',
);
}
}
public get key(): string | undefined {
return this._key;
}
private _key?: string | undefined;
/**
* @deprecated Use the `layout` property instead. Will be removed in Umbraco 20.
*/
@property({ attribute: false })
public get contentKey(): string | undefined {
return this._contentKey;
}
public set contentKey(value: string | undefined) {
if (!value) return;
new UmbDeprecation({
deprecated: 'umb-block-single-entry.contentKey property',
solution: 'Use the `layout` property instead.',
removeInVersion: '20.0.0',
}).warn();
this._contentKey = value;
if (!this._key) {
this.#context.setKey(value);
}
this.#context.setContentKey(value);
new UmbObserveValidationStateController(
@@ -74,6 +119,8 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
@state()
private _exposed?: boolean;
private _localExpose?: boolean;
@state()
private _unsupported?: boolean;
@@ -97,10 +144,11 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
config: { showContentEdit: false, showSettingsEdit: false },
}; // Set to undefined cause it will be set before we render.
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockSingleLayoutModel>>) {
this._blockViewProps = { ...this._blockViewProps, ...incoming };
this.requestUpdate('_blockViewProps');
}
@property({ type: Boolean, attribute: 'is-reference', reflect: true })
private _isExternalContent = false;
@state()
private _externalContentVariantState: string | null | undefined;
@state()
private _isReadOnly = false;
@@ -109,6 +157,7 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
super();
this.#init();
}
#init() {
this.observe(
this.#context.showContentEdit,
@@ -152,8 +201,24 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
this.observe(
this.#context.hasExpose,
(exposed) => {
this.#updateBlockViewProps({ unpublished: !exposed });
this._exposed = exposed;
this._localExpose = exposed;
this.#updateExposedState();
},
null,
);
this.observe(
this.#context.isExternalContent,
(isExternalContent) => {
this._isExternalContent = isExternalContent;
this.#updateExposedState();
},
null,
);
this.observe(
this.#context.externalContentVariantState,
(state) => {
this._externalContentVariantState = state;
this.#updateExposedState();
},
null,
);
@@ -245,6 +310,21 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
);
}
#updateBlockViewProps(incoming: Partial<UmbBlockEditorCustomViewProperties<UmbBlockSingleLayoutModel>>) {
this._blockViewProps = { ...this._blockViewProps, ...incoming };
this.requestUpdate('_blockViewProps');
}
#updateExposedState() {
// External content blocks use the element's variant state; local blocks use the expose entry
const isExposed = this._isExternalContent
? this._externalContentVariantState === UmbElementVariantState.PUBLISHED ||
this._externalContentVariantState === UmbElementVariantState.PUBLISHED_PENDING_CHANGES
: this._localExpose;
this.#updateBlockViewProps({ unpublished: !isExposed });
this._exposed = isExposed;
}
override connectedCallback(): void {
super.connectedCallback();
// element styling:
@@ -313,33 +393,42 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
};
#renderRefBlock() {
return html`<umb-ref-single-block
.label=${this._label}
.icon=${this._icon}
.unpublished=${!this._exposed}
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}></umb-ref-single-block>`;
return html`
<umb-ref-single-block
.label=${this._label}
.icon=${this._icon}
.unpublished=${!this._exposed}
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}>
</umb-ref-single-block>
`;
}
#renderInlineBlock() {
return html`<umb-inline-single-block
.label=${this._label}
.icon=${this._icon}
.unpublished=${!this._exposed}
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}></umb-inline-single-block>`;
return html`
<umb-inline-single-block
.label=${this._label}
.icon=${this._icon}
.unpublished=${!this._exposed}
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}>
</umb-inline-single-block>
`;
}
#renderUnsupportedBlock() {
return html`<umb-unsupported-single-block
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}></umb-unsupported-single-block>`;
return html`
<umb-unsupported-single-block
.config=${this._blockViewProps.config}
.content=${this._blockViewProps.content}
.settings=${this._blockViewProps.settings}
${umbDestroyOnDisconnect()}>
</umb-unsupported-single-block>
`;
}
#renderBuiltinBlockView = () => {
@@ -356,6 +445,9 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
return this.contentKey && (this._contentTypeAlias || this._unsupported)
? html`
<div class="umb-block-single__block">
<umb-entity-frame>
${when(this._isExternalContent, () => html`<uui-icon name="link"></uui-icon>`)} ${this._label}
</umb-entity-frame>
<umb-extension-slot
type="blockEditorCustomView"
default-element=${this._inlineEditingMode ? 'umb-inline-single-block' : 'umb-ref-single-block'}
@@ -375,7 +467,7 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
#renderActionBar() {
if (!this._showActions) return nothing;
return html`<umb-block-action-list id="actions" block-editor=${UMB_BLOCK_SINGLE}></umb-block-action-list>`;
return html`<umb-block-action-list id="actions" .blockEditor=${UMB_BLOCK_SINGLE}></umb-block-action-list>`;
}
override render() {
@@ -471,6 +563,21 @@ export class UmbBlockSingleEntryElement extends UmbLitElement implements UmbProp
transition: opacity 50ms 16ms;
opacity: 0;
}
:host([is-reference]) .umb-block-single__block {
--umb-entity-frame-color: var(--umb-color-reference, #7532c8);
--umb-entity-frame-contrast-color: var(--umb-color-reference-contrast, #ffffff);
}
.umb-block-single__block {
--umb-entity-frame-opacity: 0;
--umb-entity-frame-color: var(--uui-color-interactive-emphasis);
&:hover,
&:focus-within {
--umb-entity-frame-opacity: 1;
}
}
`,
];
}
@@ -37,10 +37,11 @@ export class UmbRefSingleBlockElement extends UmbLitElement {
<umb-ufm-render slot="name" inline .markdown=${this.label} .value=${blockValue}></umb-ufm-render>
${when(
this.unpublished,
() =>
html`<uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}
><umb-localize key="blockEditor_notExposedLabel"></umb-localize
></uui-tag>`,
() => html`
<uui-tag slot="name" look="secondary" title=${this.localize.term('blockEditor_notExposedDescription')}>
<umb-localize key="blockEditor_notExposedLabel"></umb-localize>
</uui-tag>
`,
)}
</uui-ref-node>
`;
@@ -56,6 +56,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
const valueResolver = new UmbClipboardPastePropertyValueTranslatorValueResolver(this);
const blockTypes = this._manager.getBlockTypes() ?? [];
const libraryAllowedElementTypeKeys = await this._getLibraryAllowedElementTypeKeys(blockTypes);
const configuredSize = this._manager
.getEditorConfiguration()
@@ -73,6 +74,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
blocks: blockTypes,
blockGroups: [],
openClipboard: routingInfo.view === 'clipboard',
libraryAllowedElementTypeKeys,
clipboardFilter: async (clipboardEntryDetail) => {
const hasSupportedPasteTranslator = clipboardContext.hasSupportedPasteTranslator(
pasteTranslatorManifests,
@@ -104,7 +106,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
};
})
.onSubmit(async (value, data) => {
if (value?.create && data) {
if (value && 'create' in value && data) {
const created = await this.create(
value.create.contentElementTypeKey,
{},
@@ -120,7 +122,9 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
} else {
throw new Error('Failed to create block');
}
} else if (value?.clipboard && value.clipboard.selection?.length && data) {
} else if (value && 'library' in value) {
this._manager?.insertExternalContent(value.library.elementKey);
} else if (value && 'clipboard' in value && value.clipboard.selection?.length && data) {
const clipboardContext = await this.getContext(UMB_CLIPBOARD_PROPERTY_CONTEXT);
if (!clipboardContext) {
throw new Error('Clipboard context not found');
@@ -196,7 +200,7 @@ export class UmbBlockSingleEntriesContext extends UmbBlockEntriesContext<
async create(
contentElementTypeKey: string,
partialLayoutEntry?: Omit<UmbBlockSingleLayoutModel, 'contentKey'>,
partialLayoutEntry?: Omit<UmbBlockSingleLayoutModel, 'contentKey' | 'key'>,
originData?: UmbBlockSingleWorkspaceOriginData,
) {
await this._retrieveManager;
@@ -28,7 +28,7 @@ export class UmbBlockSingleManagerContext<
*/
async createWithPresets(
contentElementTypeKey: string,
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey'>,
partialLayoutEntry?: Omit<BlockLayoutType, 'contentKey' | 'key'>,
// This property is used by some implementations, but not used in this. Do not remove. [NL]
_originData?: UmbBlockSingleWorkspaceOriginData,
@@ -41,10 +41,10 @@ import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant';
const SORTER_CONFIG: UmbSorterConfig<UmbBlockSingleLayoutModel, UmbBlockSingleEntryElement> = {
getUniqueOfElement: (element) => {
return element.contentKey!;
return element.key!;
},
getUniqueOfModel: (modelEntry) => {
return modelEntry.contentKey;
return modelEntry.key;
},
//identifier: 'block-single-editor',
itemSelector: 'umb-block-single-entry',
@@ -395,13 +395,9 @@ export class UmbPropertyEditorUIBlockSingleElement
return html`
${repeat(
this._layouts,
(x) => x.contentKey,
(x) => x.key,
(layoutEntry) => html`
<umb-block-single-entry
.contentKey=${layoutEntry.contentKey}
.layout=${layoutEntry}
${umbDestroyOnDisconnect()}>
</umb-block-single-entry>
<umb-block-single-entry .layout=${layoutEntry} ${umbDestroyOnDisconnect()}></umb-block-single-entry>
`,
)}
${this.#renderCreateButtonGroup()}
@@ -1,5 +1,6 @@
import type { UmbBlockActionArgs } from './types.js';
import type { UmbAction } from '@umbraco-cms/backoffice/action';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
export interface UmbBlockAction<ArgsMetaType> extends UmbAction<UmbBlockActionArgs<ArgsMetaType>> {
/**
@@ -9,6 +10,13 @@ export interface UmbBlockAction<ArgsMetaType> extends UmbAction<UmbBlockActionAr
*/
getHref(): Promise<string | undefined>;
/**
* An optional reactive observable for the href location.
* When provided, the default kind element subscribes to it and updates the link reactively,
* rather than resolving `getHref()` once at initialisation time.
*/
href?: Observable<string | undefined>;
/**
* The `execute` method, the action will act as a button.
* @returns {Promise<void>}
@@ -22,4 +30,12 @@ export interface UmbBlockAction<ArgsMetaType> extends UmbAction<UmbBlockActionAr
* @returns {Promise<string | undefined>}
*/
getValidationDataPath(): Promise<string | undefined>;
/**
* An optional reactive observable for the validation data path.
* When provided, the default kind element subscribes to it and updates the validation
* state controller reactively, rather than resolving `getValidationDataPath()` once at
* initialisation time.
*/
validationDataPath?: Observable<string | undefined>;
}
@@ -0,0 +1 @@
export const UMB_BLOCK_ACTION_DISCONNECT_FROM_ELEMENT_LIBRARY_ALIAS = 'Umb.BlockAction.DisconnectFromElementLibrary';
@@ -0,0 +1,12 @@
import type { MetaBlockActionDefaultKind } from '../../default/types.js';
import { UmbBlockActionBase } from '../../block-action-base.js';
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
export class UmbDisconnectFromElementLibraryBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
override async execute() {
const context = await this.getContext(UMB_BLOCK_ENTRY_CONTEXT);
await context?.requestDisconnectFromExternalContent();
}
}
export { UmbDisconnectFromElementLibraryBlockAction as api };
@@ -0,0 +1,30 @@
import {
UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
} from '../../../conditions/constants.js';
import { UMB_BLOCK_ACTION_DISCONNECT_FROM_ELEMENT_LIBRARY_ALIAS } from './constants.js';
export const manifests: Array<UmbExtensionManifest> = [
{
type: 'blockAction',
kind: 'default',
alias: UMB_BLOCK_ACTION_DISCONNECT_FROM_ELEMENT_LIBRARY_ALIAS,
name: 'Disconnect Block From Element Library Action',
weight: 250,
api: () => import('./disconnect-from-element-library-block.action.js'),
meta: {
icon: 'icon-unlink',
label: '#blockEditor_disconnectFromElementLibrary',
},
conditions: [
{
alias: UMB_BLOCK_ENTRY_IS_READ_ONLY_CONDITION_ALIAS,
match: false,
},
{
alias: UMB_BLOCK_ENTRY_HAS_EXTERNAL_CONTENT_CONDITION_ALIAS,
match: true,
},
],
},
];
@@ -4,16 +4,19 @@ import { UmbBlockActionBase } from '../../block-action-base.js';
import { UmbDataPathBlockElementDataQuery } from '../../../validation/data-path-element-data-query.function.js';
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
/**
* Block action that navigates to the block's content editor workspace.
* Exposes the workspace edit path via `getHref()` and the content validation data path via `getValidationDataPath()`.
*/
/** Block action that navigates to the block's content editor workspace. */
export class UmbEditContentBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
#context?: typeof UMB_BLOCK_ENTRY_CONTEXT.TYPE;
#contextReady: Promise<void>;
#resolveContext!: () => void;
readonly #href = new UmbStringState(undefined);
readonly href = this.#href.asObservable();
readonly #validationDataPath = new UmbStringState(undefined);
readonly validationDataPath = this.#validationDataPath.asObservable();
constructor(host: UmbControllerHost, args: UmbBlockActionArgs<MetaBlockActionDefaultKind>) {
super(host, args);
@@ -22,22 +25,31 @@ export class UmbEditContentBlockAction extends UmbBlockActionBase<MetaBlockActio
});
this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => {
this.#context = context;
if (!context) return;
this.#resolveContext();
this.observe(context.workspaceEditContentPath, (path) => this.#href.setValue(path || undefined), 'observeHref');
this.observe(
context.contentKey,
(contentKey) => {
this.#validationDataPath.setValue(
contentKey ? `$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]` : undefined,
);
},
'observeValidationDataPath',
);
});
}
override async getHref() {
await this.#contextReady;
const path = await this.observe(this.#context?.workspaceEditContentPath)?.asPromise();
return path || undefined;
return (await this.observe(this.href)?.asPromise()) || undefined;
}
override async getValidationDataPath() {
await this.#contextReady;
const contentKey = await this.observe(this.#context?.contentKey)?.asPromise();
if (!contentKey) return undefined;
return `$.contentData[${UmbDataPathBlockElementDataQuery({ key: contentKey })}]`;
return await this.observe(this.validationDataPath)?.asPromise();
}
}
@@ -3,17 +3,20 @@ import type { UmbBlockActionArgs } from '../../types.js';
import { UmbBlockActionBase } from '../../block-action-base.js';
import { UmbDataPathBlockElementDataQuery } from '../../../validation/data-path-element-data-query.function.js';
import { UMB_BLOCK_ENTRY_CONTEXT } from '../../../context/block-entry.context-token.js';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
/**
* Block action that navigates to the block's settings editor workspace.
* Exposes the workspace edit path via `getHref()` and the settings validation data path via `getValidationDataPath()`.
*/
/** Block action that navigates to the block's settings editor workspace. */
export class UmbEditSettingsBlockAction extends UmbBlockActionBase<MetaBlockActionDefaultKind> {
#context?: typeof UMB_BLOCK_ENTRY_CONTEXT.TYPE;
#contextReady: Promise<void>;
#resolveContext!: () => void;
readonly #href = new UmbStringState(undefined);
readonly href = this.#href.asObservable();
readonly #validationDataPath = new UmbStringState(undefined);
readonly validationDataPath = this.#validationDataPath.asObservable();
constructor(host: UmbControllerHost, args: UmbBlockActionArgs<MetaBlockActionDefaultKind>) {
super(host, args);
@@ -22,22 +25,31 @@ export class UmbEditSettingsBlockAction extends UmbBlockActionBase<MetaBlockActi
});
this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => {
this.#context = context;
if (!context) return;
this.#resolveContext();
this.observe(context.workspaceEditSettingsPath, (path) => this.#href.setValue(path || undefined), 'observeHref');
this.observe(
context.settingsKey,
(settingsKey) => {
this.#validationDataPath.setValue(
settingsKey ? `$.settingsData[${UmbDataPathBlockElementDataQuery({ key: settingsKey })}]` : undefined,
);
},
'observeValidationDataPath',
);
});
}
override async getHref() {
await this.#contextReady;
const path = await this.observe(this.#context?.workspaceEditSettingsPath)?.asPromise();
return path || undefined;
return (await this.observe(this.href)?.asPromise()) || undefined;
}
override async getValidationDataPath() {
await this.#contextReady;
const settingsKey = await this.observe(this.#context?.settingsKey)?.asPromise();
if (!settingsKey) return undefined;
return `$.settingsData[${UmbDataPathBlockElementDataQuery({ key: settingsKey })}]`;
return await this.observe(this.validationDataPath)?.asPromise();
}
}
@@ -0,0 +1 @@
export const UMB_BLOCK_ACTION_TRANSFER_TO_ELEMENT_LIBRARY_ALIAS = 'Umb.BlockAction.TransferToElementLibrary';

Some files were not shown because too many files have changed in this diff Show More