Enhance/more ns plugin api (#4828)

* improve(plugin): WIP add settings schema

* improve(plugin): add identity for settings modal

* improve(plugin): WIP add settings input

* fix(ui): scrollbar overlay of modal panel content

* improve(plugin): WIP add more render types of setting item

* improve(plugin): WIP polish settings items

* improve(plugin): WIP settings list of plugins

* improve(plugin): more settings types & polish releated ui

* fix(plugin): sometimes disable plugin not work

* improve(plugin): polish ui of plugin settings

* fix(dev): warning of lint

* improve(plugin): add api of settings changed

* chore: build libs core

* fix(ui): width of settings panel wrap

* improve(plugin): separate layouts data from settings aio file

* imporve(plugin): container size of single plugin settings

* fix: add missing state

* improve(plugin): add Git ns

* improve(plugin): git related api

* improve(api): type of git result

* chore: build libs core

* fix(dev): kondo lint

* fix(plugin): use cdn sdk when js entry

* chore: build libs core

* fix(plugin): env condition

* improve(plugin): add UI ns

* fix(api): arguments of datascript query

* enhance(api): manageable message instance of UI tools

* enhance(api): WIP add experiments api

* enhance(api): WIP add resources state of plugin

* improve(plugin): add status of loading script resources

* improve(plugin): more opts for script loader

* improve(plugin): WIP add fenced code renderer hook

* improve(plugin): fenced code renderer hook

* fix(plugin): resource root path of plugin fs location

* imporve(plugin): support local files for loading scripts

* improve(plugin): types of expirements api

* fix: typo of class

* enhance(api): add namespace related apis

* enhance(api): add linked refrences related apis

* enhance(plugin): add sample links to related api comments

* improve(plugin): add db changed hook & optimize strategy of caller for hooks

* improve(plugin): compatible commands registration for old sdk

* improve(plugin): collect user sdk version for plugin local

* improve(plugin): add internal callable apis for user sdk

* chore(plugin): missing files & bump libs version

* improve(plugin): compatiable for old sdk about hook messaging optimization

* improve(plugin): db hook optimization for old sdk

* enhance(ux): auto focus searchbar when open plugins list

* improve(plugin): api of a hook from specific block changed event

* improve(plugin): api of db block change hook

* improve(plugin): add show bracket user config of api

* improve(plugin): api of db block change hook

* fix(api): toggle collapsed of block

* improve(api): try to init grpah with git before exec git commands

* improve(plugin): attributes of sandbox container

* improve(dev): support register command with keybinding

* improve(plugin): add api of register shortcut command

* fix(plugin): reubild slash commands when new command registration

* fix(dev): lint

* improve(dev): lint script of libs codebase

* chore(dev): remove useless codes

* improve(plugin):sanitize path string of plugin repo value

* fix(plugin): rebuild commands list when unregister a plugin

* fix(ui): overflow width of query result table

* chore: rebuild libs core

* improve(plugin): add assets related apis

* chore: rebuild libs core

* improve(plugin): support replace state of into block in page api

* improve(plugin): prepend/append child block in page

* improve(plugin): polished exceptions message of plugin update/install

* fix(plugin): update settings within gui

* improve(ux): debounce change event of input for plugin settings gui

* chore: rebuild libs core

* enhance(plugin): catch exception of hook plugin
This commit is contained in:
Charlie
2022-04-21 18:43:16 +08:00
committed by GitHub
parent a87e5ea0fa
commit 79bc33e1e3
35 changed files with 3444 additions and 937 deletions

View File

@@ -1,7 +1,13 @@
import { deepMerge, mergeSettingsWithSchema, safetyPathJoin } from './helpers'
import {
deepMerge,
mergeSettingsWithSchema,
safeSnakeCase,
safetyPathJoin,
} from './helpers'
import { LSPluginCaller } from './LSPlugin.caller'
import {
IAppProxy, IDBProxy,
IAppProxy,
IDBProxy,
IEditorProxy,
ILSPluginUser,
LSPluginBaseInfo,
@@ -10,19 +16,33 @@ import {
BlockCommandCallback,
StyleString,
ThemeOptions,
UIOptions, IHookEvent, BlockIdentity,
UIOptions,
IHookEvent,
BlockIdentity,
BlockPageName,
UIContainerAttrs, SimpleCommandCallback, SimpleCommandKeybinding, SettingSchemaDesc, IUserOffHook
UIContainerAttrs,
SimpleCommandCallback,
SimpleCommandKeybinding,
SettingSchemaDesc,
IUserOffHook,
IGitProxy,
IUIProxy,
UserProxyTags,
BlockUUID,
BlockEntity,
IDatom,
IAssetsProxy,
} from './LSPlugin'
import Debug from 'debug'
import * as CSS from 'csstype'
import { snakeCase } from 'snake-case'
import EventEmitter from 'eventemitter3'
import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
declare global {
interface Window {
__LSP__HOST__: boolean
logseq: LSPluginUser
}
}
@@ -34,14 +54,14 @@ const debug = Debug('LSPlugin:user')
* @param opts
* @param action
*/
function registerSimpleCommand (
function registerSimpleCommand(
this: LSPluginUser,
type: string,
opts: {
key: string,
label: string,
desc?: string,
palette?: boolean,
key: string
label: string
desc?: string
palette?: boolean
keybinding?: SimpleCommandKeybinding
},
action: SimpleCommandCallback
@@ -57,40 +77,62 @@ function registerSimpleCommand (
this.caller?.call(`api:call`, {
method: 'register-plugin-simple-command',
args: [this.baseInfo.id, [{ key, label, type, desc, keybinding }, ['editor/hook', eventKey]], palette]
args: [
this.baseInfo.id,
[{ key, label, type, desc, keybinding }, ['editor/hook', eventKey]],
palette,
],
})
}
const app: Partial<IAppProxy> = {
registerCommand: registerSimpleCommand,
registerCommandPalette (
opts: { key: string; label: string, keybinding?: SimpleCommandKeybinding },
action: SimpleCommandCallback) {
registerCommandPalette(
opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
action: SimpleCommandCallback
) {
const { key, label, keybinding } = opts
const group = 'global-palette-command'
const group = '$palette$'
return registerSimpleCommand.call(
this, group,
this,
group,
{ key, label, palette: true, keybinding },
action)
action
)
},
registerUIItem (
registerCommandShortcut(
keybinding: SimpleCommandKeybinding,
action: SimpleCommandCallback
) {
const { binding } = keybinding
const group = '$shortcut$'
const key = group + safeSnakeCase(binding)
return registerSimpleCommand.call(
this,
group,
{ key, palette: false, keybinding },
action
)
},
registerUIItem(
type: 'toolbar' | 'pagebar',
opts: { key: string, template: string }
opts: { key: string; template: string }
) {
const pid = this.baseInfo.id
// opts.key = `${pid}_${opts.key}`
this.caller?.call(`api:call`, {
method: 'register-plugin-ui-item',
args: [pid, type, opts]
args: [pid, type, opts],
})
},
registerPageMenuItem (
registerPageMenuItem(
this: LSPluginUser,
tag: string,
action: (e: IHookEvent & { page: string }) => void
@@ -103,29 +145,34 @@ const app: Partial<IAppProxy> = {
const label = tag
const type = 'page-menu-item'
registerSimpleCommand.call(this,
type, {
key, label
}, action)
registerSimpleCommand.call(
this,
type,
{
key,
label,
},
action
)
},
setFullScreen (flag) {
setFullScreen(flag) {
const sf = (...args) => this._callWin('setFullScreen', ...args)
if (flag === 'toggle') {
this._callWin('isFullScreen').then(r => {
this._callWin('isFullScreen').then((r) => {
r ? sf() : sf(true)
})
} else {
flag ? sf(true) : sf()
}
}
},
}
let registeredCmdUid = 0
const editor: Partial<IEditorProxy> = {
registerSlashCommand (
registerSlashCommand(
this: LSPluginUser,
tag: string,
actions: BlockCommandCallback | Array<SlashCommandAction>
@@ -136,7 +183,7 @@ const editor: Partial<IEditorProxy> = {
actions = [
['editor/clear-current-slash', false],
['editor/restore-saved-cursor'],
['editor/hook', actions]
['editor/hook', actions],
]
}
@@ -169,11 +216,11 @@ const editor: Partial<IEditorProxy> = {
this.caller?.call(`api:call`, {
method: 'register-plugin-slash-command',
args: [this.baseInfo.id, [tag, actions]]
args: [this.baseInfo.id, [tag, actions]],
})
},
registerBlockContextMenuItem (
registerBlockContextMenuItem(
this: LSPluginUser,
tag: string,
action: BlockCommandCallback
@@ -186,30 +233,68 @@ const editor: Partial<IEditorProxy> = {
const label = tag
const type = 'block-context-menu-item'
registerSimpleCommand.call(this,
type, {
key, label
}, action)
registerSimpleCommand.call(
this,
type,
{
key,
label,
},
action
)
},
scrollToBlockInPage (
scrollToBlockInPage(
this: LSPluginUser,
pageName: BlockPageName,
blockId: BlockIdentity
blockId: BlockIdentity,
opts?: { replaceState: boolean }
) {
const anchor = `block-content-` + blockId
this.App.pushState(
'page',
{ name: pageName },
{ anchor }
)
}
if (opts?.replaceState) {
this.App.replaceState('page', { name: pageName }, { anchor })
} else {
this.App.pushState('page', { name: pageName }, { anchor })
}
},
}
const db: Partial<IDBProxy> = {}
const db: Partial<IDBProxy> = {
onBlockChanged(
this: LSPluginUser,
uuid: BlockUUID,
callback: (
block: BlockEntity,
txData: Array<IDatom>,
txMeta?: { outlinerOp: string; [p: string]: any }
) => void
): IUserOffHook {
const pid = this.baseInfo.id
const hook = `hook:db:${safeSnakeCase(`block:${uuid}`)}`
const aBlockChange = ({ block, txData, txMeta }) => {
if (block.uuid !== uuid) {
return
}
callback(block, txData, txMeta)
}
this.caller.on(hook, aBlockChange)
this.App._installPluginHook(pid, hook)
return () => {
this.caller.off(hook, aBlockChange)
this.App._uninstallPluginHook(pid, hook)
}
},
}
const git: Partial<IGitProxy> = {}
const ui: Partial<IUIProxy> = {}
const assets: Partial<IAssetsProxy> = {}
type uiState = {
key?: number,
key?: number
visible: boolean
}
@@ -219,7 +304,12 @@ const KEY_MAIN_UI = 0
* User plugin instance
* @public
*/
export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements ILSPluginUser {
export class LSPluginUser
extends EventEmitter<LSPluginUserEvents>
implements ILSPluginUser {
// @ts-ignore
private _version: string = LIB_VERSION
private _debugTag: string = ''
private _settingsSchema?: Array<SettingSchemaDesc>
private _connected: boolean = false
@@ -229,7 +319,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
*/
private _ui = new Map<number, uiState>()
private readonly _fileStorage: LSPluginFileStorage
private _mFileStorage: LSPluginFileStorage
private _mExperiments: LSPluginExperiments
/**
* handler of before unload plugin
@@ -241,7 +332,7 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
* @param _baseInfo
* @param _caller
*/
constructor (
constructor(
private _baseInfo: LSPluginBaseInfo,
private _caller: LSPluginCaller
) {
@@ -264,22 +355,16 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
const cb = this._beforeunloadCallback
try {
cb && await cb(rest)
cb && (await cb(rest))
actor?.resolve(null)
} catch (e) {
console.debug(`${_caller.debugTag} [beforeunload] `, e)
actor?.reject(e)
}
})
// modules
this._fileStorage = new LSPluginFileStorage(this)
}
async ready (
model?: any,
callback?: any
) {
async ready(model?: any, callback?: any) {
if (this._connected) return
try {
@@ -296,7 +381,8 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
if (this._settingsSchema) {
baseInfo.settings = mergeSettingsWithSchema(
baseInfo.settings, this._settingsSchema
baseInfo.settings,
this._settingsSchema
)
// TODO: sync host settings schema
@@ -304,50 +390,56 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
}
if (baseInfo?.id) {
this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
this._debugTag =
this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
}
await this._execCallableAPIAsync('setSDKMetadata', {
version: this._version,
})
callback && callback.call(this, baseInfo)
} catch (e) {
console.error('[LSPlugin Ready Error]', e)
console.error(`${this._debugTag} [Ready Error]`, e)
}
}
ensureConnected () {
ensureConnected() {
if (!this._connected) {
throw new Error('not connected')
}
}
beforeunload (callback: (e: any) => Promise<void>): void {
beforeunload(callback: (e: any) => Promise<void>): void {
if (typeof callback !== 'function') return
this._beforeunloadCallback = callback
}
provideModel (model: Record<string, any>) {
provideModel(model: Record<string, any>) {
this.caller._extendUserModel(model)
return this
}
provideTheme (theme: ThemeOptions) {
provideTheme(theme: ThemeOptions) {
this.caller.call('provider:theme', theme)
return this
}
provideStyle (style: StyleString) {
provideStyle(style: StyleString) {
this.caller.call('provider:style', style)
return this
}
provideUI (ui: UIOptions) {
provideUI(ui: UIOptions) {
this.caller.call('provider:ui', ui)
return this
}
useSettingsSchema (schema: Array<SettingSchemaDesc>) {
useSettingsSchema(schema: Array<SettingSchemaDesc>) {
if (this.connected) {
this.caller.call('settings:schema', {
schema, isSync: true
schema,
isSync: true,
})
}
@@ -355,48 +447,56 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
return this
}
updateSettings (attrs: Record<string, any>) {
updateSettings(attrs: Record<string, any>) {
this.caller.call('settings:update', attrs)
// TODO: update associated baseInfo settings
}
onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook {
onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
const type = 'settings:changed'
this.on(type, cb)
return () => this.off(type, cb)
}
showSettingsUI () {
showSettingsUI() {
this.caller.call('settings:visible:changed', { visible: true })
}
hideSettingsUI () {
hideSettingsUI() {
this.caller.call('settings:visible:changed', { visible: false })
}
setMainUIAttrs (attrs: Partial<UIContainerAttrs>): void {
setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
this.caller.call('main-ui:attrs', attrs)
}
setMainUIInlineStyle (style: CSS.Properties): void {
setMainUIInlineStyle(style: CSS.Properties): void {
this.caller.call('main-ui:style', style)
}
hideMainUI (opts?: { restoreEditingCursor: boolean }): void {
const payload = { key: KEY_MAIN_UI, visible: false, cursor: opts?.restoreEditingCursor }
hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
const payload = {
key: KEY_MAIN_UI,
visible: false,
cursor: opts?.restoreEditingCursor,
}
this.caller.call('main-ui:visible', payload)
this.emit('ui:visible:changed', payload)
this._ui.set(payload.key, payload)
}
showMainUI (opts?: { autoFocus: boolean }): void {
const payload = { key: KEY_MAIN_UI, visible: true, autoFocus: opts?.autoFocus }
showMainUI(opts?: { autoFocus: boolean }): void {
const payload = {
key: KEY_MAIN_UI,
visible: true,
autoFocus: opts?.autoFocus,
}
this.caller.call('main-ui:visible', payload)
this.emit('ui:visible:changed', payload)
this._ui.set(payload.key, payload)
}
toggleMainUI (): void {
toggleMainUI(): void {
const payload = { key: KEY_MAIN_UI, toggle: true }
const state = this._ui.get(payload.key)
if (state && state.visible) {
@@ -406,28 +506,32 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
}
}
get isMainUIVisible (): boolean {
get version(): string {
return this._version
}
get isMainUIVisible(): boolean {
const state = this._ui.get(KEY_MAIN_UI)
return Boolean(state && state.visible)
}
get connected (): boolean {
get connected(): boolean {
return this._connected
}
get baseInfo (): LSPluginBaseInfo {
get baseInfo(): LSPluginBaseInfo {
return this._baseInfo
}
get settings () {
get settings() {
return this.baseInfo?.settings
}
get caller (): LSPluginCaller {
get caller(): LSPluginCaller {
return this._caller
}
resolveResourceFullUrl (filePath: string) {
resolveResourceFullUrl(filePath: string) {
this.ensureConnected()
if (!filePath) return
filePath = filePath.replace(/^[.\\/]+/, '')
@@ -437,20 +541,17 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
/**
* @internal
*/
_makeUserProxy (
target: any,
tag?: 'app' | 'editor' | 'db'
) {
_makeUserProxy(target: any, tag?: UserProxyTags) {
const that = this
const caller = this.caller
return new Proxy(target, {
get (target: any, propKey, receiver) {
get(target: any, propKey, receiver) {
const origMethod = target[propKey]
return function (this: any, ...args: any) {
if (origMethod) {
const ret = origMethod.apply(that, args)
const ret = origMethod.apply(that, args.concat(tag))
if (ret !== PROXY_CONTINUE) return
}
@@ -462,50 +563,96 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
const f = hookMatcher[0].toLowerCase()
const s = hookMatcher.input!
const e = s.slice(f.length)
const isOff = f === 'off'
const pid = that.baseInfo.id
const type = `hook:${tag}:${snakeCase(e)}`
const type = `hook:${tag}:${safeSnakeCase(e)}`
const handler = args[0]
caller[f](type, handler)
return f !== 'off' ? () => (caller.off(type, handler)) : void 0
if (isOff) {
return () => {
caller.off(type, handler)
that.App._uninstallPluginHook(pid, type)
}
} else {
return that.App._installPluginHook(pid, type)
}
}
}
let method = propKey as string
if ((['git', 'ui', 'assets'] as UserProxyTags[]).includes(tag)) {
method = tag + '_' + method
}
// Call host
return caller.callAsync(`api:call`, {
tag, method: propKey, args: args
tag,
method,
args: args,
})
}
}
},
})
}
/**
* @param args
*/
_callWin (...args) {
_execCallableAPIAsync(method, ...args) {
return this._caller.callAsync(`api:call`, {
method: '_callMainWin',
args: args
method,
args,
})
}
_execCallableAPI(method, ...args) {
this._caller.call(`api:call`, {
method,
args,
})
}
_callWin(...args) {
return this._execCallableAPIAsync(`_callMainWin`, ...args)
}
/**
* The interface methods of {@link IAppProxy}
*/
get App (): IAppProxy {
get App(): IAppProxy {
return this._makeUserProxy(app, 'app')
}
get Editor (): IEditorProxy {
get Editor(): IEditorProxy {
return this._makeUserProxy(editor, 'editor')
}
get DB (): IDBProxy {
return this._makeUserProxy(db)
get DB(): IDBProxy {
return this._makeUserProxy(db, 'db')
}
get FileStorage (): LSPluginFileStorage {
return this._fileStorage
get Git(): IGitProxy {
return this._makeUserProxy(git, 'git')
}
get UI(): IUIProxy {
return this._makeUserProxy(ui, 'ui')
}
get Assets(): IAssetsProxy {
return this._makeUserProxy(assets, 'assets')
}
get FileStorage(): LSPluginFileStorage {
let m = this._mFileStorage
if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
return m
}
get Experiments(): LSPluginExperiments {
let m = this._mExperiments
if (!m) m = this._mExperiments = new LSPluginExperiments(this)
return m
}
}
@@ -514,15 +661,15 @@ export * from './LSPlugin'
/**
* @internal
*/
export function setupPluginUserInstance (
export function setupPluginUserInstance(
pluginBaseInfo: LSPluginBaseInfo,
pluginCaller: LSPluginCaller
) {
return new LSPluginUser(pluginBaseInfo, pluginCaller)
}
if (window.__LSP__HOST__ == null) { // Entry of iframe mode
// entry of iframe mode
if (window.__LSP__HOST__ == null) {
const caller = new LSPluginCaller(null)
// @ts-ignore
window.logseq = setupPluginUserInstance({} as any, caller)
}