From a375bf07ae789b42b38e63d42912192f2f919df6 Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Tue, 15 Sep 2026 00:04:12 +0800 Subject: [PATCH 1/3] feat: support editing comments and replies on the frontend --- .../comment-widget/src/base-comment-item.ts | 15 +- .../comment-widget/src/comment-edit-form.ts | 264 ++++++++++++++++++ packages/comment-widget/src/comment-item.ts | 25 +- .../comment-widget/src/comment-management.ts | 17 ++ .../src/generated/locales/es.ts | 8 + .../src/generated/locales/zh-CN.ts | 8 + .../src/generated/locales/zh-TW.ts | 8 + packages/comment-widget/src/index.ts | 2 + packages/comment-widget/src/reply-item.ts | 25 +- .../src/utils/comment-management.ts | 38 ++- packages/comment-widget/xliff/es.xlf | 32 +++ packages/comment-widget/xliff/zh-CN.xlf | 32 +++ packages/comment-widget/xliff/zh-TW.xlf | 32 +++ 13 files changed, 501 insertions(+), 5 deletions(-) create mode 100644 packages/comment-widget/src/comment-edit-form.ts diff --git a/packages/comment-widget/src/base-comment-item.ts b/packages/comment-widget/src/base-comment-item.ts index 81850de9..80ac846b 100644 --- a/packages/comment-widget/src/base-comment-item.ts +++ b/packages/comment-widget/src/base-comment-item.ts @@ -43,6 +43,9 @@ export class BaseCommentItem extends LitElement { @property({ type: Boolean }) private: boolean | undefined; + @property({ type: Boolean }) + editing = false; + @consume({ context: configMapDataContext }) @state() configMapData: ConfigMapData | undefined; @@ -106,7 +109,12 @@ export class BaseCommentItem extends LitElement { ${when(!this.approved, () => html`
${msg('Reviewing')}
`)} -
+
${when( + this.editing, + () => html``, + () => + html`` + )}
@@ -125,6 +133,11 @@ export class BaseCommentItem extends LitElement { contain-intrinsic-size: auto 4em; } + /* Paint containment would clip the focus ring of the slotted editor. */ + .item-content-editing { + content-visibility: visible; + } + .animate-breath { animation: breath 1s ease-in-out infinite; } diff --git a/packages/comment-widget/src/comment-edit-form.ts b/packages/comment-widget/src/comment-edit-form.ts new file mode 100644 index 00000000..db87703e --- /dev/null +++ b/packages/comment-widget/src/comment-edit-form.ts @@ -0,0 +1,264 @@ +import type { CommentVo, ReplyVo } from '@halo-dev/api-client'; +import { consume } from '@lit/context'; +import { msg } from '@lit/localize'; +import { css, html, LitElement } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { createRef, type Ref, ref } from 'lit/directives/ref.js'; +import { when } from 'lit/directives/when.js'; +import { FetchError } from 'ofetch'; +import type { CommentEditor } from './comment-editor'; +import { baseUrlContext, configMapDataContext, toastContext } from './context'; +import type { ToastManager } from './lit-toast'; +import baseStyles from './styles/base'; +import type { ConfigMapData } from './types'; +import { + fetchCommentContent, + updateCommentContent, +} from './utils/comment-management'; +import './comment-editor'; +import './loading-block'; +import './icons/icon-loading'; + +export class CommentEditForm extends LitElement { + @consume({ context: baseUrlContext }) + @state() + baseUrl = ''; + + @consume({ context: configMapDataContext }) + @state() + configMapData: ConfigMapData | undefined; + + @consume({ context: toastContext, subscribe: true }) + @state() + toastManager: ToastManager | undefined; + + @property({ attribute: false }) + target: CommentVo | ReplyVo | undefined; + + @property() + resource: 'comments' | 'replies' = 'comments'; + + @state() + private loading = true; + + @state() + private deleted = false; + + @state() + private loadFailed = false; + + @state() + private version: number | undefined; + + @state() + private content = ''; + + @state() + private saving = false; + + @state() + private errorMessage = ''; + + private initialRaw = ''; + + private editorRef: Ref = createRef(); + + override connectedCallback(): void { + super.connectedCallback(); + this.addEventListener('keydown', this.onKeydown); + void this.loadLatest(); + } + + override disconnectedCallback(): void { + this.removeEventListener('keydown', this.onKeydown); + super.disconnectedCallback(); + } + + private onKeydown = (event: KeyboardEvent) => { + if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + void this.handleSave(); + } + }; + + private async loadLatest() { + if (!this.target) { + return; + } + this.loading = true; + this.loadFailed = false; + try { + const latest = await fetchCommentContent( + this.baseUrl, + this.resource, + this.target.metadata.name + ); + if (latest.metadata.deletionTimestamp) { + this.deleted = true; + return; + } + this.version = latest.metadata.version ?? undefined; + this.initialRaw = latest.spec.raw || ''; + this.content = this.initialRaw; + } catch { + this.loadFailed = true; + } finally { + this.loading = false; + } + } + + private onEditorUpdate( + event: CustomEvent<{ content: string; characterCount: number }> + ) { + this.content = event.detail.content; + this.errorMessage = ''; + } + + private get hasContent() { + const body = new DOMParser().parseFromString( + this.content, + 'text/html' + ).body; + return ( + !!body.textContent?.replaceAll('\u00a0', ' ').trim() || + Array.from(body.querySelectorAll('img[src]')).some((image) => + image.getAttribute('src')?.trim() + ) + ); + } + + private get canSave() { + return ( + !this.saving && + this.version !== undefined && + this.content !== this.initialRaw && + this.hasContent + ); + } + + private async handleSave() { + if (!this.canSave || !this.target) { + return; + } + this.saving = true; + this.errorMessage = ''; + try { + await updateCommentContent( + this.baseUrl, + this.resource, + this.target.metadata.name, + { + raw: this.content, + content: this.content, + version: this.version as number, + } + ); + this.toastManager?.success(msg('Comment updated successfully')); + this.dispatchEvent( + new CustomEvent('comment-managed', { + bubbles: true, + composed: true, + detail: { + action: 'edit', + restoreFocus: false, + commentName: + this.resource === 'replies' + ? (this.target as ReplyVo).spec.commentName + : undefined, + }, + }) + ); + this.dispatchEvent( + new CustomEvent('close', { bubbles: true, composed: true }) + ); + } catch (error) { + this.errorMessage = + error instanceof FetchError && error.response?.status === 409 + ? msg( + 'This comment or reply has changed. Copy your draft, then reopen the editor to load the latest version.' + ) + : msg('Could not save. Your draft has been kept.'); + this.saving = false; + } + } + + private handleCancel() { + this.dispatchEvent( + new CustomEvent('close', { bubbles: true, composed: true }) + ); + } + + override render() { + if (this.loading) { + return html``; + } + if (this.deleted) { + return html` + ${this.renderCancelButton()}`; + } + if (this.loadFailed) { + return html` + ${this.renderCancelButton()}`; + } + return html` + + ${when( + this.errorMessage, + () => + html`` + )} +
+ ${this.renderCancelButton()} + +
+ `; + } + + private renderCancelButton() { + return html``; + } + + static override styles = [ + ...baseStyles, + css` + :host { + display: block; + } + @unocss-placeholder; + `, + ]; +} + +customElements.get('comment-edit-form') || + customElements.define('comment-edit-form', CommentEditForm); + +declare global { + interface HTMLElementTagNameMap { + 'comment-edit-form': CommentEditForm; + } +} diff --git a/packages/comment-widget/src/comment-item.ts b/packages/comment-widget/src/comment-item.ts index 58476d39..12723068 100644 --- a/packages/comment-widget/src/comment-item.ts +++ b/packages/comment-widget/src/comment-item.ts @@ -7,6 +7,7 @@ import './comment-replies'; import './user-avatar'; import './base-comment-item'; import './comment-management'; +import './comment-edit-form'; import { consume } from '@lit/context'; import { msg } from '@lit/localize'; import { createRef, type Ref, ref } from 'lit/directives/ref.js'; @@ -36,6 +37,9 @@ export class CommentItem extends LitElement { @state() showReplyForm = false; + @state() + showEditForm = false; + @state() upvoted = false; @@ -143,6 +147,15 @@ export class CommentItem extends LitElement { this.showReplyForm = false; } + private async handleCloseEditForm() { + this.showEditForm = false; + await this.updateComplete; + this.renderRoot + .querySelector('comment-management') + ?.shadowRoot?.querySelector('summary') + ?.focus({ preventScroll: true }); + } + handleToggleReplyForm() { if (this.showReplyForm) { this.closeReplyForm(); @@ -169,6 +182,7 @@ export class CommentItem extends LitElement { .userWebsite=${this.comment?.spec.owner.annotations?.website} .ua=${this.comment?.spec.userAgent} .private=${this.comment?.spec.hidden} + .editing=${this.showEditForm} > ${ this.resource === 'comments' ? html` diff --git a/packages/comment-widget/src/generated/locales/es.ts b/packages/comment-widget/src/generated/locales/es.ts index 48541bac..52b00601 100644 --- a/packages/comment-widget/src/generated/locales/es.ts +++ b/packages/comment-widget/src/generated/locales/es.ts @@ -20,14 +20,19 @@ 's0fbf6dc6a1966408': `Siguiente`, 's107ccef507b51f2c': `Completa la verificación`, 's15e33945d13c5176': `Se está confirmando tu envío anterior. Inténtalo más tarde; se conservarán tus imágenes.`, +'s176fe95698857ad9': `El comentario o la respuesta ha cambiado. Copia tu borrador y vuelve a abrir el editor para cargar la última versión.`, 's1c6fefb092506753': `Error al cargar la lista de comentarios, por favor intente más tarde`, 's1d468f888124a55e': `Tu borrador ha cambiado. Vuelve a abrirlo antes de reintentar.`, 's1e3e30a26025484c': `Error al cargar la lista de respuestas, por favor intente más tarde`, +'s2254107bb49631da': `Este comentario o respuesta ha sido eliminado.`, +'s22ae36191f9a1a8d': `No se pudo cargar el contenido más reciente. Inténtalo de nuevo más tarde.`, 's2406b89e991a4524': `Actualizar código de verificación`, 's26e4d65f2801ac9c': `Por favor, ingrese el contenido`, 's299b10f3a58a09fd': `Haga clic en OK para ir a la página de cierre de sesión, Por favor, asegúrese de que el contenido editado haya sido guardado.`, 's2ca7699f7c5a4996': `Administrar`, +'s2ceb11be2290bb1b': `Cancelar`, 's2e192b19ed15fcf6': `Página`, +'s33f85f24c0f5f008': `Guardar`, 's34239948d1de01f1': `Verificación no disponible. Haz clic para reintentar.`, 's35c3125c7750681e': `Cita`, 's3643189d1abbb7f4': `Código`, @@ -41,6 +46,8 @@ 's59b95f09e3b42060': `La verificación ha fallado. Vuelve a verificar e intenta enviar de nuevo.`, 's5fc35a09a85fe63e': `Cerrar imagen`, 's63ce0636351bf780': `Los resultados de la subida están incompletos. Inténtalo de nuevo.`, +'s63efea23113001ee': `No se pudo guardar. Se ha conservado tu borrador.`, +'s64ef2a6c2dd1d3d1': `Editar`, 's67749057edb2586b': `Cerrar sesión`, 's6cb61eeccda272d5': `Bloque de código`, 's6f23997fffbecc6a': `Cancelar aprobación`, @@ -62,6 +69,7 @@ 'sa10fbe3fcd6ae148': `Agregar respuesta`, 'sa3443b99ecf186ea': `Mostrar`, 'sa82fbb7f361c6f44': `Mostrar respuestas`, +'sa862e76e95967816': `Comentario actualizado correctamente`, 'sa8dddacbaa66f8e0': `Por favor, inicie sesión primero`, 'sb206d700d26b14ff': `Subrayado`, 'sb3d4f79d9d8b71e5': `Enviar comentario`, diff --git a/packages/comment-widget/src/generated/locales/zh-CN.ts b/packages/comment-widget/src/generated/locales/zh-CN.ts index 0024d5b0..48531b53 100644 --- a/packages/comment-widget/src/generated/locales/zh-CN.ts +++ b/packages/comment-widget/src/generated/locales/zh-CN.ts @@ -20,14 +20,19 @@ 's0fbf6dc6a1966408': `下一页`, 's107ccef507b51f2c': `请完成人机验证`, 's15e33945d13c5176': `上次提交结果正在确认,请稍后重试;图片会被保留。`, +'s176fe95698857ad9': `评论或回复已发生变化。请先复制草稿,再重新打开编辑框获取最新版本。`, 's1c6fefb092506753': `加载评论列表失败,请稍后重试`, 's1d468f888124a55e': `草稿已更改,请重新打开后再重试。`, 's1e3e30a26025484c': `加载回复列表失败,请稍后重试`, +'s2254107bb49631da': `该评论或回复已被删除。`, +'s22ae36191f9a1a8d': `加载最新内容失败,请稍后重试。`, 's2406b89e991a4524': `刷新验证码`, 's26e4d65f2801ac9c': `请输入内容`, 's299b10f3a58a09fd': `点击确定将跳转至退出登录页面,请确保正在编辑的内容已保存。`, 's2ca7699f7c5a4996': `管理`, +'s2ceb11be2290bb1b': `取消`, 's2e192b19ed15fcf6': `页码`, +'s33f85f24c0f5f008': `保存`, 's34239948d1de01f1': `验证暂不可用,点击重试`, 's35c3125c7750681e': `引用`, 's3643189d1abbb7f4': `代码`, @@ -41,6 +46,8 @@ 's59b95f09e3b42060': `人机验证未通过,请重新验证后提交。`, 's5fc35a09a85fe63e': `关闭图片`, 's63ce0636351bf780': `上传结果不完整,请重试。`, +'s63efea23113001ee': `保存失败,草稿已保留。`, +'s64ef2a6c2dd1d3d1': `编辑`, 's67749057edb2586b': `退出登录`, 's6cb61eeccda272d5': `代码块`, 's6f23997fffbecc6a': `取消通过`, @@ -62,6 +69,7 @@ 'sa10fbe3fcd6ae148': `加入回复`, 'sa3443b99ecf186ea': `取消隐藏`, 'sa82fbb7f361c6f44': `显示回复`, +'sa862e76e95967816': `评论更新成功`, 'sa8dddacbaa66f8e0': `请先登录`, 'sb206d700d26b14ff': `下划线`, 'sb3d4f79d9d8b71e5': `提交评论`, diff --git a/packages/comment-widget/src/generated/locales/zh-TW.ts b/packages/comment-widget/src/generated/locales/zh-TW.ts index cc6c0649..f8ebe3c0 100644 --- a/packages/comment-widget/src/generated/locales/zh-TW.ts +++ b/packages/comment-widget/src/generated/locales/zh-TW.ts @@ -20,14 +20,19 @@ 's0fbf6dc6a1966408': `下一頁`, 's107ccef507b51f2c': `請完成人機驗證`, 's15e33945d13c5176': `上次提交結果正在確認,請稍後重試;圖片會被保留。`, +'s176fe95698857ad9': `評論或回覆已發生變更。請先複製草稿,再重新開啟編輯框取得最新版本。`, 's1c6fefb092506753': `載入評論列表失敗,請稍後重試`, 's1d468f888124a55e': `草稿已變更,請重新開啟後再重試。`, 's1e3e30a26025484c': `載入回覆列表失敗,請稍後重試`, +'s2254107bb49631da': `該評論或回覆已被刪除。`, +'s22ae36191f9a1a8d': `載入最新內容失敗,請稍後再試。`, 's2406b89e991a4524': `重新整理驗證碼`, 's26e4d65f2801ac9c': `請輸入內容`, 's299b10f3a58a09fd': `点击确定将跳转至退出登录页面,请确保正在编辑的内容已保存。`, 's2ca7699f7c5a4996': `管理`, +'s2ceb11be2290bb1b': `取消`, 's2e192b19ed15fcf6': `頁碼`, +'s33f85f24c0f5f008': `儲存`, 's34239948d1de01f1': `驗證暫不可用,點擊重試`, 's35c3125c7750681e': `引用`, 's3643189d1abbb7f4': `程式碼`, @@ -41,6 +46,8 @@ 's59b95f09e3b42060': `人機驗證未通過,請重新驗證後提交。`, 's5fc35a09a85fe63e': `關閉圖片`, 's63ce0636351bf780': `上傳結果不完整,請重試。`, +'s63efea23113001ee': `儲存失敗,草稿已保留。`, +'s64ef2a6c2dd1d3d1': `編輯`, 's67749057edb2586b': `登出`, 's6cb61eeccda272d5': `程式碼區塊`, 's6f23997fffbecc6a': `取消通過`, @@ -62,6 +69,7 @@ 'sa10fbe3fcd6ae148': `新增回覆`, 'sa3443b99ecf186ea': `取消隱藏`, 'sa82fbb7f361c6f44': `顯示回覆`, +'sa862e76e95967816': `評論更新成功`, 'sa8dddacbaa66f8e0': `請先登入`, 'sb206d700d26b14ff': `底線`, 'sb3d4f79d9d8b71e5': `提交評論`, diff --git a/packages/comment-widget/src/index.ts b/packages/comment-widget/src/index.ts index cdba6794..0c83c8a6 100644 --- a/packages/comment-widget/src/index.ts +++ b/packages/comment-widget/src/index.ts @@ -1,6 +1,7 @@ import { BaseCommentItem } from './base-comment-item'; import { BaseForm } from './base-form'; import { CommentContent } from './comment-content'; +import { CommentEditForm } from './comment-edit-form'; import { CommentEditor } from './comment-editor'; import { CommentItem } from './comment-item'; import { CommentList } from './comment-list'; @@ -19,6 +20,7 @@ export { BaseCommentItem, BaseForm, CommentContent, + CommentEditForm, CommentEditor, CommentItem, CommentList, diff --git a/packages/comment-widget/src/reply-item.ts b/packages/comment-widget/src/reply-item.ts index 06f4a535..b5bfb2e3 100644 --- a/packages/comment-widget/src/reply-item.ts +++ b/packages/comment-widget/src/reply-item.ts @@ -6,6 +6,7 @@ import baseStyles from './styles/base'; import './user-avatar'; import './base-comment-item'; import './comment-management'; +import './comment-edit-form'; import './reply-form'; import { consume } from '@lit/context'; import { msg } from '@lit/localize'; @@ -35,6 +36,9 @@ export class ReplyItem extends LitElement { @state() showReplyForm = false; + @state() + showEditForm = false; + @state() upvoted = false; @@ -84,6 +88,15 @@ export class ReplyItem extends LitElement { this.showReplyForm = false; } + private async handleCloseEditForm() { + this.showEditForm = false; + await this.updateComplete; + this.renderRoot + .querySelector('comment-management') + ?.shadowRoot?.querySelector('summary') + ?.focus({ preventScroll: true }); + } + onReplyCreated( event: CustomEvent<{ resetForm: (form: BaseForm) => boolean }> ) { @@ -170,6 +183,7 @@ export class ReplyItem extends LitElement { .userWebsite=${this.reply?.spec.owner.annotations?.website} .ua=${this.reply?.spec.userAgent} .private=${this.comment?.spec.hidden || this.reply?.spec.hidden} + .editing=${this.showEditForm} > - + (this.showEditForm = true)}> + ${when( + this.showEditForm, + () => html`` + )} ${when( this.showReplyForm, () => html`
diff --git a/packages/comment-widget/src/utils/comment-management.ts b/packages/comment-widget/src/utils/comment-management.ts index 434fc627..7f7bd643 100644 --- a/packages/comment-widget/src/utils/comment-management.ts +++ b/packages/comment-widget/src/utils/comment-management.ts @@ -1,4 +1,9 @@ -import type { ListedReplyList, UserPermission } from '@halo-dev/api-client'; +import type { + Comment, + ListedReplyList, + Reply, + UserPermission, +} from '@halo-dev/api-client'; import { ofetch } from 'ofetch'; export type ManagementAction = @@ -62,8 +67,37 @@ export async function manageComment( }); } +export interface CommentContentUpdate { + raw: string; + content: string; + version: number; +} + +export async function fetchCommentContent( + baseUrl: string, + resource: 'comments' | 'replies', + name: string +) { + return ofetch( + `${baseUrl}/apis/content.halo.run/v1alpha1/${resource}/${encodeURIComponent(name)}`, + { retry: 0 } + ); +} + +export async function updateCommentContent( + baseUrl: string, + resource: 'comments' | 'replies', + name: string, + body: CommentContentUpdate +) { + await ofetch( + `${baseUrl}/apis/api.console.halo.run/v1alpha1/${resource}/${encodeURIComponent(name)}/content`, + { method: 'PUT', body, retry: 0 } + ); +} + export interface CommentManagedDetail { - action: ManagementAction; + action: ManagementAction | 'edit'; restoreFocus: boolean; commentName?: string; } diff --git a/packages/comment-widget/xliff/es.xlf b/packages/comment-widget/xliff/es.xlf index 8905730f..1028bf58 100644 --- a/packages/comment-widget/xliff/es.xlf +++ b/packages/comment-widget/xliff/es.xlf @@ -302,6 +302,38 @@ Zoom image Ampliar imagen + + Comment updated successfully + Comentario actualizado correctamente + + + This comment or reply has changed. Copy your draft, then reopen the editor to load the latest version. + El comentario o la respuesta ha cambiado. Copia tu borrador y vuelve a abrir el editor para cargar la última versión. + + + Could not save. Your draft has been kept. + No se pudo guardar. Se ha conservado tu borrador. + + + This comment or reply has been deleted. + Este comentario o respuesta ha sido eliminado. + + + Failed to load the latest content. Please try again later. + No se pudo cargar el contenido más reciente. Inténtalo de nuevo más tarde. + + + Save + Guardar + + + Cancel + Cancelar + + + Edit + Editar + diff --git a/packages/comment-widget/xliff/zh-CN.xlf b/packages/comment-widget/xliff/zh-CN.xlf index df62c1b0..e5c5e9d0 100644 --- a/packages/comment-widget/xliff/zh-CN.xlf +++ b/packages/comment-widget/xliff/zh-CN.xlf @@ -302,6 +302,38 @@ Zoom image 放大图片 + + Comment updated successfully + 评论更新成功 + + + This comment or reply has changed. Copy your draft, then reopen the editor to load the latest version. + 评论或回复已发生变化。请先复制草稿,再重新打开编辑框获取最新版本。 + + + Could not save. Your draft has been kept. + 保存失败,草稿已保留。 + + + This comment or reply has been deleted. + 该评论或回复已被删除。 + + + Failed to load the latest content. Please try again later. + 加载最新内容失败,请稍后重试。 + + + Save + 保存 + + + Cancel + 取消 + + + Edit + 编辑 + diff --git a/packages/comment-widget/xliff/zh-TW.xlf b/packages/comment-widget/xliff/zh-TW.xlf index 10e2fc4a..77e655a4 100644 --- a/packages/comment-widget/xliff/zh-TW.xlf +++ b/packages/comment-widget/xliff/zh-TW.xlf @@ -302,6 +302,38 @@ Zoom image 放大圖片 + + Comment updated successfully + 評論更新成功 + + + This comment or reply has changed. Copy your draft, then reopen the editor to load the latest version. + 評論或回覆已發生變更。請先複製草稿,再重新開啟編輯框取得最新版本。 + + + Could not save. Your draft has been kept. + 儲存失敗,草稿已保留。 + + + This comment or reply has been deleted. + 該評論或回覆已被刪除。 + + + Failed to load the latest content. Please try again later. + 載入最新內容失敗,請稍後再試。 + + + Save + 儲存 + + + Cancel + 取消 + + + Edit + 編輯 + From e68dda894f170bee17a425b93b17e51498fbe05b Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Tue, 15 Sep 2026 10:42:45 +0800 Subject: [PATCH 2/3] fix: address review findings in comment editing - Preserve newlines and consecutive spaces when editing plain-text comments created from the Console textarea editor - Show a dedicated message when saving fails with 404 on Halo versions without the content endpoints - Allow canceling while the latest content is loading and retrying after a load failure - Focus the editor once it is ready and confirm before discarding unsaved changes - Rename the generic close event to edit-close - Cover the new content endpoints and plain-text conversion with tests --- .../comment-widget/src/comment-edit-form.ts | 62 +++++++++++++++-- packages/comment-widget/src/comment-item.ts | 2 +- .../src/generated/locales/es.ts | 3 + .../src/generated/locales/zh-CN.ts | 3 + .../src/generated/locales/zh-TW.ts | 3 + packages/comment-widget/src/reply-item.ts | 2 +- packages/comment-widget/src/utils/html.ts | 28 ++++++++ .../tests/comment-management.test.ts | 67 +++++++++++++++++++ .../tests/editor-content.test.ts | 29 ++++++++ packages/comment-widget/xliff/es.xlf | 12 ++++ packages/comment-widget/xliff/zh-CN.xlf | 12 ++++ packages/comment-widget/xliff/zh-TW.xlf | 12 ++++ 12 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 packages/comment-widget/tests/editor-content.test.ts diff --git a/packages/comment-widget/src/comment-edit-form.ts b/packages/comment-widget/src/comment-edit-form.ts index db87703e..3d5f43a8 100644 --- a/packages/comment-widget/src/comment-edit-form.ts +++ b/packages/comment-widget/src/comment-edit-form.ts @@ -18,6 +18,7 @@ import { import './comment-editor'; import './loading-block'; import './icons/icon-loading'; +import { toEditorContent } from './utils/html'; export class CommentEditForm extends LitElement { @consume({ context: baseUrlContext }) @@ -61,6 +62,8 @@ export class CommentEditForm extends LitElement { private initialRaw = ''; + private editorContent = ''; + private editorRef: Ref = createRef(); override connectedCallback(): void { @@ -83,6 +86,8 @@ export class CommentEditForm extends LitElement { private async loadLatest() { if (!this.target) { + this.loading = false; + this.loadFailed = true; return; } this.loading = true; @@ -99,12 +104,31 @@ export class CommentEditForm extends LitElement { } this.version = latest.metadata.version ?? undefined; this.initialRaw = latest.spec.raw || ''; + this.editorContent = toEditorContent(this.initialRaw); this.content = this.initialRaw; } catch { this.loadFailed = true; } finally { this.loading = false; } + if (!this.loadFailed && !this.deleted) { + void this.focusEditor(); + } + } + + private async focusEditor() { + await this.updateComplete; + const editor = this.editorRef.value; + if (!editor) { + return; + } + // The editor is created asynchronously after its dynamic imports resolve. + for (let i = 0; i < 100 && !editor.editor && this.isConnected; i++) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + if (this.isConnected && editor.editor) { + editor.setFocus(); + } } private onEditorUpdate( @@ -169,28 +193,43 @@ export class CommentEditForm extends LitElement { }) ); this.dispatchEvent( - new CustomEvent('close', { bubbles: true, composed: true }) + new CustomEvent('edit-close', { bubbles: true, composed: true }) ); } catch (error) { + const status = + error instanceof FetchError ? error.response?.status : undefined; this.errorMessage = - error instanceof FetchError && error.response?.status === 409 + status === 409 ? msg( 'This comment or reply has changed. Copy your draft, then reopen the editor to load the latest version.' ) - : msg('Could not save. Your draft has been kept.'); + : status === 404 + ? msg( + 'The current Halo version does not support editing comments. Please upgrade Halo.' + ) + : msg('Could not save. Your draft has been kept.'); this.saving = false; } } private handleCancel() { + if ( + this.content !== this.initialRaw && + !window.confirm(msg('Discard your changes?')) + ) { + return; + } this.dispatchEvent( - new CustomEvent('close', { bubbles: true, composed: true }) + new CustomEvent('edit-close', { bubbles: true, composed: true }) ); } override render() { if (this.loading) { - return html``; + return html` +
+ ${this.renderCancelButton()} +
`; } if (this.deleted) { return html`