Skip to content

Commit

Permalink
feat(client): support ctx.define() and ActionContext
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 29, 2023
1 parent 3d99a52 commit 0f8ccd4
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 53 deletions.
2 changes: 1 addition & 1 deletion packages/client/app/layout/layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<template #right>
<slot name="menu">
<template v-if="typeof menu === 'string'">
<layout-menu-item v-for="item in ctx.menus[menu]" v-bind="{ ...item, ...ctx.actions[item.id]?.[0] }"></layout-menu-item>
<layout-menu-item v-for="item in ctx.internal.menus[menu]" v-bind="{ ...item, ...ctx.internal.actions[item.id]?.[0] }"></layout-menu-item>
</template>
<template v-else>
<layout-menu-item v-for="item in menu" v-bind="item" />
Expand Down
15 changes: 11 additions & 4 deletions packages/client/app/layout/menu-item.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
<template>
<el-tooltip :disabled="disabled" :content="toValue(label)" placement="bottom">
<span class="menu-item" :class="[type, { disabled }]" @click="action()">
<span class="menu-item" :class="[type, { disabled }]" @click="action(ctx.internal.createScope())">
<k-icon class="menu-icon" :name="toValue(icon)"></k-icon>
</span>
</el-tooltip>
</template>

<script lang="ts" setup>
import { LegacyMenuItem } from '@koishijs/client'
import { computed, toValue } from 'vue'
import { LegacyMenuItem, MaybeGetter, useContext } from '@koishijs/client'
import { computed } from 'vue'
const props = defineProps<LegacyMenuItem>()
const disabled = computed(() => toValue(props.disabled))
const ctx = useContext()
const disabled = computed(() => props.disabled ? toValue(props.disabled) : true)
function toValue<T>(getter: MaybeGetter<T>): T {
if (typeof getter !== 'function') return getter
return (getter as any)(ctx.internal.createScope())
}
</script>
4 changes: 2 additions & 2 deletions packages/client/app/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function (ctx: Context) {
component: Theme,
})

