mirror of https://github.com/halo-dev/halo-admin
refactor: user and role editing modal
Signed-off-by: Ryan Wang <i@ryanc.cc>pull/603/head
parent
3722a7fb36
commit
4031eb7d85
|
@ -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="角色">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue