Skip to content

Commit

Permalink
feat: support multiselect in table, fix koishijs/koishi#1300
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Dec 12, 2023
1 parent f3d54fb commit 20a6f57
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 135 deletions.
64 changes: 6 additions & 58 deletions packages/form/src/extensions/checkbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
<template #prefix><slot name="prefix"></slot></template>
<template #suffix><slot name="suffix"></slot></template>
<ul class="bottom">
<li v-for="item in list" :key="item.value">
<li v-for="item in items" :key="item.value">
<el-checkbox
:disabled="disabled || item.meta.disabled"
:modelValue="config.includes(item.value)"
:modelValue="values.includes(item.value)"
@update:modelValue="toggle(item.value)"
>
{{ tt(item.meta.description) || item.value }}
Expand All @@ -35,16 +35,15 @@

<script lang="ts" setup>
import { PropType, computed } from 'vue'
import { PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import { difference, union } from 'cosmokit'
import { Schema, useI18nText, useModel } from '../utils'
import { Schema, useI18nText, useMultiSelect } from '../utils'
import SchemaBase from '../base.vue'
import zhCN from '../locales/zh-CN.yml'
import enUS from '../locales/en-US.yml'
import { IconSquareCheck, IconSquareEmpty } from '../icons'
const props = defineProps({
defineProps({
schema: {} as PropType<Schema>,
modelValue: {} as PropType<number>,
disabled: {} as PropType<boolean>,
Expand All @@ -56,58 +55,7 @@ defineEmits(['update:modelValue'])
const tt = useI18nText()
const keys = computed(() => {
if (props.schema.type === 'bitset') {
return Object.keys(props.schema.bits)
} else if (props.schema.type === 'array') {
return props.schema.inner.list.map(item => item.value)
}
})
const list = computed(() => {
if (props.schema.type === 'bitset') {
return Object.keys(props.schema.bits).map(key => Schema.const(key))
} else if (props.schema.type === 'array') {
return props.schema.inner.list
}
})
// force mutate array
function toggle(key: string) {
if (config.value.includes(key)) {
config.value = config.value.filter(k => k !== key)
} else {
config.value = [...config.value, key]
}
}
const config = useModel<string[]>({
input(value) {
if (Array.isArray(value)) return value
return Object.entries(props.schema.bits)
.filter(([key, bit]) => value & bit)
.map(([key]) => key)
},
output(value) {
return value.sort((a, b) => {
const indexA = keys.value.indexOf(a)
const indexB = keys.value.indexOf(b)
if (indexA < 0) {
return indexB < 0 ? 0 : 1
} else {
return indexB < 0 ? -1 : indexA - indexB
}
})
},
})
function selectAll() {
config.value = union(config.value, keys.value)
}
function selectNone() {
config.value = difference(config.value, keys.value)
}
const { values, items, toggle, selectAll, selectNone } = useMultiSelect()
const { t, setLocaleMessage } = useI18n({
messages: {
Expand Down
55 changes: 6 additions & 49 deletions packages/form/src/extensions/multiselect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<el-select
multiple
collapse-tags
v-model="config"
v-model="values"
:disabled="disabled">
<el-option
v-for="item in list"
v-for="item in items"
:key="item.value"
:value="item.value"
:disabled="item.meta.disabled">
Expand All @@ -39,16 +39,15 @@

<script lang="ts" setup>
import { PropType, computed } from 'vue'
import { PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import { difference, union } from 'cosmokit'
import { Schema, useI18nText, useModel } from '../utils'
import { Schema, useI18nText, useMultiSelect } from '../utils'
import SchemaBase from '../base.vue'
import zhCN from '../locales/zh-CN.yml'
import enUS from '../locales/en-US.yml'
import { IconSquareCheck, IconSquareEmpty } from '../icons'
const props = defineProps({
defineProps({
schema: {} as PropType<Schema>,
modelValue: {} as PropType<number>,
disabled: {} as PropType<boolean>,
Expand All @@ -60,49 +59,7 @@ defineEmits(['update:modelValue'])
const tt = useI18nText()
const keys = computed(() => {
if (props.schema.type === 'bitset') {
return Object.keys(props.schema.bits)
} else if (props.schema.type === 'array') {
return props.schema.inner.list.map(item => item.value)
}
})
const list = computed(() => {
if (props.schema.type === 'bitset') {
return Object.keys(props.schema.bits).map(key => Schema.const(key))
} else if (props.schema.type === 'array') {
return props.schema.inner.list
}
})
const config = useModel<string[]>({
input(value) {
if (Array.isArray(value)) return value
return Object.entries(props.schema.bits)
.filter(([key, bit]) => value & bit)
.map(([key]) => key)
},
output(value) {
return value.sort((a, b) => {
const indexA = keys.value.indexOf(a)
const indexB = keys.value.indexOf(b)
if (indexA < 0) {
return indexB < 0 ? 0 : 1
} else {
return indexB < 0 ? -1 : indexA - indexB
}
})
},
})
function selectAll() {
config.value = union(config.value, keys.value)
}
function selectNone() {
config.value = difference(config.value, keys.value)
}
const { values, items, selectAll, selectNone } = useMultiSelect()
const { t, setLocaleMessage } = useI18n({
messages: {
Expand Down
22 changes: 2 additions & 20 deletions packages/form/src/extensions/table.vue
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
import { computed, ref, PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import { IconArrowUp, IconArrowDown, IconDelete, IconInvalid } from '../icons'
import { Schema, useEntries, useI18nText, explain } from '../utils'
import { Schema, useEntries, useI18nText, explain, toColumns } from '../utils'
import SchemaBase from '../base.vue'
import SchemaPrimitive from '../primitive.vue'
import zhCN from '../locales/zh-CN.yml'
Expand All @@ -119,25 +119,7 @@ const props = defineProps({
defineEmits(['update:modelValue'])
function isPrimitive(schema: Schema): boolean {
if (['string', 'number', 'boolean'].includes(schema.type)) return true
if (schema.type === 'union') return schema.list.every(item => item.type === 'const')
}
function ensureColumns(entries: [string, Schema][]) {
if (entries.every(([, schema]) => isPrimitive(schema))) return entries
}
const columns = computed<[string, Schema][]>(() => {
const { inner } = props.schema
if (isPrimitive(inner)) {
return [[null, inner]]
} else if (inner.type === 'tuple') {
return ensureColumns(Object.entries(inner.list))
} else if (inner.type === 'object') {
return ensureColumns(Object.entries(inner.dict))
}
})
const columns = computed(() => toColumns(props.schema.inner))
const { entries, insert, del, up, down } = useEntries()
Expand Down
6 changes: 3 additions & 3 deletions packages/form/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { App, Component } from 'vue'
import extensions, { Schema, useDisabled, useEntries, useModel } from './utils'
import extensions, { Schema, toColumns, useDisabled, useEntries, useModel } from './utils'
import SchemaBase from './base.vue'
import Primitive from './primitive.vue'
import SchemaCheckbox from './extensions/checkbox.vue'
Expand Down Expand Up @@ -119,14 +119,14 @@ form.extensions.add({
type: 'array',
role: 'table',
component: SchemaTable,
validate: value => Array.isArray(value),
validate: (value, schema) => Array.isArray(value) && !!toColumns(schema.inner),
})

form.extensions.add({
type: 'dict',
role: 'table',
component: SchemaTable,
validate: value => typeof value === 'object',
validate: (value, schema) => typeof value === 'object' && !!toColumns(schema.inner),
})

form.extensions.add({
Expand Down
31 changes: 27 additions & 4 deletions packages/form/src/primitive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,31 @@
v-for="(item, index) in schema.list"
:key="index"
:value="index"
:disabled="item.meta.disabled"
:label="tt(item.meta.description) || item.value"
></el-option>
:disabled="item.meta.disabled">
{{ tt(item.meta.description) || item.value }}
<k-badge :type="type" v-for="{ text, type } in item.meta.badges || []">
{{ t('badge.' + text) }}
</k-badge>
</el-option>
</el-select>
</template>

<template v-else-if="schema.type === 'array' || schema.type === 'bitset'">
<el-select
multiple
collapse-tags
v-model="values"
:disabled="disabled">
<el-option
v-for="item in items"
:key="item.value"
:value="item.value"
:disabled="item.meta.disabled">
{{ tt(item.meta.description) || item.value }}
<k-badge :type="type" v-for="{ text, type } in item.meta.badges || []">
{{ t('badge.' + text) }}
</k-badge>
</el-option>
</el-select>
</template>
</template>
Expand All @@ -77,7 +99,7 @@ import { computed, PropType, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { IconExternal, IconEye, IconEyeSlash, IconInvalid } from './icons'
import { isNullable } from 'cosmokit'
import { explain, useI18nText, useModel } from './utils'
import { explain, useI18nText, useModel, useMultiSelect } from './utils'
import Schema from 'schemastery'
import zhCN from './locales/zh-CN.yml'
import enUS from './locales/en-US.yml'
Expand All @@ -94,6 +116,7 @@ const props = defineProps({
const showPass = ref(false)
const config = useModel()
const { values, items } = useMultiSelect()
const tt = useI18nText()
Expand Down
92 changes: 91 additions & 1 deletion packages/form/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Schema from 'schemastery'
import { clone, deepEqual, Dict, isNullable, valueMap } from 'cosmokit'
import { clone, deepEqual, Dict, difference, isNullable, union, valueMap } from 'cosmokit'
import { computed, getCurrentInstance, ref, watch, WatchStopHandle } from 'vue'
import { fallbackWithLocaleChain } from '@intlify/core-base'
import { useI18n } from 'vue-i18n'
Expand Down Expand Up @@ -172,3 +172,93 @@ export function useEntries() {
},
}
}

function isConstUnion(schema: Schema) {
return schema.type === 'union' && schema.list.every(item => item.type === 'const')
}

export function isMultiSelect(schema: Schema) {
if (schema.type === 'bitset') return true
if (schema.type === 'array') return isConstUnion(schema.inner)
}

export function useMultiSelect() {
const { props } = getCurrentInstance() as any

const keys = computed(() => {
if (props.schema.type === 'bitset') {
return Object.keys(props.schema.bits)
} else if (props.schema.type === 'array') {
return props.schema.inner.list.map((item: any) => item.value)
}
})

const items = computed(() => {
if (props.schema.type === 'bitset') {
return Object.keys(props.schema.bits).map(key => Schema.const(key))
} else if (props.schema.type === 'array') {
return props.schema.inner.list
}
})

const values = useModel<string[]>({
input(value) {
if (!isMultiSelect(props.schema)) return value
if (isNullable(value)) return []
if (Array.isArray(value)) return value
return Object.entries(props.schema.bits)
.filter(([key, bit]) => value & bit as any)
.map(([key]) => key)
},
output(value) {
if (!isMultiSelect(props.schema)) return value
return value.sort((a, b) => {
const indexA = keys.value.indexOf(a)
const indexB = keys.value.indexOf(b)
if (indexA < 0) {
return indexB < 0 ? 0 : 1
} else {
return indexB < 0 ? -1 : indexA - indexB
}
})
},
})

return {
values,
items,
selectAll() {
values.value = union(values.value, keys.value)
},
selectNone() {
values.value = difference(values.value, keys.value)
},
toggle(key: string) {
if (values.value.includes(key)) {
values.value = values.value.filter(k => k !== key)
} else {
values.value = [...values.value, key]
}
},
}
}

function isValidColumn(schema: Schema): boolean {
return ['string', 'number', 'boolean'].includes(schema.type)
|| isConstUnion(schema)
|| isMultiSelect(schema)
}

function ensureColumns(entries: [string, Schema][]) {
if (entries.every(([, schema]) => isValidColumn(schema))) return entries
}

export function toColumns(schema: Schema): [string, Schema][] {
if (isValidColumn(schema)) {
return [[null, schema]]
} else if (schema.type === 'tuple') {
return ensureColumns(Object.entries(schema.list))
} else if (schema.type === 'object') {
return ensureColumns(Object.entries(schema.dict))
}
}

0 comments on commit 20a6f57

Please sign in to comment.