up-doc-action.ts
The entity action class that handles the “Create Document from Source” menu click.
What it does
Section titled “What it does”When a user clicks “Create from Source” in the collection view, this action:
- Gets the parent document’s document type
- Discovers allowed child document types and their blueprints (grouped by document type)
- Fetches active workflows and filters blueprints to only those with complete workflows
- Opens the blueprint picker dialog — user selects a document type, then a blueprint
- Opens the source sidebar modal — passes the selected
blueprintIdso the modal can fetch the config and extract content - Receives
extractedSections(element ID → text lookup) andconfigfrom the modal return value - Scaffolds from the selected blueprint to get default values
- Loops over
config.map.mappingsto apply each mapping using path-based targeting, with first-write-replaces and subsequent-writes-concatenate semantics - Creates a new document via the Management API
- Saves the document to properly persist it and trigger cache updates
- Shows success/error notifications
- Navigates to the newly created document
Class structure
Section titled “Class structure”export class UpDocEntityAction extends UmbEntityActionBase<never> { #documentTypeStructureRepository = new UmbDocumentTypeStructureRepository(this); #blueprintItemRepository = new UmbDocumentBlueprintItemRepository(this); #documentItemRepository = new UmbDocumentItemRepository(this);
constructor(host: UmbControllerHost, args: UmbEntityActionArgs<never>) { super(host, args); }
override async execute() { // Opens modals, discovers blueprints, creates document }}Key concepts
Section titled “Key concepts”Modal handling with cancellation
Section titled “Modal handling with cancellation”The modal can be cancelled (clicking outside or Close button), which throws an error. This is handled gracefully:
let modalValue;try { modalValue = await umbOpenModal(this, UMB_UP_DOC_MODAL, { data: { unique: parentUnique, documentTypeName: selectedDocType?.documentTypeName ?? '', blueprintName: selectedBlueprint?.blueprintName ?? '', blueprintId: blueprintUnique, }, });} catch { // Modal was cancelled return;}Destructuring modal return value
Section titled “Destructuring modal return value”The modal returns extractedSections (a Record<string, string> keyed by element IDs like p1-e2) and config (the full DocumentTypeConfig):
const { name, mediaUnique, extractedSections, config } = modalValue;
if (!mediaUnique || !name || !config) { return;}Blueprint filtering via active workflows
Section titled “Blueprint filtering via active workflows”Before opening the blueprint picker, the action fetches the list of active workflows and filters out blueprints that don’t have complete workflows:
const authContext = await this.getContext(UMB_AUTH_CONTEXT);const token = await authContext.getLatestToken();
const activeWorkflows = await fetchActiveWorkflows(token);const activeBlueprintIds = new Set(activeWorkflows.blueprintIds);
// Only include blueprints that have complete workflowsconst workflowBlueprints = blueprints.filter((bp) => activeBlueprintIds.has(bp.unique));This means editors only see blueprints where an admin has fully configured the workflow (destination + map + at least one source). If no workflows match any allowed child type, a warning notification is shown and the action exits early.
Config-driven mapping loop with field tracking
Section titled “Config-driven mapping loop with field tracking”The action loops over config.map.mappings and applies each mapping. A mappedFields Set tracks which destination fields have already been written:
const mappedFields = new Set<string>();
for (const mapping of config.map.mappings) { if (mapping.enabled === false) continue;
const sectionValue = extractedSections[mapping.source]; if (!sectionValue) continue;
for (const dest of mapping.destinations) { this.#applyDestinationMapping(values, dest, sectionValue, config, mappedFields); }}Multi-element field handling
Section titled “Multi-element field handling”When multiple source elements map to the same destination field (e.g., a title split across two PDF lines), the mappedFields Set controls the behavior:
- First write to a field: replaces the blueprint default value
- Subsequent writes to the same field: concatenates with a space separator
if (existing) { if (mappedFields.has(alias)) { // Already written — concatenate const currentValue = typeof existing.value === 'string' ? existing.value : ''; existing.value = `${currentValue} ${transformedValue}`; } else { // First write — replace the blueprint default existing.value = transformedValue; }} else { values.push({ alias, value: transformedValue });}mappedFields.add(alias);This prevents blueprint defaults from being prepended to mapped values while still allowing multi-element concatenation.
Destination mapping with block disambiguation
Section titled “Destination mapping with block disambiguation”The #applyDestinationMapping method handles three cases:
- Block property with
blockKey— looks up the specific block instance indestination.jsonby key, retrieves itsidentifyBymatcher, then calls#applyBlockGridValueto find and update the correct block in the scaffold - Simple field — direct property alias (e.g.,
"pageTitle") - Dot-path block property (legacy) —
"gridKey.blockKey.propertyKey"format for backwards compatibility
#applyDestinationMapping(values, dest, sectionValue, config, mappedFields) { // 1. Block property with blockKey — find specific block instance if (dest.blockKey) { // Look up block in destination config by key → get identifyBy → apply }
// 2. Simple field: "pageTitle" if (pathParts.length === 1) { ... }
// 3. Legacy dot-path: "contentGrid.itineraryBlock.richTextContent" if (pathParts.length === 3) { ... }}See Mapping Directions for details on how block disambiguation works in each mapping direction.
Block grid value application
Section titled “Block grid value application”The #applyBlockGridValue method finds a block within a block grid by searching for a property value match, then writes the extracted content. It uses mappedFields with a compound key (${block.key}:${targetProperty}) to track writes — first write replaces the blueprint default, subsequent writes concatenate with newline:
#applyBlockGridValue( values: Array<{ alias: string; value: unknown }>, gridAlias: string, blockSearch: { property: string; value: string }, targetProperty: string, value: string, convertMarkdown: boolean | undefined, mappedFields: Set<string>) { // Find block by identifyBy matcher, then: const fieldKey = `${block.key}:${targetProperty}`; if (mappedFields.has(fieldKey)) { // Concatenate with newline targetValue.value = `${currentValue}\n${value}`; } else { // First write — replace blueprint default targetValue.value = value; } mappedFields.add(fieldKey);}This allows multiple source elements mapped to the same block property (e.g., 12 bullet points → one rich text field) to assemble into a single concatenated value.
Content format conversion
Section titled “Content format conversion”After all mappings are applied, #convertRichTextFields processes field values based on the destination field type (from destination.json):
richTextfields: Markdown is converted to HTML usingmarkdownToHtmland wrapped inbuildRteValuetext/textAreafields: Markdown formatting is stripped usingstripMarkdown(removes heading prefixes like#, bold markers, bullet prefixes, etc.)
This applies to both top-level document properties and block-level properties within the block grid.
Scaffolding from blueprint
Section titled “Scaffolding from blueprint”The scaffold endpoint returns the blueprint’s default values including pre-populated block grids:
const scaffoldResponse = await fetch( `/umbraco/management/api/v1/document-blueprint/${blueprintUnique}/scaffold`, { headers: { Authorization: `Bearer ${token}` } });const scaffold = await scaffoldResponse.json();Authentication for API calls
Section titled “Authentication for API calls”Umbraco uses bearer token authentication for Management API calls:
const authContext = await this.getContext(UMB_AUTH_CONTEXT);const token = await authContext.getLatestToken();
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, },});Imports
Section titled “Imports”import { UMB_UP_DOC_MODAL } from './up-doc-modal.token.js';import { UMB_BLUEPRINT_PICKER_MODAL } from './blueprint-picker-modal.token.js';import type { DocumentTypeOption } from './blueprint-picker-modal.token.js';import type { DocumentTypeConfig, MappingDestination } from './workflow.types.js';import { fetchActiveWorkflows } from './workflow.service.js';import { markdownToHtml, buildRteValue, stripMarkdown } from './transforms.js';import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';import { umbOpenModal } from '@umbraco-cms/backoffice/modal';import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';import { UmbDocumentTypeStructureRepository } from '@umbraco-cms/backoffice/document-type';import { UmbDocumentBlueprintItemRepository } from '@umbraco-cms/backoffice/document-blueprint';import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document';Data flow
Section titled “Data flow”this.args.unique— Parent document ID (from the collection view context)- Action fetches parent document to get its document type unique
- Action discovers allowed child document types and their blueprints
- Fetches active workflows, filters blueprints to only those with complete workflows
- Blueprint picker dialog returns
{ blueprintUnique, documentTypeUnique } - Source sidebar modal receives
{ unique, documentTypeName, blueprintName, blueprintId }and returns{ name, mediaUnique, extractedSections, config } - Scaffolds from selected blueprint to get default values
- Loops over
config.map.mappings, applying each to scaffold values (first write replaces, subsequent writes concatenate) - POSTs to create document API
- Fetches and saves the document to properly persist it
- Shows notification and navigates to the new document
Navigation after creation
Section titled “Navigation after creation”After successfully creating the document, the action navigates to the new document with a short delay:
if (newDocumentId) { const newPath = `/umbraco/section/content/workspace/document/edit/${newDocumentId}`; setTimeout(() => { window.location.href = newPath; }, 150);}The delay helps avoid race condition errors with Block Preview that can occur during rapid navigation.
Default export
Section titled “Default export”The class is exported both as named and default export — the default export is what the manifest’s api loader expects.