Enhance / Plugin APIs (#6945)

Added
1. All configurations of current graph.
App.getCurrentGraphConfigs: () => Promise<any>
2. All favorite pages list of current graph.
App.getCurrentGraphFavorites: () => Promise<Array<string> | null>
3. All recent pages list of current graph.
App.getCurrentGraphRecent: () => Promise<Array<string> | null>
4. Clear right sidebar blocks.
App.clearRightSidebarBlocks: (opts?: { close: boolean }) => void
5. Support register CodeMirror enhancer. #Experiment feature
Experiments.registerExtensionsEnhancer<T = any>(type: 'katex' | 'codemirror', enhancer: (v: T) => Promise<any>)
6. Support hooks for app search service. #Alpha stage
App.registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void
7. Support focus option for App.insertBlock. Credit to [[tennox]] #PR

Fixed
1. Adjust build script to be compatible for shadow-cljs bundler.
How to set up a clojurescript project with shadow-cljs?
https://github.com/rlhk/logseq-url-plus/blob/main/doc/dev-notes.md
This commit is contained in:
Charlie
2022-11-22 22:00:29 +08:00
committed by GitHub
parent 0e84052310
commit dda1f9bd9f
32 changed files with 2264 additions and 293 deletions

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import {
createRandomPage, randomInt, randomInsert, randomEditDelete, randomEditMoveUpDown, IsMac, randomString,
createRandomPage, randomInt, IsMac, randomString,
} from './utils'
/**
@@ -175,7 +175,5 @@ test('Random editor operations', async ({ page, block }) => {
// FIXME: CHECK await block.waitForBlocks(expectedBlocks)
await page.waitForTimeout(50)
}
})

View File

@@ -2,7 +2,7 @@ import { Page, Locator } from 'playwright'
import { expect, ConsoleMessage } from '@playwright/test'
import * as process from 'process'
import { Block } from './types'
import pathlib from 'path'
import * as pathlib from 'path'
export const IsMac = process.platform === 'darwin'
export const IsLinux = process.platform === 'linux'

View File

@@ -1,3 +1,4 @@
src/
webpack.*
.DS_Store
docs/

30
libs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,30 @@
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
## [0.0.11]
### Added
- All configurations of current graph.
`App.getCurrentGraphConfigs: () => Promise<any>`
- All favorite pages list of current graph.
`App.getCurrentGraphFavorites: () => Promise<Array<string> | null>`
- All recent pages list of current graph.
`App.getCurrentGraphRecent: () => Promise<Array<string> | null>`
- Clear right sidebar blocks.
`App.clearRightSidebarBlocks: (opts?: { close: boolean }) => void`
- Support register `CodeMirror` enhancer. _#Experiment feature_
`Experiments.registerExtensionsEnhancer<T = any>(type: 'katex' | 'codemirror', enhancer: (v: T) => Promise<any>)`
- Support hooks for app search service. _#Alpha stage_
`App.registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void`
- Support `focus` option for `App.insertBlock`. Credit
to [[[tennox](https://github.com/tennox)]] [#PR](https://github.com/logseq/logseq/commit/4217057a44de65e5c64be37857af2fb4e9534b24)
### Fixed
- Adjust build script to be compatible for `shadow-cljs` bundler.
> How to set up a clojurescript project with shadow-cljs?
> https://github.com/rlhk/logseq-url-plus/blob/main/doc/dev-notes.md

10
libs/babel.config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": "chrome 72"
}
]
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@logseq/libs",
"version": "0.0.10",
"version": "0.0.11",
"description": "Logseq SDK libraries",
"main": "dist/lsplugin.user.js",
"typings": "index.d.ts",
@@ -12,7 +12,8 @@
"dev:core": "npm run build:core -- --mode development --watch",
"build": "tsc && rm dist/*.js && npm run build:user",
"lint": "prettier --check \"src/**/*.{ts, js}\"",
"fix": "prettier --write \"src/**/*.{ts, js}\""
"fix": "prettier --write \"src/**/*.{ts, js}\"",
"build:docs": "typedoc --plugin typedoc-plugin-lsp-docs src/LSPlugin.user.ts && typedoc --json docs/out.json ./src/LSPlugin.user.ts"
},
"dependencies": {
"csstype": "3.1.0",
@@ -25,12 +26,18 @@
"snake-case": "3.0.4"
},
"devDependencies": {
"@babel/core": "^7.20.2",
"@babel/preset-env": "^7.20.2",
"@types/debug": "^4.1.5",
"@types/dompurify": "2.3.3",
"@types/lodash-es": "4.17.6",
"babel-loader": "^9.1.0",
"prettier": "^2.6.2",
"prettier-config-standard": "^5.0.0",
"terser-webpack-plugin": "^5.3.6",
"ts-loader": "9.3.0",
"typedoc": "^0.23.17",
"typedoc-plugin-lsp-docs": "^0.0.1",
"typescript": "4.7.3",
"webpack": "5.73.0",
"webpack-bundle-analyzer": "4.5.0",

View File

@@ -279,6 +279,7 @@ class LSPluginCaller extends EventEmitter {
debug(`[user -> *host] `, type, payload)
this._pluginLocal?.emit(type, payload || {})
this._pluginLocal?.caller.emit(type, payload || {})
})
this._call = async (...args: any) => {

View File

@@ -139,7 +139,12 @@ class PluginLogger extends EventEmitter<'change'> {
super()
}
write(type: string, payload: any[]) {
write(type: string, payload: any[], inConsole?: boolean) {
if (payload?.length && (true === payload[payload.length - 1])) {
inConsole = true
payload.pop()
}
const msg = payload.reduce((ac, it) => {
if (it && it instanceof Error) {
ac += `${it.message} ${it.stack}`
@@ -150,6 +155,11 @@ class PluginLogger extends EventEmitter<'change'> {
}, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
this._logs.push([type, msg])
if (inConsole) {
console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
}
this.emit('change')
}
@@ -907,9 +917,9 @@ class PluginLocal extends EventEmitter<'loaded'
this._dispose(cleanInjectedScripts.bind(this))
} catch (e) {
console.error('[Load Plugin Error] ', e)
this.logger?.error(e)
this.logger?.error('[Load Plugin]', e, true)
this.dispose().catch(null)
this._status = PluginLocalLoadStatus.ERROR
this._loadErr = e
} finally {
@@ -1329,7 +1339,6 @@ class LSPluginCore
}
}
pluginLocal.settings?.on('change', (a) => {
this.emit('settings-changed', pluginLocal.id, a)
pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })

View File

@@ -289,6 +289,34 @@ export type ExternalCommandType =
export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
export type SearchIndiceInitStatus = boolean
export type SearchBlockItem = { id: EntityID, uuid: BlockIdentity, content: string, page: EntityID }
export type SearchPageItem = string
export type SearchFileItem = string
export interface IPluginSearchServiceHooks {
name: string
options?: Record<string, any>
onQuery: (
graph: string,
key: string,
opts: Partial<{ limit: number }>
) =>
Promise<{
graph: string,
key: string,
blocks?: Array<Partial<SearchBlockItem>>,
pages?: Array<SearchPageItem>,
files?: Array<SearchFileItem>
}>
onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
onIndiceReset: (graph: string) => Promise<void>
onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<BlockEntity> }) => Promise<void>
onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
}
/**
* App level APIs
*/
@@ -302,6 +330,9 @@ export interface IAppProxy {
getUserInfo: () => Promise<AppUserInfo | null>
getUserConfigs: () => Promise<AppUserConfigs>
// services
registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void
// commands
registerCommand: (
type: string,
@@ -352,6 +383,7 @@ export interface IAppProxy {
* @param path
*/
getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
setStateFromStore: (path: string | Array<string>, value: any) => Promise<void>
// native
relaunch: () => Promise<void>
@@ -367,6 +399,9 @@ export interface IAppProxy {
// graph
getCurrentGraph: () => Promise<AppGraphInfo | null>
getCurrentGraphConfigs: () => Promise<any>
getCurrentGraphFavorites: () => Promise<Array<string> | null>
getCurrentGraphRecent: () => Promise<Array<string> | null>
// router
pushState: (
@@ -403,6 +438,7 @@ export interface IAppProxy {
setFullScreen: (flag: boolean | 'toggle') => void
setLeftSidebarVisible: (flag: boolean | 'toggle') => void
setRightSidebarVisible: (flag: boolean | 'toggle') => void
clearRightSidebarBlocks: (opts?: { close: boolean }) => void
registerUIItem: (
type: 'toolbar' | 'pagebar',
@@ -805,14 +841,14 @@ export interface IAssetsProxy {
* @added 0.0.2
* @param exts
*/
listFilesOfCurrentGraph(exts?: string | string[]): Promise<{
listFilesOfCurrentGraph(exts?: string | string[]): Promise<Array<{
path: string
size: number
accessTime: number
modifiedTime: number
changeTime: number
birthTime: number
}>
}>>
/**
* @example https://github.com/logseq/logseq/pull/6488

View File

@@ -33,7 +33,7 @@ import {
BlockEntity,
IDatom,
IAssetsProxy,
AppInfo,
AppInfo, IPluginSearchServiceHooks,
} from './LSPlugin'
import Debug from 'debug'
import * as CSS from 'csstype'
@@ -41,6 +41,7 @@ import EventEmitter from 'eventemitter3'
import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
import { LSPluginRequest } from './modules/LSPlugin.Request'
import { LSPluginSearchService } from './modules/LSPlugin.Search'
declare global {
interface Window {
@@ -86,13 +87,15 @@ function registerSimpleCommand(
method: 'register-plugin-simple-command',
args: [
this.baseInfo.id,
[{ key, label, type, desc, keybinding, extras}, ['editor/hook', eventKey]],
[{ key, label, type, desc, keybinding, extras }, ['editor/hook', eventKey]],
palette,
],
})
}
let _appBaseInfo: AppInfo = null
let _searchServices: Map<string, LSPluginSearchService> = new Map()
const app: Partial<IAppProxy> = {
async getInfo(
this: LSPluginUser,
@@ -106,6 +109,17 @@ const app: Partial<IAppProxy> = {
registerCommand: registerSimpleCommand,
registerSearchService<T extends IPluginSearchServiceHooks>(
this: LSPluginUser,
s: T
) {
if (_searchServices.has(s.name)) {
throw new Error(`SearchService: #${s.name} has registered!`)
}
_searchServices.set(s.name, new LSPluginSearchService(this, s))
},
registerCommandPalette(
opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
action: SimpleCommandCallback
@@ -356,7 +370,11 @@ type uiState = {
const KEY_MAIN_UI = 0
/**
* User plugin instance
* User plugin instance from global namespace `logseq`.
* @example
* ```ts
* logseq.UI.showMsg('Hello, Logseq')
* ```
* @public
*/
export class LSPluginUser
@@ -420,6 +438,7 @@ export class LSPluginUser
})
}
// Life related
async ready(model?: any, callback?: any) {
if (this._connected) return
@@ -495,6 +514,7 @@ export class LSPluginUser
return this
}
// Settings related
useSettingsSchema(schema: Array<SettingSchemaDesc>) {
if (this.connected) {
this.caller.call('settings:schema', {
@@ -526,6 +546,7 @@ export class LSPluginUser
this.caller.call('settings:visible:changed', { visible: false })
}
// UI related
setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
this.caller.call('main-ui:attrs', attrs)
}
@@ -566,6 +587,7 @@ export class LSPluginUser
}
}
// Getters
get version(): string {
return this._version
}

View File

@@ -60,7 +60,7 @@ export class LSPluginExperiments {
}
registerExtensionsEnhancer<T = any>(
type: 'katex',
type: 'katex' | 'codemirror',
enhancer: (v: T) => Promise<any>
) {
const host = this.ensureHostScope()

View File

@@ -0,0 +1,82 @@
import { IPluginSearchServiceHooks } from '../LSPlugin'
import { LSPluginUser } from '../LSPlugin.user'
import { isArray, isFunction, mapKeys } from 'lodash-es'
export class LSPluginSearchService {
/**
* @param ctx
* @param serviceHooks
*/
constructor(
private ctx: LSPluginUser,
private serviceHooks: IPluginSearchServiceHooks
) {
ctx._execCallableAPI(
'register-search-service',
ctx.baseInfo.id,
serviceHooks.name,
serviceHooks.options
)
// hook events TODO: remove listeners
const wrapHookEvent = (k) => `service:search:${k}:${serviceHooks.name}`
Object.entries(
{
query: {
f: 'onQuery', args: ['graph', 'q', true], reply: true,
transformOutput: (data: any) => {
// TODO: transform keys?
if (isArray(data?.blocks)) {
data.blocks = data.blocks.map(it => {
return it && mapKeys(it, (_, k) => `block/${k}`)
})
}
return data
}
},
rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] },
transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] },
truncateBlocks: { f: 'onIndiceReset', args: ['graph'] },
removeDb: { f: 'onGraph', args: ['graph'] }
}
).forEach(
([k, v]) => {
const hookEvent = wrapHookEvent(k)
ctx.caller.on(hookEvent, async (payload: any) => {
if (isFunction(serviceHooks?.[v.f])) {
let ret = null
try {
ret = await serviceHooks[v.f].apply(
serviceHooks, (v.args || []).map((prop: any) => {
if (!payload) return
if (prop === true) return payload
if (payload.hasOwnProperty(prop)) {
const ret = payload[prop]
delete payload[prop]
return ret
}
})
)
if (v.transformOutput) {
ret = v.transformOutput(ret)
}
} catch (e) {
console.error('[SearchService] ', e)
ret = e
} finally {
if (v.reply) {
ctx.caller.call(
`${hookEvent}:reply`, ret
)
}
}
}
})
})
}
}

View File

@@ -2,6 +2,7 @@ const pkg = require('./package.json')
const path = require('path')
const webpack = require('webpack')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
entry: './src/LSPlugin.user.ts',
@@ -9,14 +10,27 @@ module.exports = {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
use: [
{
loader: 'babel-loader'
},
{
loader: 'ts-loader'
}
],
exclude: /node_modules/,
},
}
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin()
]
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,9 +39,9 @@
api #(str "https://api.github.com/repos/" repo "/" %)
endpoint (api url-suffix)
^js res (fetch endpoint)
_ (debug "[Release URL] " endpoint "[Response Status/Text]" (.-status res) "-")
res (response-transform res)
res (.json res)
_ (debug "[Release URL] " endpoint)
res (bean/->clj res)
version (:tag_name res)
asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]

