Skip to content

Commit

Permalink
feat: add content copy feature
Browse files Browse the repository at this point in the history
  • Loading branch information
ruibaby committed Oct 20, 2024
1 parent a05b0ec commit 520e10e
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 190 deletions.
2 changes: 1 addition & 1 deletion ui/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

export {};

declare module "axios" {
declare module 'axios' {
export interface AxiosRequestConfig {
mute?: boolean;
}
Expand Down
8 changes: 7 additions & 1 deletion ui/prettier.config.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
module.exports = {
plugins: [require("prettier-plugin-organize-imports")],
plugins: [require('prettier-plugin-organize-imports')],
trailingComma: 'es5',
tabWidth: 2,
singleQuote: true,
bracketSpacing: true,
arrowParens: 'always',
printWidth: 100,
};
7 changes: 2 additions & 5 deletions ui/src/class/contentConverter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import {
convertPostContentToHTML,
convertPostContentToMarkdown,
} from "@/utils/markdown";
import type { ContentWrapper, Post } from "@halo-dev/api-client";
import { convertPostContentToHTML, convertPostContentToMarkdown } from '@/utils/markdown';
import type { ContentWrapper, Post } from '@halo-dev/api-client';

export abstract class ContentConverter {
abstract convert(post: Post, content: ContentWrapper): string;
Expand Down
37 changes: 17 additions & 20 deletions ui/src/class/contentExporter.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
import { ContentTypes } from "@/constants";
import { downloadContent } from "@/utils";
import { consoleApiClient, type Post } from "@halo-dev/api-client";
import { ConverterFactory } from "./converterFactory";
import { ContentTypes } from '@/constants';
import { downloadContent } from '@/utils';
import { consoleApiClient, type Post } from '@halo-dev/api-client';
import { ConverterFactory } from './converterFactory';

type ExportType = "original" | "markdown";
type ExportType = 'original' | 'markdown';

export class ContentExporter {
static async export(post: Post, exportType: ExportType): Promise<void> {
const { data: content } =
await consoleApiClient.content.post.fetchPostHeadContent({
name: post.metadata.name,
});
const { data: content } = await consoleApiClient.content.post.fetchPostHeadContent({
name: post.metadata.name,
});

let exportContent: string;
let fileExtension: string;

if (exportType === "original") {
exportContent = content.raw || "";
if (exportType === 'original') {
exportContent = content.raw || '';
fileExtension =
ContentTypes.find(
(type) => type.type === content.rawType?.toLowerCase(),
)?.extension || "";
} else if (exportType === "markdown") {
if (content.rawType?.toLowerCase() === "html") {
const converter = ConverterFactory.getConverter("html", "markdown");
ContentTypes.find((type) => type.type === content.rawType?.toLowerCase())?.extension || '';
} else if (exportType === 'markdown') {
if (content.rawType?.toLowerCase() === 'html') {
const converter = ConverterFactory.getConverter('html', 'markdown');
exportContent = converter.convert(post, content);
} else {
exportContent = content.raw || "";
exportContent = content.raw || '';
}
fileExtension = "md";
fileExtension = 'md';
} else {
throw new Error("Unsupported export type");
throw new Error('Unsupported export type');
}

downloadContent(exportContent, post.spec.title, fileExtension);
Expand Down
10 changes: 5 additions & 5 deletions ui/src/class/converterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {
ContentConverter,
HtmlToMarkdownConverter,
MarkdownToHtmlConverter,
} from "./contentConverter";
import ConverterManager from "./converterManager";
} from './contentConverter';
import ConverterManager from './converterManager';

export class ConverterFactory {
static getConverter(fromType: string, toType: string): ContentConverter {
Expand All @@ -13,12 +13,12 @@ export class ConverterFactory {
throw new Error(`Unsupported conversion from ${fromType} to ${toType}`);
}

if (fromType === "markdown" && toType === "html") {
if (fromType === 'markdown' && toType === 'html') {
return new MarkdownToHtmlConverter();
} else if (fromType === "html" && toType === "markdown") {
} else if (fromType === 'html' && toType === 'markdown') {
return new HtmlToMarkdownConverter();
}

throw new Error("Converter not implemented");
throw new Error('Converter not implemented');
}
}
4 changes: 2 additions & 2 deletions ui/src/class/converterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class ConverterManager {
}

private initializeConverters(): void {
this.addConverter("html", "markdown", "转换为 Markdown");
this.addConverter("markdown", "html", "转换为富文本");
this.addConverter('html', 'markdown', '转换为 Markdown');
this.addConverter('markdown', 'html', '转换为富文本');
}

private addConverter(fromType: string, toType: string, label: string): void {
Expand Down
41 changes: 16 additions & 25 deletions ui/src/class/postCloner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { randomUUID } from "@/utils/id";
import { randomUUID } from '@/utils/id';
import {
consoleApiClient,
type ContentWrapper,
type Post,
type PostRequest,
} from "@halo-dev/api-client";
import { Toast } from "@halo-dev/components";
import { cloneDeep, set } from "lodash-es";
} from '@halo-dev/api-client';
import { Toast } from '@halo-dev/components';
import { cloneDeep, set } from 'lodash-es';

class PostCloner {
static async clonePost(post: Post): Promise<void> {
Expand All @@ -19,39 +19,30 @@ class PostCloner {
postRequest: newPostData,
});

Toast.success("文章克隆成功,如果列表没有刷新,请手动刷新一次");
Toast.success('文章克隆成功,如果列表没有刷新,请手动刷新一次');
} catch (error) {
console.error("Failed to clone post", error);
Toast.error("克隆文章失败");
console.error('Failed to clone post', error);
Toast.error('克隆文章失败');
}
}

private static async fetchPostContent(
postName: string,
): Promise<ContentWrapper> {
private static async fetchPostContent(postName: string): Promise<ContentWrapper> {
const { data } = await consoleApiClient.content.post.fetchPostHeadContent({
name: postName,
});

return data;
}

private static prepareNewPostData(
originalPost: Post,
content: ContentWrapper,
): PostRequest {
private static prepareNewPostData(originalPost: Post, content: ContentWrapper): PostRequest {
const postToCreate = cloneDeep(originalPost);
set(postToCreate, "spec.baseSnapshot", "");
set(postToCreate, "spec.headSnapshot", "");
set(postToCreate, "spec.releaseSnapshot", "");
set(
postToCreate,
"spec.slug",
`${originalPost.spec.slug}-${randomUUID().split("-")[0]}`,
);
set(postToCreate, "spec.title", originalPost.spec.title + "(副本)");
set(postToCreate, "spec.publish", false);
set(postToCreate, "metadata", {
set(postToCreate, 'spec.baseSnapshot', '');
set(postToCreate, 'spec.headSnapshot', '');
set(postToCreate, 'spec.releaseSnapshot', '');
set(postToCreate, 'spec.slug', `${originalPost.spec.slug}-${randomUUID().split('-')[0]}`);
set(postToCreate, 'spec.title', originalPost.spec.title + '(副本)');
set(postToCreate, 'spec.publish', false);
set(postToCreate, 'metadata', {
name: randomUUID(),
});

Expand Down
102 changes: 102 additions & 0 deletions ui/src/class/postContentCopier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { copyHtmlAsRichText, copyText } from '@/utils/copy';
import { convertPostContentToMarkdown } from '@/utils/markdown';
import { consoleApiClient, type ContentWrapper, type Post } from '@halo-dev/api-client';
import { Toast } from '@halo-dev/components';

interface CopyOptions {
convertToMarkdown: boolean;
}

class PostContentCopier {
static async copyPostContent(post: Post, options: CopyOptions): Promise<void> {
try {
const { convertToMarkdown } = options;
const postContent = await this.fetchPostContent(post.metadata.name);

this.handleContentCopy(post, postContent, convertToMarkdown);
} catch (error) {
console.error('Failed to copy post content', error);
Toast.error('复制文章内容失败');
}
}

private static handleContentCopy(
post: Post,
postContent: ContentWrapper,
convertToMarkdown: boolean
) {
const { rawType, raw } = postContent;
const lowerCaseRawType = rawType?.toLowerCase();

switch (lowerCaseRawType) {
case 'html':
this.handleHtmlContent(post, postContent, convertToMarkdown);
break;
case 'markdown':
this.handleMarkdownContent(raw);
break;
default:
throw new Error(`Unsupported raw type: ${rawType}`);
}
}

private static handleHtmlContent(
post: Post,
postContent: ContentWrapper,
convertToMarkdown: boolean
) {
if (!convertToMarkdown) {
this.copyAsRichText(postContent.content);
} else {
const markdown = convertPostContentToMarkdown(post, postContent);
this.copyAsMarkdown(markdown);
}
}
private static handleMarkdownContent(raw: string | undefined): void {
this.copyAsMarkdown(raw || '');
}

private static copyAsRichText(content: string | undefined) {
if (!content) {
throw new Error('No content to copy');
}
copyHtmlAsRichText(this.processHTMLLinks(content));
Toast.success('文章内容已复制为富文本格式');
}

private static copyAsMarkdown(markdown: string) {
copyText(this.processMarkdownLinks(markdown));
Toast.success('文章内容已复制为 Markdown 文本');
}

private static processHTMLLinks(content: string): string {
const htmlLinkRegex = /(src|href)=["'](?!http:\/\/|https:\/\/|mailto:|tel:)([^"']+)["']/gi;
return content.replace(htmlLinkRegex, (_, attr, url) => {
return `${attr}="${this.getAbsoluteUrl(url)}"`;
});
}

private static processMarkdownLinks(content: string): string {
const markdownLinkRegex = /(!?\[.*?\]\()(?!http:\/\/|https:\/\/|mailto:|tel:)([^)]+)(\))/g;
return content.replace(markdownLinkRegex, (_, prefix, url, suffix) => {
return `${prefix}${this.getAbsoluteUrl(url)}${suffix}`;
});
}

private static getAbsoluteUrl(url: string): string {
if (url.startsWith('/')) {
return `${location.origin}${url}`;
} else {
return `${location.origin}/${url}`;
}
}

private static async fetchPostContent(name: string): Promise<ContentWrapper> {
const { data } = await consoleApiClient.content.post.fetchPostHeadContent({
name,
});
return data;
}
}

export default PostContentCopier;
51 changes: 18 additions & 33 deletions ui/src/class/postOperations.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import {
consoleApiClient,
coreApiClient,
type Post,
} from "@halo-dev/api-client";
import { Toast } from "@halo-dev/components";
import { AxiosError } from "axios";
import { ConverterFactory } from "./converterFactory";
import { consoleApiClient, coreApiClient, type Post } from '@halo-dev/api-client';
import { Toast } from '@halo-dev/components';
import { AxiosError } from 'axios';
import { ConverterFactory } from './converterFactory';

export class PostOperations {
static async convertContent(post: Post, toType: string): Promise<void> {
const { data: content } =
await consoleApiClient.content.post.fetchPostHeadContent({
name: post.metadata.name,
});
const { data: content } = await consoleApiClient.content.post.fetchPostHeadContent({
name: post.metadata.name,
});

if (!content.rawType) {
Toast.warning("原始类型未定义");
Toast.warning('原始类型未定义');
return;
}

Expand All @@ -24,24 +19,16 @@ export class PostOperations {
return;
}

const converter = ConverterFactory.getConverter(
content.rawType.toLowerCase(),
toType,
);
const converter = ConverterFactory.getConverter(content.rawType.toLowerCase(), toType);

const convertedRawContent = converter.convert(post, content);

try {
await this.updatePostContent(
post,
toType,
convertedRawContent,
content.content || "",
);
Toast.success("转换完成");
await this.updatePostContent(post, toType, convertedRawContent, content.content || '');
Toast.success('转换完成');
} catch (error) {
if (error instanceof AxiosError) {
Toast.error(error.response?.data.detail || "转换失败,请重试");
Toast.error(error.response?.data.detail || '转换失败,请重试');
}
}
}
Expand All @@ -50,7 +37,7 @@ export class PostOperations {
post: Post,
rawType: string,
raw: string,
content: string,
content: string
): Promise<void> {
const published = post.spec.publish;

Expand All @@ -75,21 +62,19 @@ export class PostOperations {
name: post.metadata.name,
jsonPatchInner: [
{
op: "add",
path: "/metadata/annotations/content.halo.run~1preferred-editor",
value: "",
op: 'add',
path: '/metadata/annotations/content.halo.run~1preferred-editor',
value: '',
},
],
},
{ mute: true },
{ mute: true }
);
success = true;
} catch (error) {
attempt++;
if (attempt < maxRetries) {
console.log(
`Optimistic lock error encountered. Retrying ${attempt}/${maxRetries}...`,
);
console.log(`Optimistic lock error encountered. Retrying ${attempt}/${maxRetries}...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay)); // Wait before retrying
} else {
throw error;
Expand Down
Loading

0 comments on commit 520e10e

Please sign in to comment.