ctx.extendSettings({
ctx.settings({
id: '',
title: '通用设置',
order: 1000,
Expand All @@ -27,7 +27,7 @@ export default function (ctx: Context) {
}).description('通用设置'),
})

ctx.extendSettings({
ctx.settings({
id: 'appearance',
title: '外观设置',
order: 1000,
Expand Down
10 changes: 5 additions & 5 deletions packages/client/app/settings/settings.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<k-layout main="page-settings">
<template #header>
{{ ctx.settings[path][0]?.title }}
{{ ctx.internal.settings[path][0]?.title }}
</template>

<template #left>
Expand All @@ -17,7 +17,7 @@

<keep-alive>
<k-content :key="path">
<template v-for="item of ctx.settings[path]">
<template v-for="item of ctx.internal.settings[path]">
<component v-if="item.component" :is="item.component" />
<k-form v-else-if="item.schema" :schema="item.schema" v-model="config" :initial="config" />
</template>
Expand All @@ -44,7 +44,7 @@ interface Tree {
children?: Tree[]
}
const data = computed(() => Object.entries(ctx.settings).map<Tree>(([id, [{ title }]]) => ({
const data = computed(() => Object.entries(ctx.internal.settings).map<Tree>(([id, [{ title }]]) => ({
id,
label: title,
})))
Expand All @@ -57,10 +57,10 @@ function handleClick(tree: Tree) {
const path = computed({
get() {
const name = route.params.name?.toString()
return name in ctx.settings ? name : ''
return name in ctx.internal.settings ? name : ''
},
set(value) {
if (!(value in ctx.settings)) value = ''
if (!(value in ctx.internal.settings)) value = ''
router.replace('/settings/' + value)
},
})
Expand Down
6 changes: 3 additions & 3 deletions packages/client/app/settings/theme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
<template #suffix><slot name="suffix"></slot></template>
<template #control>
<el-select popper-class="theme-select" v-model="model">
<template v-for="(_, key) in ctx.themes" :key="key">
<template v-for="(_, key) in ctx.internal.themes" :key="key">
<el-option :value="key" v-if="key.endsWith('-' + schema.meta.extra.mode)">
<div class="theme-root" :class="key.endsWith('-dark') ? 'dark' : 'light'" :theme="key">
<div class="theme-block-1"></div>
<div class="theme-block-2"></div>
<div class="theme-block-3"></div>
<div class="theme-title">
{{ tt(ctx.themes[key].name) }}
{{ tt(ctx.internal.themes[key].name) }}
</div>
</div>
</el-option>
Expand Down Expand Up @@ -48,7 +48,7 @@ const config = SchemaBase.useModel()
const model = computed({
get() {
return tt(ctx.themes[config.value].name)
return tt(ctx.internal.themes[config.value].name)
},
set(value) {
emit('update:modelValue', value)
Expand Down
2 changes: 1 addition & 1 deletion packages/client/client/components/slot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const KSlot = defineComponent({
const internal = props.single ? [] : [...slots.default?.() || []]
.filter(node => node.type === KSlotItem)
.map(node => ({ node, order: node.props?.order || 0 }))
const external = [...ctx.views[props.name] || []]
const external = [...ctx.internal.views[props.name] || []]
.filter(item => !item.when || item.when())
.map(item => ({
node: h(item.component, { data: props.data, ...props.data }, slots),
Expand Down
70 changes: 50 additions & 20 deletions packages/client/client/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as cordis from 'cordis'
import { Schema, SchemaBase } from '@koishijs/components'
import { Dict, remove } from 'cosmokit'
import { App, Component, createApp, defineComponent, h, inject, markRaw, onBeforeUnmount, provide, reactive, resolveComponent } from 'vue'
import { Dict, Intersect, remove } from 'cosmokit'
import {
App, Component, createApp, defineComponent, h, inject, markRaw, MaybeRefOrGetter,
onBeforeUnmount, provide, reactive, resolveComponent, shallowReactive, toValue,
} from 'vue'
import { Activity } from './activity'
import { SlotOptions } from './components'
import { useColorMode, useConfig } from './config'
import { ActionContext } from '.'

const config = useConfig()
const mode = useColorMode()
Expand All @@ -17,7 +21,7 @@ export interface ThemeOptions {
components?: Dict<Component>
}

export type MaybeGetter<T> = T | (() => T)
export type MaybeGetter<T> = T | ((scope: Flatten<ActionContext>) => T)

export interface Events<C extends Context> extends cordis.Events<C> {
'activity'(activity: Activity): boolean
Expand All @@ -35,14 +39,8 @@ export function useContext() {
}

export interface ActionOptions {
disabled?: MaybeGetter<boolean>
action: (...args: any[]) => any
}

export function useAction(id: string, options: ActionOptions) {
const ctx = useContext()
ctx.action(id, options)
return options.action
disabled?: (scope: Flatten<ActionContext>) => boolean
action: (scope: Flatten<ActionContext>) => any
}

export type LegacyMenuItem = Partial<ActionOptions> & Omit<MenuItem, 'id'>
Expand Down Expand Up @@ -77,14 +75,41 @@ function insert<T extends Ordered>(list: T[], item: T) {
}
}

export class Context extends cordis.Context {
app: App
type Store<S extends {}> = { [K in keyof S]?: MaybeRefOrGetter<S[K]> }

type Flatten<S extends {}> = Intersect<{
[K in keyof S]: K extends `${infer L}.${infer R}`
? { [P in L]: Flatten<{ [P in R]: S[K] }> }
: { [P in K]: S[K] }
}[keyof S]>

class Internal {
scope = shallowReactive<Store<ActionContext>>({})
menus = reactive<Dict<MenuItem[]>>({})
actions = reactive<Dict<ActionOptions[]>>({})
views = reactive<Dict<SlotOptions[]>>({})
themes = reactive<Dict<ThemeOptions>>({})
settings = reactive<Dict<SettingOptions[]>>({})

createScope(prefix = '') {
return new Proxy({}, {
get: (target, key) => {
if (typeof key === 'symbol') return target[key]
key = prefix + key
if (key in this.scope) return toValue(this.scope[key])
const _prefix = key + '.'
if (Object.keys(this.scope).some(k => k.startsWith(_prefix))) {
return this.createScope(key + '.')
}
},
})
}
}

export class Context extends cordis.Context {
app: App
internal = new Internal()

constructor() {
super()
this.app = createApp(defineComponent({
Expand Down Expand Up @@ -120,7 +145,7 @@ export class Context extends cordis.Context {
slot(options: SlotOptions) {
options.order ??= 0
options.component = this.wrapComponent(options.component)
const list = this.views[options.type] ||= []
const list = this.internal.views[options.type] ||= []
insert(list, options)
return this.scope.collect('view', () => remove(list, options))
}
Expand All @@ -138,40 +163,45 @@ export class Context extends cordis.Context {
}

action(id: string, options: ActionOptions) {
const list = this.actions[id] ||= []
const list = this.internal.actions[id] ||= []
markRaw(options)
list.push(options)
return this.scope.collect('actions', () => remove(list, options))
}

menu(id: string, items: MenuItem[]) {
const list = this.menus[id] ||= []
const list = this.internal.menus[id] ||= []
items.forEach(item => insert(list, item))
return this.scope.collect('menus', () => {
items.forEach(item => remove(list, item))
return true
})
}

extendSettings(options: SettingOptions) {
define<K extends keyof ActionContext>(key: K, value: MaybeRefOrGetter<ActionContext[K]>) {
this.internal.scope[key] = value as any
return this.scope.collect('activate', () => delete this.internal.scope[key])
}

settings(options: SettingOptions) {
markRaw(options)
options.order ??= 0
options.component = this.wrapComponent(options.component)
const list = this.settings[options.id] ||= []
const list = this.internal.settings[options.id] ||= []
insert(list, options)
return this.scope.collect('settings', () => remove(list, options))
}

theme(options: ThemeOptions) {
markRaw(options)
this.themes[options.id] = options
this.internal.themes[options.id] = options
for (const [type, component] of Object.entries(options.components || {})) {
this.slot({
type,
when: () => config.value.theme[mode.value] === options.id,
component,
})
}
return this.scope.collect('view', () => delete this.themes[options.id])
return this.scope.collect('view', () => delete this.internal.themes[options.id])
}
}
2 changes: 2 additions & 0 deletions packages/client/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export * from './utils'

export default install

export interface ActionContext {}

export const root = new Context()

export const router = createRouter({
Expand Down
6 changes: 4 additions & 2 deletions plugins/config/client/components/global.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<script lang="ts" setup>
import { send, store, useAction } from '@koishijs/client'
import { send, store, useContext } from '@koishijs/client'
import { computed } from 'vue'
import { Tree } from './utils'
Expand All @@ -20,7 +20,9 @@ const config = computed({
set: value => emit('update:modelValue', value),
})
useAction('config.save', {
const ctx = useContext()
ctx.action('config.save', {
action: () => send('manager/app-reload', config.value),
})
Expand Down
14 changes: 9 additions & 5 deletions plugins/config/client/components/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { clone, store, useAction } from '@koishijs/client'
import { addItem, current, plugins, removeItem, showSelect, Tree } from './utils'
import { clone, store, useContext } from '@koishijs/client'
import { addItem, current, plugins, removeItem, showSelect } from './utils'
import GlobalSettings from './global.vue'
import GroupSettings from './group.vue'
import TreeView from './tree.vue'
Expand Down Expand Up @@ -83,17 +83,21 @@ watch(() => plugins.value.paths[path.value], (value) => {
config.value = clone(value.config)
}, { immediate: true })
useAction('config.remove', {
const ctx = useContext()
ctx.define('config.tree', current)
ctx.action('config.remove', {
disabled: () => !current.value.path,
action: () => showRemove.value = true,
})
useAction('config.add-plugin', {
ctx.action('config.add-plugin', {
disabled: () => current.value.path && !current.value.children,
action: () => showSelect.value = true,
})
useAction('config.add-group', {
ctx.action('config.add-group', {
disabled: () => current.value.path && !current.value.children,
action: () => addItem(current.value.path, 'group', 'group'),
})
Expand Down
7 changes: 4 additions & 3 deletions plugins/config/client/components/plugin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@

<script lang="ts" setup>
import { store, send, useAction, message } from '@koishijs/client'
import { store, send, useContext, message } from '@koishijs/client'
import { computed, provide, watch } from 'vue'
import { useRouter } from 'vue-router'
import { coreDeps, envMap, name, SettingsData, splitPath, Tree } from './utils'
Expand Down Expand Up @@ -120,16 +120,17 @@ const data = computed<SettingsData>(() => ({
}))
const router = useRouter()
const ctx = useContext()
useAction('config.save', {
ctx.action('config.save', {
disabled: () => !name.value,
action: async () => {
await execute(props.current.disabled ? 'unload' : 'reload')
message.success(props.current.disabled ? '配置已保存。' : '配置已重载。')
},
})
useAction('config.toggle', {
ctx.action('config.toggle', {
disabled: () => !name.value || coreDeps.includes(name.value),
action: async () => {
await execute(props.current.disabled ? 'reload' : 'unload')
Expand Down
6 changes: 6 additions & 0 deletions plugins/config/client/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export const envMap = computed(() => {
.map(name => [name, getEnvInfo(name)]))
})

declare module '@koishijs/client' {
interface ActionContext {
'config.tree': Tree
}
}

export interface Tree {
id: string
alias: string
Expand Down
Loading

0 comments on commit 0f8ccd4

Please sign in to comment.