View File

@@ -3,7 +3,10 @@
[rum.core :as rum]
[shadow.lazy :as lazy]
[frontend.ui :as ui]
[frontend.state :as state]))
[frontend.config :as config]
[frontend.state :as state]
[frontend.handler.plugin :refer [hook-extensions-enhancer-by-type]]
[promesa.core :as p]))
;; TODO: Why does shadow fail when code is required
#_:clj-kondo/ignore
@@ -11,17 +14,28 @@
(defonce loaded? (atom false))
(rum/defc editor < rum/reactive
{:will-mount (fn [state]
(lazy/load lazy-editor
(fn []
(reset! loaded? true)))
state)}
(rum/defc editor <
rum/reactive
{:will-mount
(fn [state]
(lazy/load lazy-editor
(fn []
(if-not @loaded?
(p/finally
(p/all (when-let [enhancers (and config/lsp-enabled?
(seq (hook-extensions-enhancer-by-type :codemirror)))]
(for [{f :enhancer} enhancers]
(when (fn? f) (f (. js/window -CodeMirror))))))
(fn []
(-> (p/delay 200)
(p/then #(reset! loaded? true)))))
(reset! loaded? true))))
state)}
[config id attr code options]
(let [loaded? (rum/react loaded?)
theme (state/sub :ui/theme)
code (or code "")
code (string/replace-first code #"\n$" "")] ;; See-also: #3410
theme (state/sub :ui/theme)
code (or code "")
code (string/replace-first code #"\n$" "")] ;; See-also: #3410
(if loaded?
(@lazy-editor config id attr code theme options)
(ui/loading "CodeMirror"))))

View File

@@ -231,25 +231,33 @@
(highlight-exact-query data search-q))
:block
(let [{:block/keys [page uuid]} data ;; content here is normalized
(let [{:block/keys [page uuid content]} data ;; content here is normalized
page (util/get-page-original-name page)
repo (state/sub :git/current-repo)
format (db/get-page-format page)
block (model/query-block-by-uuid uuid)
content (:block/content block)]
block (when-not (string/blank? uuid)
(model/query-block-by-uuid uuid))
content' (if block (:block/content block) content)]
[:span {:data-block-ref uuid}
(search-result-item {:name "block"
:title (t :search-item/block)
:extension? true}
(if block
(block-search-result-item repo uuid format content search-q search-mode)
(cond
(some? block)
(block-search-result-item repo uuid format content' search-q search-mode)
(not (string/blank? content'))
content'
:else
(do (log/error "search result with non-existing uuid: " data)
(str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
nil)]))
(rum/defc search-auto-complete
[{:keys [pages files blocks has-more?] :as result} search-q all?]
[{:keys [engine pages files blocks has-more?] :as result} search-q all?]
(let [pages (when-not all? (map (fn [page]
(let [alias (model/get-redirect-page-name page)]
(cond->
@@ -264,6 +272,7 @@
blocks (map (fn [block] {:type :block :data block}) blocks)
search-mode (state/sub :search/mode)
new-page (if (or
(some? engine)
(and (seq pages)
(= (util/safe-page-name-sanity-lc search-q)
(util/safe-page-name-sanity-lc (:data (first pages)))))
@@ -401,10 +410,13 @@
state
:on-hide (fn []
(search-handler/clear-search!)))))
(rum/local nil ::active-engine-tab)
[state]
(let [search-result (state/sub :search/result)
search-q (state/sub :search/q)
search-mode (state/sub :search/mode)
engines (state/sub :search/engines)
*active-engine-tab (::active-engine-tab state)
timeout 300
in-page-search? (= search-mode :page)]
[:div.cp__palette.cp__palette-main
@@ -421,7 +433,12 @@
(default-placeholder search-mode))
:auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
:value search-q
:on-change (fn [e]
:on-key-down (fn [^js e]
(when (= 27 (.-keyCode e))
(when-not (string/blank? search-q)
(util/stop e)
(search-handler/clear-search!))))
:on-change (fn [^js e]
(when @search-timeout
(js/clearTimeout @search-timeout))
(let [value (util/evalue e)
@@ -443,9 +460,35 @@
(search-handler/search (state/get-current-repo) value)))
timeout))))))}]]
[:div.search-results-wrap
(if (seq search-result)
(search-auto-complete search-result search-q false)
(recent-search-and-pages in-page-search?))]]]))
;; list registered search engines
(when (seq engines)
[:ul.search-results-engines-tabs
[:li
{:class (when-not @*active-engine-tab "is-active")}
(ui/button
[:span.flex.items-center
(svg/logo 14) [:span.pl-2 "Default"]]
:background "orange"
:on-click #(reset! *active-engine-tab nil))]
(for [[k v] engines]
[:li
{:key k
:class (if (= k @*active-engine-tab) "is-active" "")}
(ui/button [:span.flex.items-center
[:span.pr-2 (ui/icon "puzzle")]
(:name v)
(when-let [result (and v (:result v))]
(str " (" (count (:blocks result)) ")"))]
:on-click #(reset! *active-engine-tab k))])])
(if-not (nil? @*active-engine-tab)
(let [active-engine-result (get-in engines [@*active-engine-tab :result])]
(search-auto-complete
(merge active-engine-result {:engine @*active-engine-tab}) search-q false))
(if (seq search-result)
(search-auto-complete search-result search-q false)
(recent-search-and-pages in-page-search?)))]]]))
(rum/defc more < rum/reactive
[route]

View File

@@ -12,21 +12,45 @@
}
#search-wrapper svg {
color: var(--ls-search-icon-color, #9fa6b2);
opacity: 0.6;
transition: .3s;
color: var(--ls-search-icon-color, #9fa6b2);
opacity: 0.6;
transition: .3s;
}
#search-wrapper:hover svg, #search-wrapper:focus-within svg {
color: var(--ls-link-text-hover-color, #4b5563);
opacity: 0.8;
color: var(--ls-link-text-hover-color, #4b5563);
opacity: 0.8;
}
#search-wrapper {
transition: .3s;
padding-right: 12px;
transition: .3s;
padding-right: 12px;
}
.search-item svg {
transform: scale(0.8);
transform: scale(0.8);
}
.search-results-engines {
&-tabs {
@apply flex list-none -mx-2 mb-2;
background: var(--ls-primary-background-color);
> li {
@apply p-0 m-0 min-w-[150px] flex-1;
.ui__button {
@apply w-full h-full justify-center border;
background: transparent;
border-radius: 0;
line-height: 1;
}
&.is-active .ui__button {
background: var(--ls-quaternary-background-color);
}
}
}
}

View File

@@ -166,25 +166,26 @@
{:d "M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
:fill-rule "evenodd"}]])
(rum/defc logo
[_dark?]
[:svg
{:fill "currentColor", :view-box "0 0 21 21", :height "21", :width "21"}
[:ellipse
{:transform
"matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
:ry "2.04373"
:rx "3.29236"}]
[:ellipse
{:transform
"matrix(-0.495846 0.868411 -0.825718 -0.564084 3.97209 5.54515)"
:ry "3.37606"
:rx "2.95326"}]
[:ellipse
{:transform
"matrix(0.987073 0.160274 -0.239143 0.970984 13.0843 14.72)"
:ry "6.13006"
:rx "7.78547"}]])
(defn logo
([] (logo 20))
([size]
[:svg
{:fill "currentColor", :view-box "0 0 21 21", :height size, :width size}
[:ellipse
{:transform
"matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
:ry "2.04373"
:rx "3.29236"}]
[:ellipse
{:transform
"matrix(-0.495846 0.868411 -0.825718 -0.564084 3.97209 5.54515)"
:ry "3.37606"
:rx "2.95326"}]
[:ellipse
{:transform
"matrix(0.987073 0.160274 -0.239143 0.970984 13.0843 14.72)"
:ry "6.13006"
:rx "7.78547"}]]))
(def page
[:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}

View File

@@ -146,6 +146,9 @@
(def textarea-ref-name "textarea")
(def codemirror-ref-name "codemirror-instance")
;; export CodeMirror to global scope
(set! js/window -CodeMirror cm)
(defn- extra-codemirror-options []
(get (state/get-config)
:editor/extra-codemirror-options {}))

View File

@@ -301,9 +301,9 @@
(defn <request [api-name & args]
(let [name (str api-name (.now js/Date))]
(go (swap! *on-flying-request conj name)
(let [r (<! (apply <request* api-name args))]
(swap! *on-flying-request disj name)
r))))
(let [r (<! (apply <request* api-name args))]
(swap! *on-flying-request disj name)
r))))
(defn- remove-dir-prefix [dir path]
(let [r (string/replace path (js/RegExp. (str "^" (gstring/regExpEscape dir))) "")]
@@ -327,18 +327,18 @@
(defn relative-path [o]
(let [repo-dir (config/get-repo-dir (state/get-current-repo))]
(cond
(implements? IRelativePath o)
(-relative-path o)
(implements? IRelativePath o)
(-relative-path o)
;; full path
(and (string? o) (string/starts-with? o repo-dir))
(string/replace o (str repo-dir "/") "")
;; full path
(and (string? o) (string/starts-with? o repo-dir))
(string/replace o (str repo-dir "/") "")
(string? o)
(remove-user-graph-uuid-prefix o)
(string? o)
(remove-user-graph-uuid-prefix o)
:else
(throw (js/Error. (str "unsupport type " (str o)))))))
:else
(throw (js/Error. (str "unsupport type " (str o)))))))
(defprotocol IChecksum
(-checksum [this]))
@@ -347,7 +347,7 @@
(-stop! [this]))
(defprotocol IStopped?
(-stopped? [this]))
;from-path, to-path is relative path
;from-path, to-path is relative path
(deftype FileTxn [from-path to-path updated? deleted? txid checksum]
Object
(renamed? [_]
@@ -392,19 +392,19 @@
(let [update? (= "update_files" TXType)
delete? (= "delete_files" TXType)
update-xf
(comp
(remove #(or (empty? (first %))
(empty? (last %))))
(map #(->FileTxn (first %) (first %) update? delete? TXId (last %))))
(comp
(remove #(or (empty? (first %))
(empty? (last %))))
(map #(->FileTxn (first %) (first %) update? delete? TXId (last %))))
delete-xf
(comp
(remove #(empty? (first %)))
(map #(->FileTxn (first %) (first %) update? delete? TXId nil)))
(comp
(remove #(empty? (first %)))
(map #(->FileTxn (first %) (first %) update? delete? TXId nil)))
rename-xf
(comp
(remove #(or (empty? (first %))
(empty? (second %))))
(map #(->FileTxn (second %) (first %) false false TXId nil)))
(comp
(remove #(or (empty? (first %))
(empty? (second %))))
(map #(->FileTxn (second %) (first %) false false TXId nil)))
xf (case TXType
"delete_files" delete-xf
"update_files" update-xf
@@ -623,21 +623,21 @@
#{} s1))
(comment
(defn map->FileMetadata [m]
(apply ->FileMetadata ((juxt :size :etag :path :encrypted-path :last-modified :remote? (constantly nil)) m)))
(defn map->FileMetadata [m]
(apply ->FileMetadata ((juxt :size :etag :path :encrypted-path :last-modified :remote? (constantly nil)) m)))
(assert
(=
#{(map->FileMetadata {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2})}
(diff-file-metadata-sets
(into #{}
(map map->FileMetadata)
[{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
{:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2}])
(into #{}
(map map->FileMetadata)
[{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
{:size 1 :etag 1 :path 2 :encrypted-path 2 :last-modified 1}])))))
(assert
(=
#{(map->FileMetadata {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2})}
(diff-file-metadata-sets
(into #{}
(map map->FileMetadata)
[{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
{:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2}])
(into #{}
(map map->FileMetadata)
[{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
{:size 1 :etag 1 :path 2 :encrypted-path 2 :last-modified 1}])))))
(extend-protocol IChecksum
FileMetadata
@@ -1079,8 +1079,8 @@
(go-loop []
(let [{:keys [val stop]}
(async/alt!
debug-print-sync-events-loop-stop-chan {:stop true}
out-ch ([v] {:val v}))]
debug-print-sync-events-loop-stop-chan {:stop true}
out-ch ([v] {:val v}))]
(cond
stop (do (async/unmix-all out-mix)
(doseq [[topic ch] topic&chs]
@@ -1096,28 +1096,28 @@
(comment
;; sub one type event example:
(def c1 (chan 10))
(async/sub sync-events-publication :created-local-version-file c1)
(offer! sync-events-chan {:event :created-local-version-file :data :xxx})
(poll! c1)
;; sub one type event example:
(def c1 (chan 10))
(async/sub sync-events-publication :created-local-version-file c1)
(offer! sync-events-chan {:event :created-local-version-file :data :xxx})
(poll! c1)
;; sub multiple type events example:
;; sub :created-local-version-file and :finished-remote->local events,
;; output into channel c4-out
(def c2 (chan 10))
(def c3 (chan 10))
(def c4-out (chan 10))
(def mix-out (async/mix c4-out))
(async/admix mix-out c2)
(async/admix mix-out c3)
(async/sub sync-events-publication :created-local-version-file c2)
(async/sub sync-events-publication :finished-remote->local c3)
(offer! sync-events-chan {:event :created-local-version-file :data :xxx})
(offer! sync-events-chan {:event :finished-remote->local :data :xxx})
(poll! c4-out)
(poll! c4-out)
)
;; sub multiple type events example:
;; sub :created-local-version-file and :finished-remote->local events,
;; output into channel c4-out
(def c2 (chan 10))
(def c3 (chan 10))
(def c4-out (chan 10))
(def mix-out (async/mix c4-out))
(async/admix mix-out c2)
(async/admix mix-out c3)
(async/sub sync-events-publication :created-local-version-file c2)
(async/sub sync-events-publication :finished-remote->local c3)
(offer! sync-events-chan {:event :created-local-version-file :data :xxx})
(offer! sync-events-chan {:event :finished-remote->local :data :xxx})
(poll! c4-out)
(poll! c4-out)
)
;;; sync events ends
@@ -1189,27 +1189,27 @@
(let [file-meta-list (transient #{})
encrypted-path-list (transient [])
exp-r
(<!
(go-loop [continuation-token nil]
(let [r (<! (.<request this "get_all_files"
(into
{}
(remove (comp nil? second)
{:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
(if (instance? ExceptionInfo r)
r
(let [next-continuation-token (:NextContinuationToken r)
objs (:Objects r)]
(apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
(apply conj! file-meta-list
(map
#(hash-map :checksum (:checksum %)
:encrypted-path (remove-user-graph-uuid-prefix (:Key %))
:size (:Size %)
:last-modified (:LastModified %))
objs))
(when-not (empty? next-continuation-token)
(recur next-continuation-token)))))))]
(<!
(go-loop [continuation-token nil]
(let [r (<! (.<request this "get_all_files"
(into
{}
(remove (comp nil? second)
{:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
(if (instance? ExceptionInfo r)
r
(let [next-continuation-token (:NextContinuationToken r)
objs (:Objects r)]
(apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
(apply conj! file-meta-list
(map
#(hash-map :checksum (:checksum %)
:encrypted-path (remove-user-graph-uuid-prefix (:Key %))
:size (:Size %)
:last-modified (:LastModified %))
objs))
(when-not (empty? next-continuation-token)
(recur next-continuation-token)))))))]
(if (instance? ExceptionInfo exp-r)
exp-r
(let [file-meta-list* (persistent! file-meta-list)
@@ -1278,58 +1278,58 @@
(let [txns-with-encrypted-paths (mapv #(update % :path remove-user-graph-uuid-prefix) (:Transactions r))
encrypted-paths (mapv :path txns-with-encrypted-paths)
encrypted-path->path-map
(zipmap
encrypted-paths
(<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
(zipmap
encrypted-paths
(<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
txns
(mapv
(fn [txn] (update txn :path #(get encrypted-path->path-map %)))
txns-with-encrypted-paths)]
(mapv
(fn [txn] (update txn :path #(get encrypted-path->path-map %)))
txns-with-encrypted-paths)]
txns)))))
(<get-diff [this graph-uuid from-txid]
;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
(user/<wrap-ensure-id&access-token
(let [r (<! (.<request this "get_diff" {:GraphUUID graph-uuid :FromTXId from-txid}))]
(if (instance? ExceptionInfo r)
r
(let [txns-with-encrypted-paths (sort-by :TXId (:Transactions r))
txns-with-encrypted-paths*
(mapv
(fn [txn]
(assoc txn :TXContent
(mapv
(fn [[to-path from-path checksum]]
[(remove-user-graph-uuid-prefix to-path)
(some-> from-path remove-user-graph-uuid-prefix)
checksum])
(:TXContent txn))))
txns-with-encrypted-paths)
(mapv
(fn [txn]
(assoc txn :TXContent
(mapv
(fn [[to-path from-path checksum]]
[(remove-user-graph-uuid-prefix to-path)
(some-> from-path remove-user-graph-uuid-prefix)
checksum])
(:TXContent txn))))
txns-with-encrypted-paths)
encrypted-paths
(mapcat
(fn [txn]
(remove
#(or (nil? %) (not (string/starts-with? % "e.")))
(mapcat
(fn [[to-path from-path _checksum]] [to-path from-path])
(:TXContent txn))))
txns-with-encrypted-paths*)
(mapcat
(fn [txn]
(remove
#(or (nil? %) (not (string/starts-with? % "e.")))
(mapcat
(fn [[to-path from-path _checksum]] [to-path from-path])
(:TXContent txn))))
txns-with-encrypted-paths*)
encrypted-path->path-map
(zipmap
encrypted-paths
(<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
(zipmap
encrypted-paths
(<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
txns
(mapv
(fn [txn]
(assoc
txn :TXContent
(mapv
(fn [[to-path from-path checksum]]
[(get encrypted-path->path-map to-path to-path)
(some->> from-path (get encrypted-path->path-map))
checksum])
(:TXContent txn))))
txns-with-encrypted-paths*)]
(mapv
(fn [txn]
(assoc
txn :TXContent
(mapv
(fn [[to-path from-path checksum]]
[(get encrypted-path->path-map to-path to-path)
(some->> from-path (get encrypted-path->path-map))
checksum])
(:TXContent txn))))
txns-with-encrypted-paths*)]
[txns
(:TXId (last txns))
(:TXId (first txns))])))))
@@ -1470,12 +1470,12 @@
(.-deleted? e) :delete-filetxns
(.renamed? e) :rename-filetxns)) filetxns)
update-file-items (map
(fn [filetxn]
(let [path (relative-path filetxn)]
{:remote->local-type :update
:checksum (-checksum filetxn)
:path path}))
update-filetxns)
(fn [filetxn]
(let [path (relative-path filetxn)]
{:remote->local-type :update
:checksum (-checksum filetxn)
:path path}))
update-filetxns)
rename-file-items (mapcat
(fn [^FileTxn filetxn]
(let [to-path (relative-path filetxn)
@@ -1488,12 +1488,12 @@
:path from-path}]))
rename-filetxns)
delete-file-items (map
(fn [filetxn]
(let [path (relative-path filetxn)]
{:remote->local-type :delete
:checksum (-checksum filetxn)
:path path}))
delete-filetxns)]
(fn [filetxn]
(let [path (relative-path filetxn)]
{:remote->local-type :delete
:checksum (-checksum filetxn)
:path path}))
delete-filetxns)]
(set (concat update-file-items rename-file-items delete-file-items))))
(defn- apply-filetxns
@@ -1528,8 +1528,8 @@
[recent-remote->local-file-item])
(<! (<delete-local-files rsapi graph-uuid base-path [relative-p*]))
(go (<! (timeout 5000))
(swap! *sync-state sync-state--remove-recent-remote->local-files
[recent-remote->local-file-item])))))
(swap! *sync-state sync-state--remove-recent-remote->local-files
[recent-remote->local-file-item])))))
(let [update-local-files-ch (<update-local-files rsapi graph-uuid base-path (map relative-path filetxns))
r (<! (<with-pause update-local-files-ch *paused))]
@@ -1581,8 +1581,8 @@
(not (instance? ExceptionInfo r)))]
;; remove these recent-remote->local-file-items 5s later
(go (<! (timeout 5000))
(swap! *sync-state sync-state--remove-recent-remote->local-files
recent-remote->local-file-items))
(swap! *sync-state sync-state--remove-recent-remote->local-files
recent-remote->local-file-items))
(cond
(instance? ExceptionInfo r) r
@*paused {:pause true}
@@ -1682,7 +1682,7 @@
path (relative-path e)]
{:remote->local-type tp
:checksum (if (= tp :delete) nil
(val (first (<! (get-local-files-checksum graph-uuid (.-dir e) [path])))))
(val (first (<! (get-local-files-checksum graph-uuid (.-dir e) [path])))))
:path path})))
(defn- distinct-file-change-events-xf
@@ -1744,8 +1744,8 @@
(go-loop []
(let [{:keys [rename-event local-change]}
(async/alt!
rename-page-event-chan ([v] {:rename-event v}) ;; {:repo X :old-path X :new-path}
local-changes-chan ([v] {:local-change v}))]
rename-page-event-chan ([v] {:rename-event v}) ;; {:repo X :old-path X :new-path}
local-changes-chan ([v] {:local-change v}))]
(cond
rename-event
(let [repo-dir (config/get-repo-dir (:repo rename-event))
@@ -1758,7 +1758,7 @@
(swap! *rename-events conj k1 k2)
;; remove rename-events after 2s
(go (<! (timeout 3000))
(swap! *rename-events disj k1 k2))
(swap! *rename-events disj k1 k2))
;; add 2 simulated file-watcher events
(>! ch (->FileChangeEvent "unlink" repo-dir (:old-path rename-event*) nil nil))
(>! ch (->FileChangeEvent "add" repo-dir (:new-path rename-event*)
@@ -2200,8 +2200,8 @@
if local-txid != remote-txid, return {:need-sync-remote true}"))
(defrecord ^:large-vars/cleanup-todo
Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *sync-state remoteapi
^:mutable local->remote-syncer *stopped *paused]
Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *sync-state remoteapi
^:mutable local->remote-syncer *stopped *paused]
Object
(set-local->remote-syncer! [_ s] (set! local->remote-syncer s))
(sync-files-remote->local!
@@ -2293,8 +2293,8 @@
diff-remote-files (diff-file-metadata-sets remote-all-files-meta local-all-files-meta)
recent-10-days-range ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
sorted-diff-remote-files
(sort-by
(sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
(sort-by
(sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
remote-graph-info-or-ex (<! (<get-remote-graph remoteapi nil graph-uuid))
latest-txid (:TXId remote-graph-info-or-ex)]
(if (or (instance? ExceptionInfo remote-graph-info-or-ex) (nil? latest-txid))
@@ -2308,7 +2308,7 @@
(swap! *sync-state #(sync-state-reset-full-remote->local-files % sorted-diff-remote-files))
(<! (.sync-files-remote->local!
this (map (juxt relative-path -checksum)
filtered-files)
filtered-files)
latest-txid)))))))))))
(defn- <file-changed?
@@ -2409,17 +2409,17 @@
(comp
(filter
(fn [[path _]]
; 950 = (- 1024 36 36 2)
; 1024 - length of 'user-uuid/graph-uuid/'
; 950 = (- 1024 36 36 2)
; 1024 - length of 'user-uuid/graph-uuid/'
(<= (count (get fnames-map path)) 950)))
(map second))
local-files-meta-map))))
(defrecord ^:large-vars/cleanup-todo
Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
;; control chans
private-immediately-local->remote-chan private-recent-edited-chan]
Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
;; control chans
private-immediately-local->remote-chan private-recent-edited-chan]
Object
(filter-file-change-events-fn [_]
(fn [^FileChangeEvent e]
@@ -2577,21 +2577,21 @@
<!
(sort-by (sort-file-metadata-fn :recent-days-range recent-10-days-range) >))
change-events
(sequence
(comp
;; convert to FileChangeEvent
(map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
{:size (:size %)} (:etag %)))
(remove ignored?))
diff-local-files)
(sequence
(comp
;; convert to FileChangeEvent
(map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
{:size (:size %)} (:etag %)))
(remove ignored?))
diff-local-files)
distinct-change-events (-> (distinct-file-change-events change-events)
filter-upload-files-with-reserved-chars)
_ (swap! *sync-state #(sync-state-reset-full-local->remote-files % distinct-change-events))
change-events-partitions
(sequence
;; partition FileChangeEvents
(partition-file-change-events upload-batch-size)
distinct-change-events)]
(sequence
;; partition FileChangeEvents
(partition-file-change-events upload-batch-size)
distinct-change-events)]
(println "[full-sync(local->remote)]"
(count (flatten change-events-partitions)) "files need to sync and"
(count delete-local-files) "local files need to delete")
@@ -2612,8 +2612,8 @@
[fake-recent-remote->local-file-item])
(<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
(go (<! (timeout 5000))
(swap! *sync-state sync-state--remove-recent-remote->local-files
[fake-recent-remote->local-file-item])))))
(swap! *sync-state sync-state--remove-recent-remote->local-files
[fake-recent-remote->local-file-item])))))
(recur fs)))
;; 2. upload local files
@@ -2633,14 +2633,14 @@
;;; ### put all stuff together
(defrecord ^:large-vars/cleanup-todo
SyncManager [graph-uuid base-path *sync-state
^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
^:mutable ratelimit-local-changes-chan
*txid ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
^:mutable ops-chan
;; control chans
private-full-sync-chan private-stop-sync-chan private-remote->local-sync-chan
private-remote->local-full-sync-chan private-pause-resume-chan]
SyncManager [graph-uuid base-path *sync-state
^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
^:mutable ratelimit-local-changes-chan
*txid ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
^:mutable ops-chan
;; control chans
private-full-sync-chan private-stop-sync-chan private-remote->local-sync-chan
private-remote->local-full-sync-chan private-pause-resume-chan]
Object
(schedule [this next-state args reason]
{:pre [(s/valid? ::state next-state)]}
@@ -2681,19 +2681,19 @@
(go-loop []
(let [{:keys [stop remote->local remote->local-full-sync local->remote-full-sync local->remote resume pause]}
(async/alt!
private-stop-sync-chan {:stop true}
private-remote->local-full-sync-chan {:remote->local-full-sync true}
private-remote->local-sync-chan {:remote->local true}
private-full-sync-chan {:local->remote-full-sync true}
private-pause-resume-chan ([v] (if v {:resume true} {:pause true}))
remote-change-chan ([v] (println "remote change:" v) {:remote->local v})
ratelimit-local-changes-chan ([v]
(let [rest-v (util/drain-chan ratelimit-local-changes-chan)
vs (cons v rest-v)]
(println "local changes:" vs)
{:local->remote vs}))
(timeout (* 20 60 1000)) {:local->remote-full-sync true}
:priority true)]
private-stop-sync-chan {:stop true}
private-remote->local-full-sync-chan {:remote->local-full-sync true}
private-remote->local-sync-chan {:remote->local true}
private-full-sync-chan {:local->remote-full-sync true}
private-pause-resume-chan ([v] (if v {:resume true} {:pause true}))
remote-change-chan ([v] (println "remote change:" v) {:remote->local v})
ratelimit-local-changes-chan ([v]
(let [rest-v (util/drain-chan ratelimit-local-changes-chan)
vs (cons v rest-v)]
(println "local changes:" vs)
{:local->remote vs}))
(timeout (* 20 60 1000)) {:local->remote-full-sync true}
:priority true)]
(cond
stop
(do (util/drain-chan ops-chan)
@@ -2906,13 +2906,13 @@
(.schedule this ::idle nil nil)))))))
(local->remote [this {local-changes :local}]
;; local-changes:: list of FileChangeEvent
;; local-changes:: list of FileChangeEvent
(assert (some? local-changes) local-changes)
(go
(let [distincted-local-changes (distinct-file-change-events local-changes)
_ (swap! *sync-state #(sync-state-reset-full-local->remote-files % distincted-local-changes))
change-events-partitions
(sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
(sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
_ (put-sync-event! {:event :start
:data {:type :local->remote
:graph-uuid graph-uuid
@@ -3071,15 +3071,15 @@
(go
(let [r (<! (<list-remote-graphs remoteapi))
result
(or
;; if api call failed, assume this remote graph still exists
(instance? ExceptionInfo r)
(and
(contains? r :Graphs)
(->> (:Graphs r)
(mapv :GraphUUID)
set
(#(contains? % local-graph-uuid)))))]
(or
;; if api call failed, assume this remote graph still exists
(instance? ExceptionInfo r)
(and
(contains? r :Graphs)
(->> (:Graphs r)
(mapv :GraphUUID)
set
(#(contains? % local-graph-uuid)))))]
(when-not result
(notification/show! (t :file-sync/graph-deleted) :warning false))
@@ -3243,7 +3243,7 @@
;;; add-tap
(comment
(def *x (atom nil))
(add-tap (fn [v] (reset! *x v)))
(def *x (atom nil))
(add-tap (fn [v] (reset! *x v)))
)
)

View File

@@ -329,6 +329,16 @@
(swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
true))
(defn register-plugin-search-service
[pid name opts]
(when-let [pid (and name (keyword pid))]
(state/install-plugin-service pid :search name opts)))
(defn unregister-plugin-search-services
[pid]
(when-let [pid (keyword pid)]
(state/uninstall-plugin-service pid :search)))
(defn unregister-plugin-themes
([pid] (unregister-plugin-themes pid true))
([pid effect]
@@ -564,7 +574,7 @@
(when-not (= text "END")
[:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
[:span.flex.items-center.justify-center.w-60.flex-col
[:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo false)]
[:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo)]
[:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))
(defn ^:large-vars/cleanup-todo init-plugins!
@@ -586,7 +596,8 @@
(invoke-exported-api "unregister_plugin_simple_command" pid)
(invoke-exported-api "uninstall_plugin_hook" pid)
(unregister-plugin-ui-items pid)
(unregister-plugin-resources pid))
(unregister-plugin-resources pid)
(unregister-plugin-search-services pid))
_ (doto js/LSPluginCore
(.on "registered"

View File

@@ -111,7 +111,8 @@
([clear-search-mode?]
(let [m {:search/result nil
:search/q ""}]
(swap! state/state merge m))
(swap! state/state merge m)
(when config/lsp-enabled? (state/reset-plugin-search-engines)))
(when (and clear-search-mode? (not= (state/get-search-mode) :graph))
(state/set-search-mode! :global))))

View File

@@ -84,7 +84,7 @@
(when-not onboarding-and-home?
[:h1.flex.items-center
[:span.scale-75 (svg/logo false)]
[:span.scale-75 (svg/logo)]
[:span.pl-1 "Set up a graph"]])
(case step

View File

@@ -7,9 +7,8 @@
[frontend.db :as db]
[frontend.db.model :as db-model]
[frontend.regex :as regex]
[frontend.search.browser :as search-browser]
[frontend.search.agency :as search-agency]
[frontend.search.db :as search-db :refer [indices]]
[frontend.search.node :as search-node]
[frontend.search.protocol :as protocol]
[frontend.state :as state]
[frontend.util :as util]
@@ -19,9 +18,7 @@
(defn get-engine
[repo]
(if (util/electron?)
(search-node/->Node repo)
(search-browser/->Browser repo)))
(search-agency/->Agency repo))
;; Copied from https://gist.github.com/vaughnd/5099299
(defn str-len-distance

View File

@@ -0,0 +1,52 @@
(ns frontend.search.agency
"Agent entry for search engine impls"
(:require [frontend.search.protocol :as protocol]
[frontend.search.browser :as search-browser]
[frontend.search.node :as search-node]
[frontend.search.plugin :as search-plugin]
[frontend.state :as state]
[frontend.util :as util]))
(defn get-registered-engines
[repo]
(-> (if (util/electron?)
(search-node/->Node repo)
(search-browser/->Browser repo))
(cons
[(when state/lsp-enabled?
(for [s (state/get-all-plugin-services-with-type :search)]
(search-plugin/->Plugin s repo)))])))
(deftype Agency [repo]
protocol/Engine
(query [_this q opts]
(println "D:Search > Query blocks:" repo q opts)
(let [[e1 e2] (get-registered-engines repo)]
(doseq [e e2]
(protocol/query e q opts))
(protocol/query e1 q opts)))
(rebuild-blocks-indice! [_this]
(println "D:Search > Initial blocks indice!:" repo)
(let [[e1 e2] (get-registered-engines repo)]
(doseq [e e2]
(protocol/rebuild-blocks-indice! e))
(protocol/rebuild-blocks-indice! e1)))
(transact-blocks! [_this data]
(println "D:Search > Transact blocks!:" repo)
(doseq [e (flatten (get-registered-engines repo))]
(protocol/transact-blocks! e data)))
(truncate-blocks! [_this]
(println "D:Search > Truncate blocks!" repo)
(doseq [e (flatten (get-registered-engines repo))]
(protocol/truncate-blocks! e)))
(remove-db! [_this]
(println "D:Search > Remove Db!" repo)
(doseq [e (flatten (get-registered-engines repo))]
(protocol/remove-db! e))))

View File

@@ -0,0 +1,41 @@
(ns frontend.search.plugin
"Plugin service implementation of search protocol"
(:require [frontend.state :as state]
[frontend.handler.plugin :as plugin-handler]
[frontend.search.protocol :as protocol]
[cljs-bean.core :as bean]))
(defn call-service!
([service event payload] (call-service! service event payload false))
([service event payload reply?]
(when-let [^js pl (plugin-handler/get-plugin-inst (:pid service))]
(let [{:keys [pid name]} service
hookEvent (str "service:" event ":" name)]
(.call (.-caller pl) hookEvent (bean/->js (merge {:graph (state/get-current-repo)} payload)))
(when reply?
(.once (.-caller pl) (str hookEvent ":reply")
(fn [^js e]
(state/update-plugin-search-engine pid name #(assoc % :result (bean/->clj e))))))))))
(deftype Plugin [service repo]
protocol/Engine
(query [_this q opts]
(call-service! service "search:query" (merge {:q q} opts) true))
(rebuild-blocks-indice! [_this]
;; Not pushing all data for performance temporarily
;;(let [blocks (search-db/build-blocks-indice repo)])
(call-service! service "search:rebuildBlocksIndice" {}))
(transact-blocks! [_this data]
(let [{:keys [blocks-to-remove-set blocks-to-add]} data]
(call-service! service "search:transactBlocks"
{:data {:added blocks-to-add
:removed blocks-to-remove-set}})))
(truncate-blocks! [_this]
(call-service! service "search:truncateBlocks" {}))
(remove-db! [_this]
(call-service! service "search:removeDb" {})))

View File

@@ -54,6 +54,7 @@
:search/mode :global
:search/result nil
:search/graph-filters []
:search/engines {}
;; modals
:modal/dropdowns {}
@@ -188,6 +189,7 @@
:plugin/installed-ui-items {}
:plugin/installed-resources {}
:plugin/installed-hooks {}
:plugin/installed-services {}
:plugin/simple-commands {}
:plugin/selected-theme nil
:plugin/selected-unpacked-pkg nil
@@ -1454,6 +1456,61 @@ Similar to re-frame subscriptions"
[:plugin/installed-resources (keyword pid) (keyword type) key] resource)
resource)))
(defn get-plugin-services
[pid type]
(when-let [installed (and pid (:plugin/installed-services @state))]
(some->> (seq (get installed (keyword pid)))
(filterv #(= type (:type %))))))
(defn install-plugin-service
([pid type name] (install-plugin-service pid type name nil))
([pid type name opts]
(when-let [pid (and pid type name (keyword pid))]
(let [exists (get-plugin-services pid type)]
(when-let [service (and (or (not exists) (not (some #(= name (:name %)) exists)))
{:pid pid :type type :name name :opts opts})]
(update-state! [:plugin/installed-services pid] #(conj (vec %) service))
;; search engines state for results
(when (= type :search)
(set-state! [:search/engines (str pid name)] service)))))))
(defn uninstall-plugin-service
[pid type-or-all]
(when-let [pid (keyword pid)]
(when-let [installed (get (:plugin/installed-services @state) pid)]
(let [remove-all? (or (true? type-or-all) (nil? type-or-all))
remains (if remove-all? nil (filterv #(not= type-or-all (:type %)) installed))
removed (if remove-all? installed (filterv #(= type-or-all (:type %)) installed))]
(set-state! [:plugin/installed-services pid] remains)
;; search engines state for results
(when-let [removed' (seq (filter #(= :search (:type %)) removed))]
(update-state! :search/engines #(apply dissoc % (mapv (fn [{:keys [pid name]}] (str pid name)) removed'))))))))
(defn get-all-plugin-services-with-type
[type]
(when-let [installed (vals (:plugin/installed-services @state))]
(mapcat (fn [s] (filter #(= (keyword type) (:type %)) s)) installed)))
(defn get-all-plugin-search-engines
[]
(:search/engines @state))
(defn update-plugin-search-engine
[pid name f]
(when-let [pid (keyword pid)]
(set-state! :search/engines
(update-vals (get-all-plugin-search-engines)
#(if (and (= pid (:pid %)) (= name (:name %)))
(f %) %)))))
(defn reset-plugin-search-engines
[]
(when-let [engines (get-all-plugin-search-engines)]
(set-state! :search/engines
(update-vals engines #(assoc % :result nil)))))
(defn install-plugin-hook
[pid hook]
(when-let [pid (keyword pid)]

View File

@@ -966,7 +966,7 @@
[text & {:keys [background href class intent on-click small? large? title icon icon-props disabled?]
:or {small? false large? false}
:as option}]
(let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center")
(let [klass (if-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center" intent)
klass (if background (string/replace klass "indigo" background) klass)
klass (if small? (str klass ".px-2.py-1") klass)
klass (if large? (str klass ".text-base") klass)

View File

@@ -13,6 +13,7 @@
[frontend.db.model :as db-model]
[frontend.db.query-dsl :as query-dsl]
[frontend.db.utils :as db-utils]
[frontend.db.react :refer [sub-key-value]]
[frontend.db.query-react :as query-react]
[frontend.fs :as fs]
[frontend.handler.dnd :as editor-dnd-handler]
@@ -94,11 +95,23 @@
(defn ^:export get_state_from_store
[^js path]
(when-let [path (if (string? path) [path] (bean/->clj path))]
(->> path
(map #(if (string/starts-with? % "@")
(subs % 1)
(keyword %)))
(get-in @state/state))))
(some->> path
(map #(if (string/starts-with? % "@")
(subs % 1)
(keyword %)))
(get-in @state/state)
(normalize-keyword-for-json)
(bean/->js))))
(defn ^:export set_state_from_store
[^js path ^js value]
(when-let [path (if (string? path) [path] (bean/->clj path))]
(some->> path
(map #(if (string/starts-with? % "@")
(subs % 1)
(keyword %)))
(into [])
(#(state/set-state! % (bean/->clj value))))))
(defn ^:export get_app_info
;; get app base info
@@ -130,6 +143,20 @@
(normalize-keyword-for-json)
(bean/->js))))
(def ^:export get_current_graph_favorites
(fn []
(some->> (:favorites (state/get-config))
(remove string/blank?)
(filter string?)
(bean/->js))))
(def ^:export get_current_graph_recent
(fn []
(some->> (sub-key-value :recent/pages)
(remove string/blank?)
(filter string?)
(bean/->js))))
(def ^:export get_current_graph
(fn []
(when-let [repo (state/get-current-repo)]
@@ -370,6 +397,14 @@
(js/console.debug :shortcut/unregister-shortcut cmd)
(st/unregister-shortcut! (:handler-id cmd) (:id cmd)))))))
(defn ^:export register_search_service
[pid name ^js opts]
(plugin-handler/register-plugin-search-service pid name (bean/->clj opts)))
(defn ^:export unregister_search_services
[pid]
(plugin-handler/unregister-plugin-search-services pid))
(def ^:export register_plugin_ui_item
(fn [pid type ^js opts]
(when-let [opts (bean/->clj opts)]
@@ -415,6 +450,13 @@
(state/set-state! :ui/sidebar-open? (boolean flag)))
nil))
(def ^:export clear_right_sidebar_blocks
(fn [^js opts]
(state/clear-sidebar-blocks!)
(when-let [opts (and opts (bean/->clj opts))]
(and (:close opts) (state/hide-right-sidebar!)))
nil))
(def ^:export push_state
(fn [^js k ^js params ^js query]
(rfe/push-state
@@ -802,7 +844,11 @@
(when-let [repo (state/get-current-repo)]
(when-let [db (db/get-db repo)]
(let [query (cljs.reader/read-string query)
resolved-inputs (map (comp query-react/resolve-input cljs.reader/read-string) inputs)
resolved-inputs (map #(cond
(string? %)
(some-> % (cljs.reader/read-string) (query-react/resolve-input))
:else %)
inputs)
result (apply d/q query db resolved-inputs)]
(bean/->js (normalize-keyword-for-json result false))))))