Skip to content

Commit

Permalink
fix(ui/sidebar): refactor settings and fix save issue (#677)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwqcode committed Dec 20, 2023
1 parent 91228a9 commit a5f8d09
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 155 deletions.
21 changes: 8 additions & 13 deletions ui/packages/artalk-sidebar/src/components/PreferenceArr.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
<script setup lang="ts">
import settings from '../lib/settings'
import settings, { patchOptionValue, type OptionNode } from '../lib/settings'
const props = defineProps<{
tplData: Array<any>,
path: (string|number)[]
node: OptionNode
}>()
const ci = getCurrentInstance()
const customValue = ref<string[]>([])
onMounted(() => {
update()
sync()
})
function update() {
customValue.value = (settings.get().customs.value?.getIn(props.path) as any)?.items || []
function sync() {
const value = settings.get().getCustom(props.node.path)
customValue.value = (value && typeof value.toJSON === 'function') ? value.toJSON() : []
}
function save() {
settings.get().customs.value?.setIn([...props.path], customValue.value)
const v = patchOptionValue(customValue.value, props.node)
settings.get().setCustom(props.node.path, v)
}
watch(settings.get().customs, (customs) => {
update()
ci?.proxy?.$forceUpdate()
})
function onChange(index: number, val: string) {
customValue.value[index] = val
save()
Expand Down
56 changes: 20 additions & 36 deletions ui/packages/artalk-sidebar/src/components/PreferenceGrp.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
<script setup lang="ts">
import settings from '../lib/settings'
const router = useRouter()
import settings, { type OptionNode } from '../lib/settings'
const props = defineProps<{
tplData: Object|Array<any>
path: (string|number)[]
node: OptionNode
}>()
const desc = computed(() => settings.get().extractItemDescFromComment(props.path))
const level = computed(() => props.path.length)
const expanded = ref(true)
const expandable = computed(() => {
return props.node.level === 1 && (props.node.type === 'object' || props.node.type === 'array')
})
onMounted(() => {
if (level.value === 1) expanded.value = false
if (expandable.value) expanded.value = false
})
function onHeadClick(evt: Event) {
if (level.value !== 1) return
if (!expandable.value) return
if (!expanded.value) {
expanded.value = true
// nextTick(() => {
Expand All @@ -28,38 +26,24 @@ function onHeadClick(evt: Event) {
expanded.value = false
}
}
const hiddenNodes = ['admin_users']
</script>

<template>
<div class="pf-grp" :class="[`level-${level}`, expanded ? 'expand' : '']">
<div v-if="level > 0" class="pf-head" @click="onHeadClick">
<div class="title">{{ desc.title }}</div>
<div v-if="!!desc.subTitle" class="sub-title">{{ desc.subTitle }}</div>
<div v-if="!hiddenNodes.includes(node.name)" class="pf-grp" :class="[`level-${node.level}`, expanded ? 'expand' : '']">
<div v-if="node.level > 0 && (node.type === 'object' || node.type === 'array')" class="pf-head" @click="onHeadClick">
<div class="title">{{ node.title }}</div>
<div v-if="!!node.subTitle" class="sub-title">{{ node.subTitle }}</div>
</div>
<div v-show="expanded" class="pf-body">
<!-- Array -->
<template v-if="Array.isArray(tplData)">
<PreferenceArr :tpl-data="tplData" :path="path" />
</template>
<!-- Object -->
<template v-else>
<div v-for="[key, value] in Object.entries(tplData)">
<!-- Admin Users -->
<template v-if="key === 'admin_users'"></template>
<!-- Grp -->
<PreferenceGrp
v-else-if="value !== null && typeof value === 'object'"
:tpl-data="value"
:path="[...path, key]"
/>
<!-- Item Input -->
<PreferenceItem
v-else
:tpl-data="value"
:path="[...path, key]"
/>
</div>
<!-- Grp -->
<template v-if="node.items">
<PreferenceGrp v-for="n in node.items" :key="n.path" :node="n" />
</template>

<!-- Item -->
<PreferenceItem v-else :node="node" />
</div>
</div>
</template>
Expand Down
54 changes: 25 additions & 29 deletions ui/packages/artalk-sidebar/src/components/PreferenceItem.vue
Original file line number Diff line number Diff line change
@@ -1,62 +1,58 @@
<script setup lang="ts">
import settings from '../lib/settings'
import settings, { patchOptionValue, type OptionNode } from '../lib/settings'
const props = defineProps<{
tplData: any
path: (string|number)[]
node: OptionNode
}>()
const desc = computed(() => settings.get().extractItemDescFromComment(props.path))
const customValue = computed(() => settings.get().customs.value?.getIn(props.path) as any)
const value = ref('')
function onChange(value: boolean|string|number) {
// 类型转换
switch (typeof props.tplData) {
case "boolean":
if (value === "true") value = true
else if (value === "false") value = false
break
case "string":
value = String(value)
break
case "number":
if (!isNaN(Number(value))) value = Number(value)
break
}
onBeforeMount(() => {
// initial value
value.value = settings.get().getCustom(props.node.path)
})
settings.get().customs.value?.setIn(props.path, value)
function onChange() {
const v = patchOptionValue(props.node, props.node)
settings.get().setCustom(props.node.path, v)
console.log('[SET]', props.node.path, v)
}
</script>

<template>
<div class="pf-item">
<div class="info">
<div class="title">{{ desc.title }}</div>
<div v-if="!!desc.subTitle" class="sub-title">{{ desc.subTitle }}</div>
<div class="title">{{ node.title }}</div>
<div v-if="node.subTitle" class="sub-title">{{ node.subTitle }}</div>
</div>

<div class="value">
<!-- Array -->
<template v-if="node.type === 'array'">
<PreferenceArr :node="node" />
</template>

<!-- 候选框 -->
<template v-if="desc.opts !== null">
<select :value="customValue" @change="onChange(($event.target as any).value)">
<template v-else-if="node.selector">
<select v-model="value" @change="onChange">
<option
v-for="item in desc.opts"
v-for="item in node.selector"
:value="item"
>{{ item }}</option>
</select>
</template>

<!-- 开关 -->
<template v-else-if="typeof tplData === 'boolean'">
<input type="checkbox" :checked="customValue" @change="onChange(($event.target as any).checked)">
<template v-else-if="node.type === 'boolean'">
<input type="checkbox" v-model="value" @change="onChange">
</template>

<!-- 文本框 -->
<template v-else>
<input
type="text"
:value="(typeof customValue === 'undefined' || customValue === null) ? '' : String(customValue)"
@change="onChange(($event.target as any).value)"
v-model="value"
@change="onChange"
/>
</template>
</div>
Expand Down
178 changes: 178 additions & 0 deletions ui/packages/artalk-sidebar/src/lib/settings-option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import YAML from 'yaml'
type Pair = YAML.Pair<YAML.Scalar<any>, YAML.Scalar<any> & YAML.YAMLMap<any, any>>

export interface OptionNode {
name: string
path: string
level: number
default?: string | number | boolean
selector?: string[]
type: 'string'|'number'|'boolean'|'object'|'array'
title: string
subTitle?: string
items?: OptionNode[]
}

let f = true
function extractItemComment(item: Pair, index: number, parentPair?: Pair): string {
let comment = ''
if (index === 0 && parentPair) comment = parentPair?.value?.commentBefore || ''
else comment = item?.key?.commentBefore || ''
return comment
}

export function getTree(yamlObj: YAML.Document.Parsed): OptionNode {
const tree: OptionNode = {
name: '',
path: '',
title: '',
level: 0,
type: 'object',
items: [],
};

const traverse = (pairs: Pair[], parentNode: OptionNode = tree, parentPath: string[] = [], parentPair?: Pair) => {
pairs.forEach((item, index) => {
// get key and value
const key = item.key?.value
const value = item.value?.toJSON ? item.value.toJSON() : undefined
if (!key) return

// get path
const path = [...parentPath, key]

// get comment
const comment = extractItemComment(item, index, parentPair)

// get type
const probablyTypes = ['string', 'number', 'boolean', 'object']
const type = (Array.isArray(value) ? 'array' : probablyTypes.find(t => typeof value === t)) || undefined

if (!type) return

// get default value
const defaultValue = (type !== 'object') ? value : undefined

// create new node
const node: OptionNode = {
name: key,
path: path.join('.'),
level: parentNode ? parentNode.level + 1 : 0,
...extractComment(key, comment),
default: defaultValue,
type: type as any,
}

// traverse children
if (type === 'object' && item.value?.items) {
node.items = []
traverse(item.value.items, node, path, item)
}

// add to parent
if (!parentNode.items) parentNode.items = []
parentNode.items.push(node)
})
}

traverse((yamlObj.contents as YAML.YAMLMap<any, any>)?.items)

return tree
}

/**
* Get flatten meta data from yaml object
*
* @param yamlObj
* @returns
*/
export function getFlattenNodes(tree: OptionNode): {[path: string]: OptionNode} {
const metas: {[path: string]: OptionNode} = {}

const traverse = (node: OptionNode) => {
metas[node.path] = node
if (node.items) node.items.forEach(traverse)
}

traverse(tree)

return metas
}

/**
* Extract option info from comment
*
* @param name Option name
* @param comment Option comment in YAML
* @returns Option info
*/
function extractComment(name: string, comment: string) {
comment = comment.trim()

// ignore comments begin and end with `--`
comment = comment.replace(/--(.*?)--/gm, '')

let title = ''
let subTitle = ''
let selector: string[]|undefined

const stReg = /\(.*?\)/gm
title = comment.replace(stReg, '').trim()
const stFind = stReg.exec(comment)
subTitle = stFind ? stFind[0].substring(1, stFind[0].length-1) : ''
if (!title) {
const defaultTitles: any = {
// TODO: add i18n
'enabled': '启用'
}
title = defaultTitles[name] || snakeToCamel(name)
}

const optReg = /\[.*?\]/gm
const optFind = optReg.exec(title)
if (optFind) {
try { selector = JSON.parse(optFind[0]) } catch (err) { console.error(err) }
title = title.replace(optReg, '').trim()
}

return {
title, subTitle, selector
}
}

function snakeToCamel(str: string) {
return str.toLowerCase()
.replace(/([_][a-z]|^[a-z])/g, (group) =>
group.slice(-1).toUpperCase()
)
}

/**
* Patch the option value by meta data
*
* @param value User custom value
* @param meta Option meta data
* @returns Patched value
*/
export function patchOptionValue(value: any, node: OptionNode) {
console.log(value, node)
switch (node.type) {
case "boolean":
if (value === "true") value = true
else if (value === "false") value = false
break
case "string":
if (!node.selector) // ignore option item
value = String(value).trim()
break
case "number":
if (!isNaN(Number(value))) value = Number(value)
break
case "array":
// trim string array
if (Array.isArray(value)) value = value.map(v => typeof v === 'string' ? v.trim() : v)
break
}

return value
}
Loading

0 comments on commit a5f8d09

Please sign in to comment.