feat: add image management functionality
- Implemented image folder repository with CRUD operations. - Added image reference repository for managing image references to articles. - Created image repository for handling image assets, including listing, inserting, updating, and deleting images. - Introduced image usage repository to track storage usage and quotas for tenants. - Added SQL queries for image assets, folders, references, and usage. - Developed image handler for HTTP endpoints to manage images and folders. - Created database migration scripts for image-related tables and structures.
This commit is contained in:
@@ -31,6 +31,7 @@ type RichImageBlockFeatureConfig = {
|
||||
type ParsedImageMarkup = {
|
||||
src: string;
|
||||
caption: string;
|
||||
assetId: number;
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: number;
|
||||
@@ -42,6 +43,7 @@ type RichImageBlockNode = {
|
||||
url: string;
|
||||
title: string;
|
||||
alt: string;
|
||||
assetId: number;
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: number;
|
||||
@@ -74,6 +76,14 @@ function parseRatio(value: unknown): number {
|
||||
return Number(parsed.toFixed(2));
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: unknown): number {
|
||||
const parsed = Number.parseInt(String(value ?? "").trim(), 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function escapeHtmlAttribute(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
@@ -119,6 +129,7 @@ function parseImageMarkup(markup: string): ParsedImageMarkup | null {
|
||||
return {
|
||||
src,
|
||||
caption: (image.getAttribute("title") || image.getAttribute("alt") || "").trim(),
|
||||
assetId: parsePositiveInteger(image.getAttribute("data-asset-id")),
|
||||
width: parsePositiveNumber(image.getAttribute("width")),
|
||||
height: parsePositiveNumber(image.getAttribute("height")),
|
||||
ratio: parseRatio(image.getAttribute("data-ratio")),
|
||||
@@ -132,12 +143,14 @@ function serializeImageHtml(attrs: Record<string, unknown>): string {
|
||||
const width = parsePositiveNumber(attrs.width);
|
||||
const height = parsePositiveNumber(attrs.height);
|
||||
const ratio = parseRatio(attrs.ratio);
|
||||
const assetId = parsePositiveInteger(attrs.assetId);
|
||||
const align = clampImageAlign(attrs.align);
|
||||
const encodedCaption = escapeHtmlAttribute(caption);
|
||||
|
||||
const widthAttribute = width > 0 ? ` width="${Math.round(width)}"` : "";
|
||||
const heightAttribute = height > 0 ? ` height="${Math.round(height)}"` : "";
|
||||
const ratioAttribute = width === 0 && height === 0 && ratio !== 1 ? ` data-ratio="${ratio.toFixed(2)}"` : "";
|
||||
const assetIdAttribute = assetId > 0 ? ` data-asset-id="${assetId}"` : "";
|
||||
|
||||
return (
|
||||
`<p class="article-editor-image article-editor-image--${align}" align="${align}">` +
|
||||
@@ -147,6 +160,7 @@ function serializeImageHtml(attrs: Record<string, unknown>): string {
|
||||
widthAttribute +
|
||||
heightAttribute +
|
||||
ratioAttribute +
|
||||
assetIdAttribute +
|
||||
" />" +
|
||||
"</p>"
|
||||
);
|
||||
@@ -155,8 +169,9 @@ function serializeImageHtml(attrs: Record<string, unknown>): string {
|
||||
function needsHtmlSerialization(node: { attrs: Record<string, unknown> }): boolean {
|
||||
const width = parsePositiveNumber(node.attrs.width);
|
||||
const height = parsePositiveNumber(node.attrs.height);
|
||||
const assetId = parsePositiveInteger(node.attrs.assetId);
|
||||
const align = clampImageAlign(node.attrs.align);
|
||||
return width > 0 || height > 0 || align !== "center";
|
||||
return width > 0 || height > 0 || align !== "center" || assetId > 0;
|
||||
}
|
||||
|
||||
function toRichImageBlockNode(parsed: ParsedImageMarkup): RichImageBlockNode {
|
||||
@@ -165,6 +180,7 @@ function toRichImageBlockNode(parsed: ParsedImageMarkup): RichImageBlockNode {
|
||||
url: parsed.src,
|
||||
title: parsed.caption,
|
||||
alt: parsed.ratio === 1 ? "" : parsed.ratio.toFixed(2),
|
||||
assetId: parsed.assetId,
|
||||
width: parsed.width,
|
||||
height: parsed.height,
|
||||
ratio: parsed.ratio,
|
||||
@@ -181,6 +197,7 @@ function transformRichImageNode(node: Record<string, unknown>): Record<string, u
|
||||
url: String(firstChild.url ?? ""),
|
||||
title: String(firstChild.title ?? ""),
|
||||
alt: String(firstChild.alt ?? ""),
|
||||
assetId: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
ratio: parseRatio(firstChild.alt),
|
||||
@@ -226,6 +243,7 @@ export const richImageBlockSchema = $nodeSchema("image-block", () => ({
|
||||
attrs: {
|
||||
src: { default: "", validate: "string" },
|
||||
caption: { default: "", validate: "string" },
|
||||
assetId: { default: 0, validate: "number" },
|
||||
ratio: { default: 1, validate: "number" },
|
||||
width: { default: 0, validate: "number" },
|
||||
height: { default: 0, validate: "number" },
|
||||
@@ -242,6 +260,7 @@ export const richImageBlockSchema = $nodeSchema("image-block", () => ({
|
||||
return {
|
||||
src: dom.getAttribute("src") || "",
|
||||
caption: dom.getAttribute("title") || dom.getAttribute("alt") || "",
|
||||
assetId: parsePositiveInteger(dom.getAttribute("data-asset-id")),
|
||||
ratio: parseRatio(dom.getAttribute("data-ratio") || dom.getAttribute("ratio")),
|
||||
width: parsePositiveNumber(dom.getAttribute("width")),
|
||||
height: parsePositiveNumber(dom.getAttribute("height")),
|
||||
@@ -258,6 +277,7 @@ export const richImageBlockSchema = $nodeSchema("image-block", () => ({
|
||||
alt: node.attrs.caption,
|
||||
title: node.attrs.caption,
|
||||
align: node.attrs.align,
|
||||
"data-asset-id": parsePositiveInteger(node.attrs.assetId) || undefined,
|
||||
width: node.attrs.width || undefined,
|
||||
height: node.attrs.height || undefined,
|
||||
"data-ratio": node.attrs.ratio,
|
||||
@@ -269,6 +289,7 @@ export const richImageBlockSchema = $nodeSchema("image-block", () => ({
|
||||
state.addNode(type, {
|
||||
src: String((node as RichImageBlockNode).url ?? ""),
|
||||
caption: String((node as RichImageBlockNode).title ?? ""),
|
||||
assetId: parsePositiveInteger((node as RichImageBlockNode).assetId),
|
||||
ratio: parseRatio((node as RichImageBlockNode).ratio || (node as RichImageBlockNode).alt),
|
||||
width: parsePositiveNumber((node as RichImageBlockNode).width),
|
||||
height: parsePositiveNumber((node as RichImageBlockNode).height),
|
||||
|
||||
Reference in New Issue
Block a user