feat: user editing support using yaml

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/3445/head
Ryan Wang 2022-07-22 17:04:30 +08:00
parent e218818a59
commit 51c87c1519
8 changed files with 145 additions and 53 deletions

View File

@ -49,7 +49,8 @@
"vue": "^3.2.37", "vue": "^3.2.37",
"vue-filepond": "^7.0.3", "vue-filepond": "^7.0.3",
"vue-grid-layout": "3.0.0-beta1", "vue-grid-layout": "3.0.0-beta1",
"vue-router": "^4.1.2" "vue-router": "^4.1.2",
"yaml": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.23.2", "@changesets/cli": "^2.23.2",

View File

@ -49,7 +49,7 @@
"@rollup/plugin-typescript": "^8.3.3", "@rollup/plugin-typescript": "^8.3.3",
"histoire": "^0.7.9", "histoire": "^0.7.9",
"unplugin-icons": "^0.14.7", "unplugin-icons": "^0.14.7",
"vite-plugin-dts": "^1.3.1" "vite-plugin-dts": "^1.4.0"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.2.37", "vue": "^3.2.37",
@ -64,6 +64,8 @@
}, },
"dependencies": { "dependencies": {
"@codemirror/commands": "^6.0.1", "@codemirror/commands": "^6.0.1",
"@codemirror/language": "^6.2.1",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/state": "^6.1.0", "@codemirror/state": "^6.1.0",
"@codemirror/view": "^6.1.0", "@codemirror/view": "^6.1.0",
"codemirror": "^6.0.1" "codemirror": "^6.0.1"

View File

@ -15,3 +15,4 @@ export * from "./components/textarea";
export * from "./components/switch"; export * from "./components/switch";
export * from "./components/dialog"; export * from "./components/dialog";
export * from "./components/pagination"; export * from "./components/pagination";
export * from "./components/codemirror";

View File

@ -10,7 +10,7 @@ function initState() {
<template> <template>
<Story :initState="initState" title="Codemirror"> <Story :initState="initState" title="Codemirror">
<template #default="{ state }"> <template #default="{ state }">
<VCodemirror v-model="state.value" height="500px" language="javascript" /> <VCodemirror v-model="state.value" height="500px" language="yaml" />
</template> </template>
</Story> </Story>
</template> </template>

View File

@ -5,6 +5,12 @@ import type { EditorStateConfig } from "@codemirror/state";
import { EditorState } from "@codemirror/state"; import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view"; import { EditorView } from "@codemirror/view";
import { basicSetup } from "codemirror"; import { basicSetup } from "codemirror";
import { StreamLanguage } from "@codemirror/language";
import { yaml } from "@codemirror/legacy-modes/mode/yaml";
const languages = {
yaml: StreamLanguage.define(yaml),
};
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -15,6 +21,10 @@ const props = defineProps({
type: String, type: String,
default: "auto", default: "auto",
}, },
language: {
type: String as PropType<"yaml">,
default: "yaml",
},
extensions: { extensions: {
type: Array as PropType<EditorStateConfig["extensions"]>, type: Array as PropType<EditorStateConfig["extensions"]>,
default: () => [], default: () => [],
@ -41,6 +51,7 @@ const createCmEditor = () => {
basicSetup, basicSetup,
EditorView.lineWrapping, EditorView.lineWrapping,
customTheme, customTheme,
languages[props.language],
EditorView.updateListener.of((viewUpdate) => { EditorView.updateListener.of((viewUpdate) => {
if (viewUpdate.docChanged) { if (viewUpdate.docChanged) {
const doc = viewUpdate.state.doc.toString(); const doc = viewUpdate.state.doc.toString();

View File

@ -39,6 +39,7 @@ import IconShieldUser from "~icons/ri/shield-user-line";
import IconGitBranch from "~icons/ri/git-branch-line"; import IconGitBranch from "~icons/ri/git-branch-line";
import IconStopCircle from "~icons/ri/stop-circle-line"; import IconStopCircle from "~icons/ri/stop-circle-line";
import IconForbidLine from "~icons/ri/forbid-line"; import IconForbidLine from "~icons/ri/forbid-line";
import IconCodeBoxLine from "~icons/ri/code-box-line";
export { export {
IconDashboard, IconDashboard,
@ -82,4 +83,5 @@ export {
IconGitBranch, IconGitBranch,
IconStopCircle, IconStopCircle,
IconForbidLine, IconForbidLine,
IconCodeBoxLine,
}; };

View File

@ -71,6 +71,7 @@ importers:
vue-grid-layout: 3.0.0-beta1 vue-grid-layout: 3.0.0-beta1
vue-router: ^4.1.2 vue-router: ^4.1.2
vue-tsc: ^0.38.8 vue-tsc: ^0.38.8
yaml: ^2.1.1
dependencies: dependencies:
'@formkit/addons': 1.0.0-beta.9_vue@3.2.37 '@formkit/addons': 1.0.0-beta.9_vue@3.2.37
'@formkit/core': 1.0.0-beta.9 '@formkit/core': 1.0.0-beta.9
@ -97,6 +98,7 @@ importers:
vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37 vue-filepond: 7.0.3_filepond@4.30.4+vue@3.2.37
vue-grid-layout: 3.0.0-beta1 vue-grid-layout: 3.0.0-beta1
vue-router: 4.1.2_vue@3.2.37 vue-router: 4.1.2_vue@3.2.37
yaml: 2.1.1
devDependencies: devDependencies:
'@changesets/cli': 2.23.2 '@changesets/cli': 2.23.2
'@rushstack/eslint-patch': 1.1.4 '@rushstack/eslint-patch': 1.1.4
@ -144,6 +146,8 @@ importers:
packages/components: packages/components:
specifiers: specifiers:
'@codemirror/commands': ^6.0.1 '@codemirror/commands': ^6.0.1
'@codemirror/language': ^6.2.1
'@codemirror/legacy-modes': ^6.1.0
'@codemirror/state': ^6.1.0 '@codemirror/state': ^6.1.0
'@codemirror/view': ^6.1.0 '@codemirror/view': ^6.1.0
'@iconify-json/ri': ^1.1.3 '@iconify-json/ri': ^1.1.3
@ -151,9 +155,11 @@ importers:
codemirror: ^6.0.1 codemirror: ^6.0.1
histoire: ^0.7.9 histoire: ^0.7.9
unplugin-icons: ^0.14.7 unplugin-icons: ^0.14.7
vite-plugin-dts: ^1.3.1 vite-plugin-dts: ^1.4.0
dependencies: dependencies:
'@codemirror/commands': 6.0.1 '@codemirror/commands': 6.0.1
'@codemirror/language': 6.2.1
'@codemirror/legacy-modes': 6.1.0
'@codemirror/state': 6.1.0 '@codemirror/state': 6.1.0
'@codemirror/view': 6.1.0 '@codemirror/view': 6.1.0
codemirror: 6.0.1 codemirror: 6.0.1
@ -162,7 +168,7 @@ importers:
'@rollup/plugin-typescript': 8.3.3 '@rollup/plugin-typescript': 8.3.3
histoire: 0.7.9 histoire: 0.7.9
unplugin-icons: 0.14.7 unplugin-icons: 0.14.7
vite-plugin-dts: 1.3.1 vite-plugin-dts: 1.4.0
packages/shared: packages/shared:
specifiers: specifiers:
@ -1644,6 +1650,12 @@ packages:
style-mod: 4.0.0 style-mod: 4.0.0
dev: false dev: false
/@codemirror/legacy-modes/6.1.0:
resolution: {integrity: sha512-V/PgGpndkZeTn3Hdlg/gd8MLFdyvTCIX+iwJzjUw5iNziWiNsAY8X0jvf7m3gSfxnKkNzmid6l0g4rYSpiDaCw==}
dependencies:
'@codemirror/language': 6.2.1
dev: false
/@codemirror/lint/6.0.0: /@codemirror/lint/6.0.0:
resolution: {integrity: sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==} resolution: {integrity: sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==}
dependencies: dependencies:
@ -1652,8 +1664,8 @@ packages:
crelt: 1.0.5 crelt: 1.0.5
dev: false dev: false
/@codemirror/search/6.0.0: /@codemirror/search/6.0.1:
resolution: {integrity: sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==} resolution: {integrity: sha512-uOinkOrM+daMduCgMPomDfKLr7drGHB4jHl3Vq6xY2WRlL7MkNsBE0b+XHYa/Mee2npsJOgwvkW4n1lMFeBW2Q==}
dependencies: dependencies:
'@codemirror/state': 6.1.0 '@codemirror/state': 6.1.0
'@codemirror/view': 6.1.0 '@codemirror/view': 6.1.0
@ -3659,7 +3671,7 @@ packages:
'@codemirror/commands': 6.0.1 '@codemirror/commands': 6.0.1
'@codemirror/language': 6.2.1 '@codemirror/language': 6.2.1
'@codemirror/lint': 6.0.0 '@codemirror/lint': 6.0.0
'@codemirror/search': 6.0.0 '@codemirror/search': 6.0.1
'@codemirror/state': 6.1.0 '@codemirror/state': 6.1.0
'@codemirror/view': 6.1.0 '@codemirror/view': 6.1.0
dev: false dev: false
@ -7923,6 +7935,23 @@ packages:
- supports-color - supports-color
dev: true dev: true
/vite-plugin-dts/1.4.0:
resolution: {integrity: sha512-RyDCjQzVxUeDqF+Rl1hQT+t/rKmvfvo04gaGV/l3597FpeIWGKtNF1S4x509Kx1AfHOjLa1JdmjVgnSEIv+lpw==}
engines: {node: '>=12.0.0'}
peerDependencies:
vite: '>=2.4.4'
dependencies:
'@microsoft/api-extractor': 7.23.2
'@rushstack/node-core-library': 3.45.5
chalk: 4.1.2
debug: 4.3.4
fast-glob: 3.2.11
fs-extra: 10.1.0
ts-morph: 14.0.0
transitivePeerDependencies:
- supports-color
dev: true
/vite-plugin-externals/0.5.1_vite@2.9.14: /vite-plugin-externals/0.5.1_vite@2.9.14:
resolution: {integrity: sha512-HvRFG5y9wXoJUG9FSbSp9ikOiJRh7EzN6tJC5oIOcEj+19GUw9Z1NNCPFtAmX75Ajcr10FdELKNmuXS3lExkcg==} resolution: {integrity: sha512-HvRFG5y9wXoJUG9FSbSp9ikOiJRh7EzN6tJC5oIOcEj+19GUw9Z1NNCPFtAmX75Ajcr10FdELKNmuXS3lExkcg==}
peerDependencies: peerDependencies:
@ -8512,6 +8541,11 @@ packages:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
/yaml/2.1.1:
resolution: {integrity: sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==}
engines: {node: '>= 14'}
dev: false
/yargs-parser/18.1.3: /yargs-parser/18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'} engines: {node: '>=6'}

View File

@ -3,10 +3,18 @@ import type { PropType } from "vue";
import { computed, onMounted, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { apiClient } from "@halo-dev/admin-shared"; import { apiClient } from "@halo-dev/admin-shared";
import type { Role, User } from "@halo-dev/api-client"; import type { Role, User } from "@halo-dev/api-client";
import { IconSave, VButton, VModal } from "@halo-dev/components"; import {
IconCodeBoxLine,
IconEye,
IconSave,
VButton,
VCodemirror,
VModal,
} from "@halo-dev/components";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { roleLabels } from "@/constants/labels"; import { roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import YAML from "yaml";
const props = defineProps({ const props = defineProps({
visible: { visible: {
@ -24,6 +32,8 @@ const emit = defineEmits(["update:visible", "close"]);
interface EditingFormState { interface EditingFormState {
user: User; user: User;
saving: boolean; saving: boolean;
rawMode: boolean;
raw: string;
} }
const roles = ref<Role[]>([]); const roles = ref<Role[]>([]);
@ -46,6 +56,8 @@ const editingFormState = ref<EditingFormState>({
}, },
}, },
saving: false, saving: false,
rawMode: false,
raw: "",
}); });
const selectedRole = ref(""); const selectedRole = ref("");
@ -57,6 +69,10 @@ const creationModalTitle = computed(() => {
return isUpdateMode.value ? "编辑用户" : "新增用户"; return isUpdateMode.value ? "编辑用户" : "新增用户";
}); });
const modalWidth = computed(() => {
return editingFormState.value.rawMode ? 800 : 700;
});
const basicRoles = computed(() => { const basicRoles = computed(() => {
return roles.value.filter( return roles.value.filter(
(role) => role.metadata?.labels?.[roleLabels.TEMPLATE] !== "true" (role) => role.metadata?.labels?.[roleLabels.TEMPLATE] !== "true"
@ -118,38 +134,62 @@ const handleCreateUser = async () => {
} }
}; };
const handleRawModeChange = () => {
editingFormState.value.rawMode = !editingFormState.value.rawMode;
if (editingFormState.value.rawMode) {
editingFormState.value.raw = YAML.stringify(editingFormState.value.user);
} else {
editingFormState.value.user = YAML.parse(editingFormState.value.raw);
}
};
onMounted(handleFetchRoles); onMounted(handleFetchRoles);
</script> </script>
<template> <template>
<VModal <VModal
:title="creationModalTitle" :title="creationModalTitle"
:visible="visible" :visible="visible"
:width="700" :width="modalWidth"
@update:visible="handleVisibleChange" @update:visible="handleVisibleChange"
> >
<FormKit id="user-form" type="form" @submit="handleCreateUser"> <template #actions>
<FormKit <div class="modal-header-action" @click="handleRawModeChange">
v-model="editingFormState.user.metadata.name" <IconCodeBoxLine v-if="!editingFormState.rawMode" />
:disabled="true" <IconEye v-else />
label="用户名" </div>
type="text" </template>
validation="required"
></FormKit> <VCodemirror
<FormKit v-show="editingFormState.rawMode"
v-model="editingFormState.user.spec.displayName" v-model="editingFormState.raw"
label="显示名称" height="50vh"
type="text" language="yaml"
validation="required" />
></FormKit>
<FormKit <div v-show="!editingFormState.rawMode">
v-model="editingFormState.user.spec.email" <FormKit id="user-form" type="form" @submit="handleCreateUser">
label="电子邮箱" <FormKit
type="email" v-model="editingFormState.user.metadata.name"
validation="required" :disabled="true"
></FormKit> label="用户名"
<FormKit type="text"
v-model="selectedRole" validation="required"
:options=" ></FormKit>
<FormKit
v-model="editingFormState.user.spec.displayName"
label="显示名称"
type="text"
validation="required"
></FormKit>
<FormKit
v-model="editingFormState.user.spec.email"
label="电子邮箱"
type="email"
validation="required"
></FormKit>
<FormKit
v-model="selectedRole"
:options="
basicRoles.map((role:Role) => { basicRoles.map((role:Role) => {
return { return {
label: role.metadata?.annotations?.[rbacAnnotations.DISPLAY_NAME] || role.metadata.name, label: role.metadata?.annotations?.[rbacAnnotations.DISPLAY_NAME] || role.metadata.name,
@ -157,26 +197,27 @@ onMounted(handleFetchRoles);
}; };
}) })
" "
label="角色" label="角色"
type="select" type="select"
validation="required" validation="required"
></FormKit> ></FormKit>
<FormKit <FormKit
v-model="editingFormState.user.spec.phone" v-model="editingFormState.user.spec.phone"
label="手机号" label="手机号"
type="text" type="text"
></FormKit> ></FormKit>
<FormKit <FormKit
v-model="editingFormState.user.spec.avatar" v-model="editingFormState.user.spec.avatar"
label="头像" label="头像"
type="text" type="text"
></FormKit> ></FormKit>
<FormKit <FormKit
v-model="editingFormState.user.spec.bio" v-model="editingFormState.user.spec.bio"
label="描述" label="描述"
type="textarea" type="textarea"
></FormKit> ></FormKit>
</FormKit> </FormKit>
</div>
<template #footer> <template #footer>
<VButton <VButton
:loading="editingFormState.saving" :loading="editingFormState.saving"