From f1ae5661341a2537101e5fc5f71c0827c7d6c61c Mon Sep 17 00:00:00 2001 From: qwqcode <22412567+qwqcode@users.noreply.github.com> Date: Sat, 5 Oct 2024 20:33:06 +0800 Subject: [PATCH] refactor(ui/conf): introduce `DeepPartial` type for ui config (#996) - Replace `Partial` with `ArtalkConfigPartial` for better type clarity. - Introduce `DeepPartial` type for nested partial configurations. - Update default configuration handling to use `RequiredExcept` and `FunctionKeys`. - Adjust related functions and interfaces to use `ArtalkConfigPartial`. --- ui/artalk/src/artalk.ts | 10 +-- ui/artalk/src/config.ts | 17 ++--- ui/artalk/src/context.ts | 3 +- ui/artalk/src/defaults.ts | 16 ++++- ui/artalk/src/load.ts | 4 +- ui/artalk/src/types/config.ts | 115 +++++++++++++++++-------------- ui/artalk/src/types/context.ts | 3 +- ui/plugin-kit/package.json | 2 +- ui/plugin-kit/src/plugin/main.ts | 4 +- 9 files changed, 101 insertions(+), 73 deletions(-) diff --git a/ui/artalk/src/artalk.ts b/ui/artalk/src/artalk.ts index 376ce251..63065b55 100644 --- a/ui/artalk/src/artalk.ts +++ b/ui/artalk/src/artalk.ts @@ -8,7 +8,7 @@ import * as Stat from './plugins/stat' import { Api } from './api' import type { TInjectedServices } from './service' import { GlobalPlugins, PluginOptions, load } from './load' -import type { ArtalkConfig, EventPayloadMap, ArtalkPlugin, ContextApi } from '@/types' +import type { ArtalkConfigPartial, EventPayloadMap, ArtalkPlugin, ContextApi } from '@/types' /** * Artalk @@ -18,7 +18,7 @@ import type { ArtalkConfig, EventPayloadMap, ArtalkPlugin, ContextApi } from '@/ export default class Artalk { public ctx!: ContextApi - constructor(conf: Partial) { + constructor(conf: ArtalkConfigPartial) { // Init Config const handledConf = handelCustomConf(conf, true) @@ -49,7 +49,7 @@ export default class Artalk { } /** Update config of Artalk */ - public update(conf: Partial) { + public update(conf: ArtalkConfigPartial) { this.ctx.updateConf(conf) return this } @@ -92,7 +92,7 @@ export default class Artalk { // =========================== /** Init Artalk */ - public static init(conf: Partial): Artalk { + public static init(conf: ArtalkConfigPartial): Artalk { return new Artalk(conf) } @@ -103,7 +103,7 @@ export default class Artalk { } /** Load count widget */ - public static loadCountWidget(c: Partial) { + public static loadCountWidget(c: ArtalkConfigPartial) { const conf = handelCustomConf(c, true) Stat.initCountWidget({ diff --git a/ui/artalk/src/config.ts b/ui/artalk/src/config.ts index 62de44a3..d19a5022 100644 --- a/ui/artalk/src/config.ts +++ b/ui/artalk/src/config.ts @@ -2,7 +2,7 @@ import type { ApiOptions } from './api/options' import { mergeDeep } from './lib/merge-deep' import { createApiHandlers } from './api' import Defaults from './defaults' -import type { ArtalkConfig, ContextApi } from '@/types' +import type { ArtalkConfig, ArtalkConfigPartial, ContextApi } from '@/types' /** * Handle the custom config which is provided by the user @@ -11,14 +11,11 @@ import type { ArtalkConfig, ContextApi } from '@/types' * @param full - If `full` is `true`, the return value will be the complete config for Artalk instance creation * @returns The config for Artalk instance creation */ -export function handelCustomConf(customConf: Partial, full: true): ArtalkConfig -export function handelCustomConf( - customConf: Partial, - full?: false, -): Partial -export function handelCustomConf(customConf: Partial, full = false) { +export function handelCustomConf(customConf: ArtalkConfigPartial, full: true): ArtalkConfig +export function handelCustomConf(customConf: ArtalkConfigPartial, full?: false): ArtalkConfigPartial +export function handelCustomConf(customConf: ArtalkConfigPartial, full = false) { // 合并默认配置 - const conf: Partial = full ? mergeDeep(Defaults, customConf) : customConf + const conf: ArtalkConfigPartial = full ? mergeDeep(Defaults, customConf) : customConf // 绑定元素 if (conf.el && typeof conf.el === 'string') { @@ -59,7 +56,7 @@ export function handelCustomConf(customConf: Partial, full = false * @param conf - The Server response config for control the frontend of Artalk remotely * @returns The config for Artalk instance creation */ -export function handleConfFormServer(conf: Partial) { +export function handleConfFormServer(conf: ArtalkConfigPartial): ArtalkConfigPartial { const ExcludedKeys: (keyof ArtalkConfig)[] = [ 'el', 'pageKey', @@ -95,7 +92,7 @@ export function handleConfFormServer(conf: Partial) { * @param ctx - If `ctx` not provided, `checkAdmin` and `checkCaptcha` will be disabled * @returns ApiOptions for Api client instance creation */ -export function convertApiOptions(conf: Partial, ctx?: ContextApi): ApiOptions { +export function convertApiOptions(conf: ArtalkConfigPartial, ctx?: ContextApi): ApiOptions { return { baseURL: `${conf.server}/api/v2`, siteName: conf.site || '', diff --git a/ui/artalk/src/context.ts b/ui/artalk/src/context.ts index a1fb4bd2..45fe9053 100644 --- a/ui/artalk/src/context.ts +++ b/ui/artalk/src/context.ts @@ -15,6 +15,7 @@ import { watchConf } from './lib/watch-conf' import type { ArtalkConfig, + ArtalkConfigPartial, CommentData, ListFetchParams, ContextApi, @@ -165,7 +166,7 @@ class Context implements ContextApi { this.updateConf({ darkMode }) } - updateConf(nConf: Partial): void { + updateConf(nConf: ArtalkConfigPartial): void { this.conf = mergeDeep(this.conf, handelCustomConf(nConf, false)) this.mounted && this.events.trigger('updated', this.conf) } diff --git a/ui/artalk/src/defaults.ts b/ui/artalk/src/defaults.ts index 83ab79be..da613c20 100644 --- a/ui/artalk/src/defaults.ts +++ b/ui/artalk/src/defaults.ts @@ -1,6 +1,13 @@ import type { ArtalkConfig } from '@/types' -const defaults: ArtalkConfig = { +type RequiredExcept = Required> & Pick +type FunctionKeys = Exclude< + { [K in keyof T]: NonNullable extends (...args: any[]) => any ? K : never }[keyof T], + undefined +> +type ExcludedKeys = FunctionKeys + +const defaults: RequiredExcept = { el: '', pageKey: '', pageTitle: '', @@ -45,12 +52,19 @@ const defaults: ArtalkConfig = { scrollable: false, }, + pvAdd: true, imgUpload: true, + imgLazyLoad: 'native', reqTimeout: 15000, versionCheck: true, useBackendConf: true, + listUnreadHighlight: false, locale: 'en', + apiVersion: '', + pluginURLs: [], + markedReplacers: [], + markedOptions: {}, } if (ARTALK_LITE) { diff --git a/ui/artalk/src/load.ts b/ui/artalk/src/load.ts index 86897ad4..91e67153 100644 --- a/ui/artalk/src/load.ts +++ b/ui/artalk/src/load.ts @@ -1,5 +1,5 @@ import { DefaultPlugins } from './plugins' -import type { ArtalkConfig, ArtalkPlugin, ContextApi } from '@/types' +import type { ArtalkConfigPartial, ArtalkPlugin, ContextApi } from '@/types' import { handleConfFormServer } from '@/config' import { showErrorDialog } from '@/components/error-dialog' @@ -37,7 +37,7 @@ export async function load(ctx: ContextApi) { }) // Initial config - let conf: Partial = { + let conf: ArtalkConfigPartial = { apiVersion: data.version?.version, // version info } diff --git a/ui/artalk/src/types/config.ts b/ui/artalk/src/types/config.ts index 74c72bd3..db8be097 100644 --- a/ui/artalk/src/types/config.ts +++ b/ui/artalk/src/types/config.ts @@ -4,156 +4,171 @@ import type { EditorApi } from './editor' import type { I18n } from '@/i18n' export interface ArtalkConfig { - /** 装载元素 */ + /** Element selector or Element to mount the Artalk */ el: string | HTMLElement - /** 页面唯一标识(完整 URL) */ + /** Unique page identifier */ pageKey: string - /** 页面标题 */ + /** Title of the page */ pageTitle: string - /** 服务器地址 */ + /** Server address */ server: string - /** 站点名 */ + /** Site name */ site: string - /** 评论框占位字符 */ + /** Placeholder text for the comment input box */ placeholder: string - /** 评论为空时显示字符 */ + /** Text to display when there are no comments */ noComment: string - /** 发送按钮文字 */ + /** Text for the send button */ sendBtn: string - /** 评论框穿梭(显示在待回复评论后面) */ + /** Movable comment box (display below the comment to be replied) */ editorTravel: boolean - /** 表情包 */ + /** Emoticons settings */ emoticons: object | any[] | string | false - /** Gravatar 头像 */ + /** Gravatar avatar settings */ gravatar: { - /** API 地址 */ + /** API endpoint */ mirror: string - /** API 参数 */ + /** API parameters */ params: string } - /** 头像链接生成器 */ + /** Avatar URL generator function */ avatarURLBuilder?: (comment: CommentData) => string - /** 分页配置 */ + /** Pagination settings */ pagination: { - /** 每次请求获取数量 */ + /** Number of comments to fetch per request */ pageSize: number - /** 阅读更多模式 */ + /** "Read more" mode */ readMore: boolean - /** 滚动到底部自动加载 */ + /** Automatically load more comments when scrolled to the bottom */ autoLoad: boolean } - /** 内容限高 */ + /** Height limit configuration */ heightLimit: { - /** 评论内容限高 */ + /** Maximum height for comment content */ content: number - /** 子评论区域限高 */ + /** Maximum height for child comments */ children: number - /** 滚动限高 */ + /** Whether the content is scrollable */ scrollable: boolean } - /** 评论投票按钮 */ + /** Voting feature for comments */ vote: boolean - /** 评论投票反对按钮 */ + /** Downvote button for comments */ voteDown: boolean - /** 评论预览功能 */ + /** Preview feature for comments */ preview: boolean - /** 评论数绑定元素 Selector */ + /** Selector for the element binding to the comment count */ countEl: string - /** PV 数绑定元素 Selector */ + /** Selector for the element binding to the page views (PV) count */ pvEl: string - /** 统计组件 PageKey 属性名 */ + /** Attribute name for the PageKey in statistics components */ statPageKeyAttr: string - /** 夜间模式 */ + /** Dark mode settings */ darkMode: boolean | 'auto' - /** 请求超时(单位:秒) */ + /** Request timeout (in seconds) */ reqTimeout: number - /** 平铺模式 */ + /** Flat mode for comment display */ flatMode: boolean | 'auto' - /** 嵌套模式 · 最大层数 */ + /** Maximum number of levels for nested comments */ nestMax: number - /** 嵌套模式 · 排序方式 */ + /** Sorting order for nested comments */ nestSort: 'DATE_ASC' | 'DATE_DESC' - /** 显示 UA 徽标 */ + /** Display UA badge (user agent badge) */ uaBadge: boolean - /** 评论列表排序功能 (显示 Dropdown) */ + /** Show sorting dropdown for comment list */ listSort: boolean - /** 图片上传功能 */ + /** Enable image upload feature */ imgUpload: boolean - /** 图片上传器 */ + /** Image uploader function */ imgUploader?: (file: File) => Promise - /** Image lazy load */ + /** Image lazy load mode */ imgLazyLoad?: 'native' | 'data-src' - /** 版本检测 */ + /** Enable version check */ versionCheck: boolean - /** 引用后端配置 */ + /** Use backend configuration */ useBackendConf: boolean - /** 语言本地化 */ + /** Localization settings */ locale: I18n | string - /** 后端版本 (系统数据,用户不允许更改) */ + /** Backend API version (system data, not allowed for user modification) */ apiVersion?: string - /** Plugin script urls */ + /** URLs for plugin scripts */ pluginURLs?: string[] - /** Replacer for marked */ + /** Replacers for the marked (Markdown parser) */ markedReplacers?: ((raw: string) => string)[] - /** Marked options */ + /** Options for the marked (Markdown parser) */ markedOptions?: MarkedOptions - /** 列表请求参数修改器 */ + /** Modifier for list fetch request parameters */ listFetchParamsModifier?: (params: any) => void /** - * Date formatter for custom date format - * @param date - Date object + * Custom date formatter + * @param date - The Date object to format * @returns Formatted date string */ dateFormatter?: (date: Date) => string - // TODO consider merge list related config into one object, or flatten all to keep simple (keep consistency) - remoteConfModifier?: (conf: Partial) => void + /** Custom configuration modifier for remote configuration (referenced by artalk-sidebar) */ + // TODO: Consider merging list-related configuration into a single object, or flatten everything for simplicity and consistency + remoteConfModifier?: (conf: DeepPartial) => void + + /** List unread highlight (enable by default in artalk-sidebar) */ listUnreadHighlight?: boolean + + /** The relative element for scrolling (useful if artalk is in a scrollable container) */ scrollRelativeTo?: () => HTMLElement + + /** Page view increment when comment list is loaded */ pvAdd?: boolean + + /** Callback before submitting a comment */ beforeSubmit?: (editor: EditorApi, next: () => void) => void } +type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K] +} + +export type ArtalkConfigPartial = DeepPartial + /** * Local User Data (in localStorage) * diff --git a/ui/artalk/src/types/context.ts b/ui/artalk/src/types/context.ts index be7751fc..a3417334 100644 --- a/ui/artalk/src/types/context.ts +++ b/ui/artalk/src/types/context.ts @@ -2,6 +2,7 @@ import type { Marked } from 'marked' import type { SidebarShowPayload, EventPayloadMap, + ArtalkConfigPartial, ArtalkConfig, CommentData, DataManagerApi, @@ -114,7 +115,7 @@ export interface ContextApi extends EventManagerFuncs { getEl(): HTMLElement /** 更新配置 */ - updateConf(conf: Partial): void + updateConf(conf: ArtalkConfigPartial): void /** 监听配置更新 */ watchConf( diff --git a/ui/plugin-kit/package.json b/ui/plugin-kit/package.json index 7fea4bf4..d663bd36 100644 --- a/ui/plugin-kit/package.json +++ b/ui/plugin-kit/package.json @@ -1,6 +1,6 @@ { "name": "@artalk/plugin-kit", - "version": "1.0.7", + "version": "1.0.8", "description": "The plugin kit for Artalk", "type": "module", "main": "dist/main.js", diff --git a/ui/plugin-kit/src/plugin/main.ts b/ui/plugin-kit/src/plugin/main.ts index 538ca36f..48e7e047 100644 --- a/ui/plugin-kit/src/plugin/main.ts +++ b/ui/plugin-kit/src/plugin/main.ts @@ -2,7 +2,7 @@ import path from 'node:path' import fs from 'node:fs' import type { LibraryOptions, Plugin } from 'vite' import ts from 'typescript' -import type { ArtalkConfig } from 'artalk' +import type { ArtalkConfigPartial } from 'artalk' import { RUNTIME_PATH, getRuntimeCode, wrapVirtualPrefix } from './runtime-helper' import { getInjectHTMLTags, hijackIndexPage } from './dev-page' @@ -20,7 +20,7 @@ export interface ViteArtalkPluginKitOptions { /** * The options for Artalk instance initialization in the dev page. */ - artalkInitOptions?: Partial + artalkInitOptions?: ArtalkConfigPartial } export const ViteArtalkPluginKit = (opts: ViteArtalkPluginKitOptions = {}): Plugin => {