owainwilliams_umbPublisher/services/DocumentService.ts
2026-06-15 09:16:04 +01:00

502 lines
No EOL
18 KiB
TypeScript

import { UmbracoApiService } from './UmbracoApiService';
import { MediaService } from './MediaService';
import { GenerateGuid } from '../methods/generateGuid';
import { Notice, TFile, App, Vault } from 'obsidian';
import { umbpublisherSettings, UmbracoDocType, UmbracoProperty } from '../types/index';
export interface CreateDocumentRequest {
id: string;
parent: { id: string } | null;
documentType: { id: string };
template: { id: string } | null;
values: Array<{
editorAlias?: string;
alias: string;
value: unknown;
culture: string | null;
segment: string | null;
entityType?: string;
}>;
variants: Array<{
culture: string | null;
segment: string | null;
state: string | null;
name: string;
publishDate: string | null;
createDate: string | null;
updateDate: string | null;
scheduledPublishDate: string | null;
scheduledUnpublishDate: string | null;
}>;
}
export class DocumentService {
private mediaService: MediaService;
constructor(private apiService: UmbracoApiService, private app: App) {
this.mediaService = new MediaService(apiService);
}
async processImagesInContent(content: string, vault: Vault): Promise<{ content: string; uploadedImages: string[] }> {
const imageRegex = /!\[\[([^\]]+)\]\]/g;
const uploadedImages: string[] = [];
let processedContent = content;
const matches = Array.from(processedContent.matchAll(imageRegex));
for (const match of matches) {
const imageName = match[1];
try {
// Get all files in the vault to find the image
const files = vault.getFiles();
const imageFile = files.find((f: TFile) =>
f.name === imageName ||
f.path === imageName ||
f.path.endsWith('/' + imageName)
);
if (!imageFile) {
continue;
}
const arrayBuffer = await vault.adapter.readBinary(imageFile.path);
const fileName = imageFile.name;
const obsidianFolderId = await this.mediaService.getOrCreateObsidianFolder();
const mediaId = await this.mediaService.uploadImage(arrayBuffer, fileName, obsidianFolderId);
// Get the media URL
const mediaUrl = await this.mediaService.getMediaUrl(mediaId);
uploadedImages.push(mediaId);
// Replace markdown image with HTML img tag using the media URL
const replacement = `<img src="${mediaUrl}" alt="${imageName}" />`;
processedContent = processedContent.replace(match[0], replacement);
} catch {
// Image not found or upload failed — skip and continue
}
}
return { content: processedContent, uploadedImages };
}
async createDocument(
docTypeId: string,
title: string,
content: string,
parentId: string | null,
titleAlias: string,
contentAlias: string,
sourceFile?: TFile,
settings?: umbpublisherSettings,
tags: string[] = []
): Promise<unknown> {
let processedContent = content;
if (sourceFile) {
// Pass the app.vault instead of sourceFile
const { content: processed } = await this.processImagesInContent(content, this.app.vault);
processedContent = processed;
}
const documentId = await GenerateGuid();
// Branch based on content mode
if (settings?.contentMode === 'blockList' || settings?.contentMode === 'blockGrid') {
return this.createDocumentWithBlocks(
documentId,
docTypeId,
title,
processedContent,
parentId,
settings,
tags
);
} else {
return this.createDocumentLegacy(
documentId,
docTypeId,
title,
processedContent,
parentId,
titleAlias,
contentAlias,
settings?.tagsAlias || '',
tags
);
}
}
/**
* Legacy method: Creates document with direct markdown property
*/
private async createDocumentLegacy(
documentId: string,
docTypeId: string,
title: string,
content: string,
parentId: string | null,
titleAlias: string,
contentAlias: string,
tagsAlias: string = '',
tags: string[] = []
): Promise<unknown> {
// First, get the document type details to understand the property structure
const docTypeDetails = await this.apiService.callApi<UmbracoDocType>(`/umbraco/management/api/v1/document-type/${docTypeId}`);
if (!docTypeDetails) {
throw new Error('Failed to fetch document type details');
}
// Build the values array with proper editor aliases
const values: CreateDocumentRequest['values'] = [];
// Collect all properties: direct + from compositions
let properties: UmbracoProperty[] = docTypeDetails.properties || [];
if (docTypeDetails.compositions) {
for (const comp of docTypeDetails.compositions) {
if (comp.properties) {
properties = properties.concat(comp.properties);
}
const compDocTypeId = comp.documentType?.id || comp.id;
if (compDocTypeId) {
const compDetails = await this.apiService.callApi<UmbracoDocType>(`/umbraco/management/api/v1/document-type/${compDocTypeId}`);
if (compDetails?.properties) {
properties = properties.concat(compDetails.properties);
}
}
}
}
const titleProperty = properties.find((p) => p.alias === titleAlias);
const contentProperty = properties.find((p) => p.alias === contentAlias);
// If we can't find the properties, let's try a simpler approach
if (!titleProperty || !contentProperty) {
// Add title property with fallback
values.push({
editorAlias: 'Umbraco.TextBox',
alias: titleAlias,
value: title || "",
culture: null,
segment: null
});
// Add content property with fallback
values.push({
editorAlias: 'Umbraco.MarkdownEditor',
alias: contentAlias,
value: content || "",
culture: null,
segment: null
});
} else {
// Add title property
values.push({
editorAlias: titleProperty.dataType?.editorAlias || 'Umbraco.TextBox',
alias: titleAlias,
value: title || "",
culture: null,
segment: null
});
// Add content property
values.push({
editorAlias: contentProperty.dataType?.editorAlias || 'Umbraco.MarkdownEditor',
alias: contentAlias,
value: content || "",
culture: null,
segment: null
});
}
// Add default values for other properties if we found them
if (properties.length > 0) {
properties.forEach((prop) => {
if (prop.alias !== titleAlias && prop.alias !== contentAlias) {
const editorAlias = prop.dataType?.editorAlias;
// Add common default properties
if (prop.alias === 'isIndexable' || prop.alias === 'isFollowable') {
values.push({
editorAlias: 'Umbraco.TrueFalse',
alias: prop.alias,
value: true,
culture: null,
segment: null
});
} else if (prop.alias === 'hideFromTopNavigation' || prop.alias === 'umbracoNaviHide' || prop.alias === 'hideFromXMLSitemap') {
values.push({
editorAlias: 'Umbraco.TrueFalse',
alias: prop.alias,
value: false,
culture: null,
segment: null
});
} else if (prop.alias === 'articleDate' && editorAlias === 'Umbraco.DateTime') {
values.push({
editorAlias: 'Umbraco.DateTime',
entityType: 'document-property-value',
culture: null,
segment: null,
alias: prop.alias,
value: new Date().toISOString().replace('T', ' ').substring(0, 19)
});
}
}
});
}
// Add tags property if configured and tags exist
if (tagsAlias && tags.length > 0) {
values.push({
editorAlias: 'Umbraco.Tags',
alias: tagsAlias,
value: tags,
culture: null,
segment: null
});
}
const documentRequest: CreateDocumentRequest = {
id: documentId,
parent: parentId && parentId.trim() !== '' && parentId !== 'null'
? { id: parentId }
: null,
documentType: { id: docTypeId },
template: null,
values: values,
variants: [
{
culture: null,
segment: null,
state: null,
name: title || "Untitled",
publishDate: null,
createDate: null,
updateDate: null,
scheduledPublishDate: null,
scheduledUnpublishDate: null
}
]
};
try {
// Create document directly without validation
const createResponse = await this.apiService.callApi(
'/umbraco/management/api/v1/document',
'POST',
documentRequest
);
if (!createResponse) {
throw new Error('Document creation failed - no response received');
}
return createResponse;
} catch (error) {
if (error.message && error.message.includes('400')) {
throw new Error(`Invalid request data. Check:\n1. Document type ID '${docTypeId}' exists\n2. Property aliases '${titleAlias}' and '${contentAlias}' are correct\n3. Parent node ID '${parentId}' is valid\n\nOriginal error: ${error.message}`);
}
throw error;
}
}
/**
* Creates document with Block List or Block Grid structure
*/
private async createDocumentWithBlocks(
documentId: string,
docTypeId: string,
title: string,
content: string,
parentId: string | null,
settings: umbpublisherSettings,
tags: string[] = []
): Promise<unknown> {
const isBlockGrid = settings.contentMode === 'blockGrid';
const modeLabel = isBlockGrid ? 'Block Grid' : 'Block List';
// Generate GUIDs for block structure
const elementUdi = await GenerateGuid();
const elementTypeKey = settings.blockElementTypeId;
// Build layout item based on mode
const layoutKey = isBlockGrid ? 'Umbraco.BlockGrid' : 'Umbraco.BlockList';
const layoutItem = isBlockGrid
? {
contentUdi: `umb://element/${elementUdi}`,
areas: [],
columnSpan: 12,
rowSpan: 1
}
: {
contentUdi: `umb://element/${elementUdi}`
};
// Build block JSON structure
const blockValue = {
layout: {
[layoutKey]: [layoutItem]
},
contentData: [
{
contentTypeKey: elementTypeKey,
udi: `umb://element/${elementUdi}`,
[settings.blockContentPropertyAlias]: content
}
],
settingsData: []
};
// Fetch document type to get property details
const docTypeDetails = await this.apiService.callApi<UmbracoDocType>(`/umbraco/management/api/v1/document-type/${docTypeId}`);
if (!docTypeDetails) {
throw new Error('Failed to fetch document type details');
}
// Collect all properties: direct + from compositions
let properties: UmbracoProperty[] = docTypeDetails.properties || [];
if (docTypeDetails.compositions) {
for (const comp of docTypeDetails.compositions) {
if (comp.properties) {
properties = properties.concat(comp.properties);
}
const compDocTypeId = comp.documentType?.id || comp.id;
if (compDocTypeId) {
const compDetails = await this.apiService.callApi<UmbracoDocType>(`/umbraco/management/api/v1/document-type/${compDocTypeId}`);
if (compDetails?.properties) {
properties = properties.concat(compDetails.properties);
}
}
}
}
// Find the BlockList property
const blockListProperty = properties.find((p) => p.alias === settings.blockPropertyAlias);
if (!blockListProperty) {
throw new Error(`BlockList property '${settings.blockPropertyAlias}' not found on document type.`);
}
// Build values array
const editorAlias = isBlockGrid ? 'Umbraco.BlockGrid' : 'Umbraco.BlockList';
const values: CreateDocumentRequest['values'] = [
{
editorAlias: editorAlias,
alias: settings.blockPropertyAlias,
value: blockValue,
culture: null,
segment: null
}
];
// Add other required properties (title, etc.)
const titleProperty = properties.find((p) => p.alias === settings.titleAlias);
if (titleProperty) {
values.push({
editorAlias: titleProperty.dataType?.editorAlias || 'Umbraco.TextBox',
alias: settings.titleAlias,
value: title || "",
culture: null,
segment: null
});
}
// Add default boolean and date properties
properties.forEach((prop) => {
if (prop.alias !== settings.blockPropertyAlias && prop.alias !== settings.titleAlias) {
const propEditorAlias = prop.dataType?.editorAlias;
if (prop.alias === 'isIndexable' || prop.alias === 'isFollowable') {
values.push({
editorAlias: 'Umbraco.TrueFalse',
alias: prop.alias,
value: true,
culture: null,
segment: null
});
} else if (prop.alias === 'hideFromTopNavigation' || prop.alias === 'umbracoNaviHide' || prop.alias === 'hideFromXMLSitemap') {
values.push({
editorAlias: 'Umbraco.TrueFalse',
alias: prop.alias,
value: false,
culture: null,
segment: null
});
} else if (prop.alias === 'articleDate' && propEditorAlias === 'Umbraco.DateTime') {
values.push({
editorAlias: 'Umbraco.DateTime',
entityType: 'document-property-value',
culture: null,
segment: null,
alias: prop.alias,
value: new Date().toISOString().replace('T', ' ').substring(0, 19)
});
}
}
});
// Add tags property if configured and tags exist
if (settings.tagsAlias && tags.length > 0) {
values.push({
editorAlias: 'Umbraco.Tags',
alias: settings.tagsAlias,
value: tags,
culture: null,
segment: null
});
}
const documentRequest: CreateDocumentRequest = {
id: documentId,
parent: parentId && parentId.trim() !== '' && parentId !== 'null'
? { id: parentId }
: null,
documentType: { id: docTypeId },
template: null,
values: values,
variants: [
{
culture: null,
segment: null,
state: null,
name: title || "Untitled",
publishDate: null,
createDate: null,
updateDate: null,
scheduledPublishDate: null,
scheduledUnpublishDate: null
}
]
};
try {
const createResponse = await this.apiService.callApi(
'/umbraco/management/api/v1/document',
'POST',
documentRequest
);
if (!createResponse) {
throw new Error('Document creation failed - no response received');
}
return createResponse;
} catch (error) {
if (error.message && error.message.includes('400')) {
throw new Error(`Invalid ${modeLabel} request. Check element type and property configuration.\n\nOriginal error: ${error.message}`);
}
throw error;
}
}
}