refactor: user and role editing modal

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/603/head
Ryan Wang 2022-08-23 15:27:42 +08:00
parent 3722a7fb36
commit 4031eb7d85
4 changed files with 179 additions and 98 deletions

View File

@ -1,4 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
// core libs
import { ref } from "vue";
import type { Role } from "@halo-dev/api-client";
// components
import { import {
IconAddCircle, IconAddCircle,
IconArrowDown, IconArrowDown,
@ -12,44 +17,34 @@ import {
VTag, VTag,
} from "@halo-dev/components"; } from "@halo-dev/components";
import RoleEditingModal from "./components/RoleEditingModal.vue"; import RoleEditingModal from "./components/RoleEditingModal.vue";
import { onMounted, ref } from "vue";
import type { Role } from "@halo-dev/api-client"; // constants
import { apiClient } from "@halo-dev/admin-shared";
import { roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
const roles = ref<Role[]>([]); // hooks
import { useFetchRole } from "@/modules/system/roles/composables/use-role";
const editingModal = ref<boolean>(false); const editingModal = ref<boolean>(false);
const selectedRole = ref<Role | null>(null); const selectedRole = ref<Role | null>(null);
const handleFetchRoles = async () => { const { roles, handleFetchRoles } = useFetchRole();
try {
const { data } = await apiClient.extension.role.listv1alpha1Role(0, 0, [
`!${roleLabels.TEMPLATE}`,
]);
roles.value = data.items;
} catch (e) {
console.error(e);
} finally {
selectedRole.value = null;
}
};
const handleOpenEditingModal = (role: Role) => { const handleOpenEditingModal = (role: Role) => {
selectedRole.value = role; selectedRole.value = role;
editingModal.value = true; editingModal.value = true;
}; };
onMounted(() => { const onEditingModalClose = () => {
selectedRole.value = null;
handleFetchRoles(); handleFetchRoles();
}); };
</script> </script>
<template> <template>
<RoleEditingModal <RoleEditingModal
v-model:visible="editingModal" v-model:visible="editingModal"
:role="selectedRole" :role="selectedRole"
@close="handleFetchRoles" @close="onEditingModalClose"
/> />
<VPageHeader title="角色"> <VPageHeader title="角色">

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components"; import { VButton, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
import { computed, ref, watch } from "vue"; import { computed, ref, watch, watchEffect } from "vue";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
import type { Role } from "@halo-dev/api-client"; import type { Role } from "@halo-dev/api-client";
import { import {
@ -10,6 +10,7 @@ import {
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { reset, submitForm } from "@formkit/core"; import { reset, submitForm } from "@formkit/core";
import { useMagicKeys } from "@vueuse/core"; import { useMagicKeys } from "@vueuse/core";
import { v4 as uuid } from "uuid";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -48,32 +49,38 @@ watch(
} }
); );
watch(props, (newVal) => { const { Command_Enter } = useMagicKeys();
const { Command_Enter } = useMagicKeys();
let keyboardWatcher; watchEffect(() => {
if (newVal.visible) { if (Command_Enter.value && props.visible) {
keyboardWatcher = watch(Command_Enter, (v) => {
if (v) {
submitForm("role-form"); submitForm("role-form");
} }
}); });
} else {
keyboardWatcher?.unwatch();
}
if (newVal.visible && props.role) { watch(
formState.value = cloneDeep(props.role); () => props.visible,
(visible) => {
if (!visible) {
handleResetForm();
}
}
);
watch(
() => props.role,
(role) => {
if (role) {
formState.value = cloneDeep(role);
const dependencies = const dependencies =
props.role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES]; role.metadata.annotations?.[rbacAnnotations.DEPENDENCIES];
if (dependencies) { if (dependencies) {
selectedRoleTemplates.value = new Set(JSON.parse(dependencies)); selectedRoleTemplates.value = new Set(JSON.parse(dependencies));
} }
return; } else {
handleResetForm();
} }
formState.value = cloneDeep(initialFormState); }
selectedRoleTemplates.value.clear(); );
reset("role-form");
});
const tabActiveId = ref("general"); const tabActiveId = ref("general");
@ -84,25 +91,31 @@ const editingModalTitle = computed(() => {
const handleCreateOrUpdateRole = async () => { const handleCreateOrUpdateRole = async () => {
try { try {
await handleCreateOrUpdate(); await handleCreateOrUpdate();
handleVisibleChange(false); onVisibleChange(false);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
}; };
const handleVisibleChange = (visible: boolean) => { const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible); emit("update:visible", visible);
if (!visible) { if (!visible) {
emit("close"); emit("close");
} }
}; };
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
formState.value.metadata.name = uuid();
reset("role-form");
};
</script> </script>
<template> <template>
<VModal <VModal
:title="editingModalTitle" :title="editingModalTitle"
:visible="visible" :visible="visible"
:width="700" :width="700"
@update:visible="handleVisibleChange" @update:visible="onVisibleChange"
> >
<VTabs v-model:active-id="tabActiveId" type="outline"> <VTabs v-model:active-id="tabActiveId" type="outline">
<VTabItem id="general" label="基础信息"> <VTabItem id="general" label="基础信息">
@ -198,7 +211,7 @@ const handleVisibleChange = (visible: boolean) => {
> >
提交 + 提交 +
</VButton> </VButton>
<VButton @click="handleVisibleChange(false)"> Esc</VButton> <VButton @click="onVisibleChange(false)"> Esc</VButton>
</VSpace> </VSpace>
</template> </template>
</VModal> </VModal>

View File

@ -1,4 +1,5 @@
import type { Role } from "@halo-dev/api-client"; import type { Role } from "@halo-dev/api-client";
import type { ComputedRef, Ref } from "vue";
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { roleLabels } from "@/constants/labels"; import { roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations"; import { rbacAnnotations } from "@/constants/annotations";
@ -23,7 +24,65 @@ const initialFormState: Role = {
rules: [], rules: [],
}; };
export function useRoleForm() { interface useFetchRoleReturn {
roles: Ref<Role[]>;
loading: Ref<boolean>;
handleFetchRoles: () => void;
}
interface useRoleFormReturn {
formState: Ref<Role>;
initialFormState: Role;
saving: Ref<boolean>;
isUpdateMode: ComputedRef<boolean>;
handleCreateOrUpdate: () => void;
}
interface useRoleTemplateSelectionReturn {
selectedRoleTemplates: Ref<Set<string>>;
roleTemplates: Ref<Role[]>;
roleTemplateGroups: ComputedRef<RoleTemplateGroup[]>;
handleRoleTemplateSelect: (e: Event) => void;
}
/**
* Fetch all roles(without role templates) from the API.
*
* @returns {useFetchRoleReturn}
*/
export function useFetchRole(): useFetchRoleReturn {
const roles = ref<Role[]>([]);
const loading = ref(false);
const handleFetchRoles = async () => {
try {
loading.value = true;
const { data } = await apiClient.extension.role.listv1alpha1Role(0, 0, [
`!${roleLabels.TEMPLATE}`,
]);
roles.value = data.items;
} catch (e) {
console.error("Failed to fetch roles", e);
} finally {
loading.value = false;
}
};
onMounted(handleFetchRoles);
return {
roles,
loading,
handleFetchRoles,
};
}
/**
* Create or update a role.
*
* @returns {useRoleFormReturn}
*/
export function useRoleForm(): useRoleFormReturn {
const formState = ref<Role>(initialFormState); const formState = ref<Role>(initialFormState);
const saving = ref(false); const saving = ref(false);
@ -58,7 +117,12 @@ export function useRoleForm() {
}; };
} }
export function useRoleTemplateSelection() { /**
* Logic for selecting role templates
*
* @returns {useRoleTemplateSelectionReturn}
*/
export function useRoleTemplateSelection(): useRoleTemplateSelectionReturn {
const roleTemplates = ref<Role[]>([] as Role[]); const roleTemplates = ref<Role[]>([] as Role[]);
const selectedRoleTemplates = ref<Set<string>>(new Set()); const selectedRoleTemplates = ref<Set<string>>(new Set());

View File

@ -1,7 +1,10 @@
<script lang="ts" name="UserEditingModal" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from "vue"; // core libs
import { computed, ref, watch, watchEffect } 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 { User } from "@halo-dev/api-client";
// components
import { import {
IconCodeBoxLine, IconCodeBoxLine,
IconEye, IconEye,
@ -10,14 +13,21 @@ import {
VModal, VModal,
VSpace, VSpace,
} from "@halo-dev/components"; } from "@halo-dev/components";
// libs
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { roleLabels } from "@/constants/labels";
import { rbacAnnotations } from "@/constants/annotations";
import YAML from "yaml"; import YAML from "yaml";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { useMagicKeys } from "@vueuse/core"; import { useMagicKeys } from "@vueuse/core";
import { reset, submitForm } from "@formkit/core"; import { reset, submitForm } from "@formkit/core";
// constants
import { rbacAnnotations } from "@/constants/annotations";
// hooks
import { useFetchRole } from "@/modules/system/roles/composables/use-role";
import type { FormKitOptionsList } from "@formkit/inputs";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
visible: boolean; visible: boolean;
@ -64,7 +74,6 @@ const initialFormState: FormState = {
raw: "", raw: "",
}; };
const roles = ref<Role[]>([]);
const formState = ref<FormState>(cloneDeep(initialFormState)); const formState = ref<FormState>(cloneDeep(initialFormState));
const selectedRole = ref(""); const selectedRole = ref("");
@ -80,50 +89,59 @@ const modalWidth = computed(() => {
return formState.value.rawMode ? 800 : 700; return formState.value.rawMode ? 800 : 700;
}); });
const basicRoles = computed(() => { const { roles } = useFetchRole();
return roles.value.filter( const rolesMap = computed<FormKitOptionsList>(() => {
(role) => role.metadata?.labels?.[roleLabels.TEMPLATE] !== "true" return roles.value.map((role) => {
); return {
label:
role.metadata?.annotations?.[rbacAnnotations.DISPLAY_NAME] ||
role.metadata.name,
value: role.metadata?.name,
};
});
}); });
const { Command_Enter } = useMagicKeys(); const { Command_Enter } = useMagicKeys();
watch(props, (newVal) => { watchEffect(() => {
let keyboardWatcher; if (Command_Enter.value && props.visible) {
if (newVal.visible) {
keyboardWatcher = watch(Command_Enter, (v) => {
if (v) {
submitForm("user-form"); submitForm("user-form");
} }
});
} else {
keyboardWatcher?.unwatch();
}
if (newVal.visible && props.user) {
formState.value.user = cloneDeep(props.user);
return;
}
formState.value = cloneDeep(initialFormState);
reset("user-form");
}); });
const handleFetchRoles = async () => { watch(
try { () => props.visible,
const { data } = await apiClient.extension.role.listv1alpha1Role(); (visible) => {
roles.value = data.items; if (!visible) {
} catch (e) { handleResetForm();
console.error(e);
} }
}; }
);
const handleVisibleChange = (visible: boolean) => { watch(
() => props.user,
(user) => {
if (user) {
formState.value.user = cloneDeep(user);
} else {
handleResetForm();
}
}
);
const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible); emit("update:visible", visible);
if (!visible) { if (!visible) {
emit("close"); emit("close");
} }
}; };
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
formState.value.user.metadata.name = uuid();
reset("user-form");
};
const handleCreateUser = async () => { const handleCreateUser = async () => {
try { try {
formState.value.saving = true; formState.value.saving = true;
@ -149,7 +167,7 @@ const handleCreateUser = async () => {
}); });
} }
handleVisibleChange(false); onVisibleChange(false);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally { } finally {
@ -166,15 +184,13 @@ const handleRawModeChange = () => {
formState.value.user = YAML.parse(formState.value.raw); formState.value.user = YAML.parse(formState.value.raw);
} }
}; };
onMounted(handleFetchRoles);
</script> </script>
<template> <template>
<VModal <VModal
:title="creationModalTitle" :title="creationModalTitle"
:visible="visible" :visible="visible"
:width="modalWidth" :width="modalWidth"
@update:visible="handleVisibleChange" @update:visible="onVisibleChange"
> >
<template #actions> <template #actions>
<div class="modal-header-action" @click="handleRawModeChange"> <div class="modal-header-action" @click="handleRawModeChange">
@ -213,14 +229,7 @@ onMounted(handleFetchRoles);
></FormKit> ></FormKit>
<FormKit <FormKit
v-model="selectedRole" v-model="selectedRole"
:options=" :options="rolesMap"
basicRoles.map((role:Role) => {
return {
label: role.metadata?.annotations?.[rbacAnnotations.DISPLAY_NAME] || role.metadata.name,
value: role.metadata?.name,
};
})
"
label="角色" label="角色"
type="select" type="select"
></FormKit> ></FormKit>
@ -250,7 +259,7 @@ onMounted(handleFetchRoles);
> >
保存 + 保存 +
</VButton> </VButton>
<VButton @click="handleVisibleChange(false)"> Esc</VButton> <VButton @click="onVisibleChange(false)"> Esc</VButton>
</VSpace> </VSpace>
</template> </template>
</VModal> </VModal>