基于naive ui重构设置-权限页面及弹窗

This commit is contained in:
jingrow 2025-12-30 01:51:24 +08:00
parent b0172b9aa8
commit 81f6620bf1
5 changed files with 725 additions and 262 deletions

View File

@ -15,7 +15,7 @@
<n-form-item :label="$t('Endpoint')" :required="true"> <n-form-item :label="$t('Endpoint')" :required="true">
<n-input <n-input
v-model:value="endpoint" v-model:value="endpoint"
:placeholder="$t('Enter webhook endpoint URL')" placeholder=""
:size="inputSize" :size="inputSize"
class="w-full" class="w-full"
/> />
@ -23,7 +23,7 @@
<n-form-item :label="$t('Secret Key')"> <n-form-item :label="$t('Secret Key')">
<n-input <n-input
v-model:value="secret" v-model:value="secret"
:placeholder="$t('Enter secret key (optional)')" placeholder=""
:size="inputSize" :size="inputSize"
class="w-full" class="w-full"
> >

View File

@ -1,138 +1,198 @@
<template> <template>
<Dialog <n-modal
v-if="role" v-if="role"
:options="{ title: `${role.title}`, size: 'xl' }" v-model:show="show"
v-model="show" preset="card"
:title="role.title"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="role-configure-modal"
> >
<template v-slot:body-content> <template #header>
<FTabs <span class="text-lg font-semibold">{{ role.title }}</span>
:tabs="[ </template>
{ <n-tabs v-model:value="activeTab" type="line" animated>
label: $t('Members'), <n-tab-pane name="members" :tab="$t('Members')">
value: 'members', <n-space vertical :size="20">
}, <div class="flex gap-2 items-end">
{ <n-select
label: $t('Settings'), v-model:value="selectedMember"
value: 'settings', :options="autoCompleteList"
}, :placeholder="$t('Select member to add')"
]" :size="inputSize"
v-model="tabIndex" class="flex-1"
> filterable
<TabList v-slot="{ tab, selected }" class="pl-0">
<div
class="flex cursor-pointer items-center gap-1.5 border-b border-transparent py-3 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900 focus:outline-none focus:transition-none [&>div]:pl-0"
:class="{ 'text-gray-900': selected }"
>
<span>{{ tab.label }}</span>
</div>
</TabList>
<TabPanel v-slot="{ tab }">
<div v-if="tab.value === 'members'" class="text-base">
<div class="my-4 flex gap-2">
<div class="flex-1">
<Autocomplete
:options="autoCompleteList"
v-model="member"
:placeholder="$t('Select member to add')"
/>
</div>
<Button
variant="solid"
:label="$t('Add Member')"
:disabled="!member?.value"
:loading="$resources.role.addUser?.loading"
@click="() => addUser(member.value)"
/> />
<n-button
type="primary"
:disabled="!selectedMember"
:loading="$resources.role.addUser?.loading"
@click="() => addUser(selectedMember)"
:size="buttonSize"
>
{{ $t('Add Member') }}
</n-button>
</div> </div>
<div class="rounded border px-3"> <n-card>
<div class="mt-2 text-gray-600">{{ $t('Members') }}</div> <div class="mb-3 text-gray-600 font-medium">{{ $t('Members') }}</div>
<div <div
v-if="roleUsers.length === 0" v-if="roleUsers.length === 0"
class="p-6 text-center text-gray-500" class="p-6 text-center text-gray-500"
> >
<span>{{ $t('No members have been added to this role yet.') }}</span> <span>{{ $t('No members have been added to this role yet.') }}</span>
</div> </div>
<div v-else class="flex flex-col divide-y"> <div v-else class="space-y-2">
<div <div
v-for="user in roleUsers" v-for="user in roleUsers"
class="flex justify-between py-3" :key="user.user"
class="flex justify-between items-center py-2 border-b border-gray-100 last:border-0"
>
<UserWithAvatarCell
:avatarImage="user.user_image"
:fullName="user.full_name"
:email="user.user"
/>
<n-button
tertiary
circle
size="small"
@click="() => removeUser(user.user)"
> >
<UserWithAvatarCell <template #icon>
:avatarImage="user.user_image" <n-icon><XIcon /></n-icon>
:fullName="user.full_name" </template>
:email="user.user" </n-button>
:key="user.user" </div>
/> </div>
<Button variant="ghost" @click="() => removeUser(user.user)"> </n-card>
<template #icon> </n-space>
<i-lucide-x class="h-4 w-4 text-gray-600" /> </n-tab-pane>
</template> <n-tab-pane name="settings" :tab="$t('Settings')">
</Button> <n-space vertical :size="20">
<n-card>
<n-form-item>
<div class="flex items-center justify-between w-full">
<div class="flex-1">
<div class="text-base font-medium text-gray-900 mb-1">
{{ $t('Admin Access') }}
</div>
<div class="text-sm text-gray-600">
{{ $t('Grant members permissions similar to team owner. Includes access to all pages and settings.') }}
</div>
</div> </div>
</div> <n-switch
</div> :value="adminAccess"
</div> @update:value="(val) => adminAccess = val"
<div v-else-if="tab.value === 'settings'" class="mt-4 text-base"> size="medium"
<div class="space-y-3">
<div class="rounded border p-4">
<Switch
class="ml-2"
v-model="adminAccess"
:label="$t('Admin Access')"
:description="$t('Grant members permissions similar to team owner. Includes access to all pages and settings.')"
/> />
</div> </div>
<div class="space-y-1 rounded border p-4"> </n-form-item>
<h2 class="mb-2 ml-2 font-semibold">{{ $t('Page Access Permissions') }}</h2> </n-card>
<Switch <n-card>
v-model="allowBilling" <h2 class="mb-4 text-base font-semibold">{{ $t('Page Access Permissions') }}</h2>
:label="$t('Allow Billing Access')" <n-space vertical :size="16">
:disabled="adminAccess" <n-form-item>
/> <div class="flex items-center justify-between w-full">
<Switch <span class="text-base">{{ $t('Allow Billing Access') }}</span>
v-model="allowApps" <n-switch
:label="$t('Allow Apps Access')" :value="allowBilling"
:disabled="adminAccess" @update:value="(val) => allowBilling = val"
/> :disabled="adminAccess"
<Switch size="medium"
v-if="$team.pg.jerp_partner" />
v-model="allowPartner" </div>
:label="$t('Allow Partner Access')" </n-form-item>
:disabled="adminAccess" <n-form-item>
/> <div class="flex items-center justify-between w-full">
<Switch <span class="text-base">{{ $t('Allow Apps Access') }}</span>
v-model="allowSiteCreation" <n-switch
:label="$t('Allow Site Creation')" :value="allowApps"
:disabled="adminAccess" @update:value="(val) => allowApps = val"
/> :disabled="adminAccess"
<Switch size="medium"
v-model="allowBenchCreation" />
:label="$t('Allow Release Group Creation')" </div>
:disabled="adminAccess" </n-form-item>
/> <n-form-item v-if="$team.pg.jerp_partner">
<Switch <div class="flex items-center justify-between w-full">
v-model="allowServerCreation" <span class="text-base">{{ $t('Allow Partner Access') }}</span>
:label="$t('Allow Server Creation')" <n-switch
:disabled="adminAccess" :value="allowPartner"
/> @update:value="(val) => allowPartner = val"
<Switch :disabled="adminAccess"
v-model="allowWebhookConfiguration" size="medium"
:label="$t('Allow Webhook Configuration')" />
:disabled="adminAccess" </div>
/> </n-form-item>
</div> <n-form-item>
</div> <div class="flex items-center justify-between w-full">
</div> <span class="text-base">{{ $t('Allow Site Creation') }}</span>
</TabPanel> <n-switch
</FTabs> :value="allowSiteCreation"
</template> @update:value="(val) => allowSiteCreation = val"
</Dialog> :disabled="adminAccess"
size="medium"
/>
</div>
</n-form-item>
<n-form-item>
<div class="flex items-center justify-between w-full">
<span class="text-base">{{ $t('Allow Release Group Creation') }}</span>
<n-switch
:value="allowBenchCreation"
@update:value="(val) => allowBenchCreation = val"
:disabled="adminAccess"
size="medium"
/>
</div>
</n-form-item>
<n-form-item>
<div class="flex items-center justify-between w-full">
<span class="text-base">{{ $t('Allow Server Creation') }}</span>
<n-switch
:value="allowServerCreation"
@update:value="(val) => allowServerCreation = val"
:disabled="adminAccess"
size="medium"
/>
</div>
</n-form-item>
<n-form-item>
<div class="flex items-center justify-between w-full">
<span class="text-base">{{ $t('Allow Webhook Configuration') }}</span>
<n-switch
:value="allowWebhookConfiguration"
@update:value="(val) => allowWebhookConfiguration = val"
:disabled="adminAccess"
size="medium"
/>
</div>
</n-form-item>
</n-space>
</n-card>
</n-space>
</n-tab-pane>
</n-tabs>
</n-modal>
</template> </template>
<script> <script>
import { Switch, Tabs, TabList, TabPanel } from 'jingrow-ui'; import {
NModal,
NTabs,
NTabPane,
NSpace,
NSelect,
NButton,
NIcon,
NCard,
NFormItem,
NSwitch,
} from 'naive-ui';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import UserWithAvatarCell from '../UserWithAvatarCell.vue'; import UserWithAvatarCell from '../UserWithAvatarCell.vue';
import XIcon from '~icons/lucide/x';
export default { export default {
props: { props: {
@ -140,16 +200,24 @@ export default {
}, },
components: { components: {
UserWithAvatarCell, UserWithAvatarCell,
FTabs: Tabs, NModal,
TabPanel, NTabs,
TabList, NTabPane,
Switch, NSpace,
NSelect,
NButton,
NIcon,
NCard,
NFormItem,
NSwitch,
XIcon,
}, },
data() { data() {
return { return {
member: {}, selectedMember: null,
show: true, show: true,
tabIndex: 0, activeTab: 'members',
windowWidth: window.innerWidth,
}; };
}, },
resources: { resources: {
@ -180,6 +248,21 @@ export default {
?.filter(({ user }) => isNotGroupMember(user)) ?.filter(({ user }) => isNotGroupMember(user))
.map(({ user }) => ({ label: user, value: user })); .map(({ user }) => ({ label: user, value: user }));
}, },
isMobile() {
return this.windowWidth <= 768;
},
modalStyle() {
return {
width: this.isMobile ? '95vw' : '900px',
maxWidth: this.isMobile ? '95vw' : '90vw',
};
},
inputSize() {
return this.isMobile ? 'medium' : 'large';
},
buttonSize() {
return this.isMobile ? 'medium' : 'medium';
},
adminAccess: { adminAccess: {
get() { get() {
return !!this.role?.admin_access; return !!this.role?.admin_access;
@ -286,14 +369,16 @@ export default {
}, },
}, },
methods: { methods: {
handleResize() {
this.windowWidth = window.innerWidth;
},
addUser(user) { addUser(user) {
if (!user) return; if (!user) return;
if (this.$resources.role.addUser.loading) return; if (this.$resources.role.addUser.loading) return;
toast.success(this.$t('Added {user} to {role}', { user, role: this.role.title }), { duration: 2000 }); toast.success(this.$t('Added {user} to {role}', { user, role: this.role.title }), { duration: 2000 });
this.member = {}; this.selectedMember = null;
this.$resources.role.addUser.submit({ user }); this.$resources.role.addUser.submit({ user });
//
setTimeout(() => { setTimeout(() => {
if (this.$resources.role) { if (this.$resources.role) {
this.$resources.role.reload(); this.$resources.role.reload();
@ -306,7 +391,6 @@ export default {
toast.success(this.$t('Removed {user} from {role}', { user, role: this.role.title }), { duration: 2000 }); toast.success(this.$t('Removed {user} from {role}', { user, role: this.role.title }), { duration: 2000 });
this.$resources.role.removeUser.submit({ user }); this.$resources.role.removeUser.submit({ user });
//
setTimeout(() => { setTimeout(() => {
if (this.$resources.role) { if (this.$resources.role) {
this.$resources.role.reload(); this.$resources.role.reload();
@ -314,5 +398,57 @@ export default {
}, 500); }, 500);
}, },
}, },
mounted() {
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
beforeUnmount() {
window.removeEventListener('resize', this.handleResize);
},
}; };
</script> </script>
<style scoped>
:deep(.role-configure-modal .n-card) {
width: 900px;
max-width: 90vw;
}
:deep(.role-configure-modal .n-card-body) {
padding: 24px;
}
:deep(.role-configure-modal .n-tabs) {
margin-top: 0;
}
:deep(.role-configure-modal .n-tab-pane) {
padding: 20px 0;
}
:deep(.role-configure-modal .n-form-item) {
margin-bottom: 0;
}
@media (max-width: 768px) {
:deep(.role-configure-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.role-configure-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.role-configure-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.role-configure-modal .n-tab-pane) {
padding: 16px 0;
}
}
</style>

View File

@ -1,20 +1,86 @@
<template> <template>
<ObjectList :options="listOptions" /> <div class="role-list-container">
<n-card class="settings-card">
<ObjectList :options="listOptions" />
</n-card>
<!-- 创建角色弹窗 -->
<n-modal
v-model:show="showCreateRoleDialog"
preset="card"
:title="$t('Create Role')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="create-role-modal"
>
<template #header>
<span class="text-lg font-semibold">{{ $t('Create Role') }}</span>
</template>
<n-space vertical :size="20">
<n-form-item :label="$t('Role')" :required="true">
<n-input
v-model:value="newRoleTitle"
placeholder=""
:size="inputSize"
class="w-full"
@keyup.enter="handleCreateRole"
/>
</n-form-item>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="showCreateRoleDialog = false" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="createRoleLoading"
:disabled="!newRoleTitle"
@click="handleCreateRole"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Confirm') }}
</n-button>
</n-space>
</template>
</n-modal>
<!-- 删除角色确认弹窗 -->
<n-modal
v-model:show="showDeleteRoleDialog"
preset="dialog"
:title="$t('Delete Role')"
:positive-text="$t('Delete')"
:positive-button-props="{ type: 'error' }"
:loading="deleteRoleLoading"
:mask-closable="true"
:close-on-esc="true"
@positive-click="handleDeleteRole"
>
<div class="py-4">
<p class="text-base" v-html="$t('Are you sure you want to delete role <b>{role}</b>?', { role: selectedRole?.title || '' })"></p>
</div>
</n-modal>
</div>
</template> </template>
<script setup lang="jsx"> <script setup lang="jsx">
import { h, ref, getCurrentInstance } from 'vue'; import { h, ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { FeatherIcon } from 'jingrow-ui'; import { FeatherIcon } from 'jingrow-ui';
import { icon, renderDialog, confirmDialog } from '../../utils/components'; import { NCard, NModal, NSpace, NFormItem, NInput, NButton } from 'naive-ui';
import { icon, renderDialog } from '../../utils/components';
import ObjectList from '../ObjectList.vue'; import ObjectList from '../ObjectList.vue';
import RoleConfigureDialog from './RoleConfigureDialog.vue'; import RoleConfigureDialog from './RoleConfigureDialog.vue';
import router from '../../router'; import { useRouter } from 'vue-router';
import UserAvatarGroup from '../AvatarGroup.vue'; import UserAvatarGroup from '../AvatarGroup.vue';
import { getToastErrorMessage } from '../../utils/toast'; import { getToastErrorMessage } from '../../utils/toast';
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key); const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
const router = useRouter();
const listOptions = ref({ const listOptions = ref({
pagetype: 'Jcloud Role', pagetype: 'Jcloud Role',
@ -75,22 +141,9 @@ const listOptions = ref({
label: $t('Delete Role'), label: $t('Delete Role'),
onClick() { onClick() {
if (roleListResource.delete.loading) return; if (roleListResource.delete.loading) return;
confirmDialog({ selectedRole.value = row;
title: $t('Delete Role'), roleListResourceRef.value = roleListResource;
message: $t('Are you sure you want to delete role <b>{role}</b>?', { role: row.title }), showDeleteRoleDialog.value = true;
onSuccess({ hide }) {
if (roleListResource.delete.loading) return;
toast.promise(roleListResource.delete.submit(row.name), {
loading: $t('Deleting role...'),
success: () => {
roleListResource.reload();
hide();
return $t('Role {role} deleted', { role: row.title });
},
error: (e) => getToastErrorMessage(e),
});
},
});
}, },
}, },
]; ];
@ -102,42 +155,158 @@ const listOptions = ref({
}; };
}, },
primaryAction({ listResource: groups }) { primaryAction({ listResource: groups }) {
roleListResourceRef.value = groups;
return { return {
label: $t('New Role'), label: $t('New Role'),
variant: 'solid',
slots: { slots: {
prefix: icon('plus'), prefix: icon('plus'),
}, },
onClick() { onClick() {
confirmDialog({ newRoleTitle.value = '';
title: $t('Create Role'), showCreateRoleDialog.value = true;
fields: [
{
fieldname: 'title',
label: $t('Role'),
autocomplete: 'off',
},
],
primaryAction: {
label: $t('Confirm'),
variant: 'solid'
},
onSuccess({ hide, values }) {
if (values.title) {
return groups.insert.submit(
{ title: values.title },
{ onSuccess: hide },
);
}
return null;
},
});
}, },
}; };
}, },
}); });
const windowWidth = ref(window.innerWidth);
const showCreateRoleDialog = ref(false);
const showDeleteRoleDialog = ref(false);
const newRoleTitle = ref('');
const selectedRole = ref(null);
const roleListResourceRef = ref(null);
const createRoleLoading = ref(false);
const deleteRoleLoading = ref(false);
const isMobile = computed(() => windowWidth.value <= 768);
const modalStyle = computed(() => ({
width: isMobile.value ? '95vw' : '700px',
maxWidth: isMobile.value ? '95vw' : '90vw',
}));
const inputSize = computed(() => isMobile.value ? 'medium' : 'large');
const buttonSize = computed(() => isMobile.value ? 'medium' : 'medium');
function handleResize() {
windowWidth.value = window.innerWidth;
}
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
async function handleCreateRole() {
if (!newRoleTitle.value || !roleListResourceRef.value) return;
if (createRoleLoading.value) return;
createRoleLoading.value = true;
try {
await roleListResourceRef.value.insert.submit({ title: newRoleTitle.value });
toast.success($t('Role created successfully'));
showCreateRoleDialog.value = false;
newRoleTitle.value = '';
} catch (error) {
toast.error(getToastErrorMessage(error));
} finally {
createRoleLoading.value = false;
}
}
async function handleDeleteRole() {
if (!selectedRole.value || !roleListResourceRef.value) return;
if (deleteRoleLoading.value) return;
const roleTitle = selectedRole.value.title;
deleteRoleLoading.value = true;
try {
await toast.promise(
roleListResourceRef.value.delete.submit(selectedRole.value.name),
{
loading: $t('Deleting role...'),
success: () => {
roleListResourceRef.value.reload();
showDeleteRoleDialog.value = false;
selectedRole.value = null;
return $t('Role {role} deleted', { role: roleTitle });
},
error: (e) => getToastErrorMessage(e),
}
);
} finally {
deleteRoleLoading.value = false;
}
}
function configureRole(row) { function configureRole(row) {
renderDialog(h(RoleConfigureDialog, { roleId: row.name })); renderDialog(h(RoleConfigureDialog, { roleId: row.name }));
} }
</script> </script>
<style scoped>
.role-list-container {
width: 100%;
padding: 0;
}
.settings-card {
border-radius: 12px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease;
}
.settings-card:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:deep(.create-role-modal .n-card) {
width: 700px;
max-width: 90vw;
}
:deep(.create-role-modal .n-card-body) {
padding: 24px;
}
:deep(.create-role-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.create-role-modal .n-input) {
width: 100%;
}
:deep(.create-role-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.create-role-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.create-role-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.create-role-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.create-role-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.create-role-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,58 +1,158 @@
<template> <template>
<div class="mb-5 flex items-center gap-2"> <div class="role-permissions-container">
<Tooltip :text="$t('All Roles')"> <div class="mb-5 flex items-center gap-2">
<Button :route="{ name: 'SettingsPermissionRoles' }"> <n-tooltip>
<template #icon> <template #trigger>
<i-lucide-arrow-left class="h-4 w-4 text-gray-700" /> <n-button
quaternary
@click="router.push({ name: 'SettingsPermissionRoles' })"
>
<template #icon>
<n-icon><ArrowLeftIcon /></n-icon>
</template>
</n-button>
</template> </template>
</Button> {{ $t('All Roles') }}
</Tooltip> </n-tooltip>
<h3 class="text-lg font-medium text-gray-900"> <h3 class="text-lg font-medium text-gray-900">
{{ role.pg?.title }} {{ role.pg?.title }}
</h3> </h3>
<Tooltip :text="$t('Admin Role')" v-if="role.pg.admin_access"> <n-tooltip v-if="role.pg?.admin_access">
<FeatherIcon name="shield" class="h-5 w-5 text-gray-700" /> <template #trigger>
</Tooltip> <n-icon class="h-5 w-5 text-gray-700">
<ShieldIcon />
</n-icon>
</template>
{{ $t('Admin Role') }}
</n-tooltip>
</div>
<n-card class="settings-card">
<ObjectList
:options="rolePermissions"
@update:selections="(e) => (selectedItems = e)"
>
<template #header-left="{ listResource }">
<n-dropdown :options="getDropdownOptions(listResource)" trigger="click">
<n-button>
<template #icon>
<n-icon><AppWindowIcon /></n-icon>
</template>
{{ currentDropdownOption.label }}
<template #suffix>
<n-icon><ChevronDownIcon /></n-icon>
</template>
</n-button>
</n-dropdown>
</template>
</ObjectList>
</n-card>
<!-- 添加权限弹窗 -->
<n-modal
v-model:show="showAddPermissionDialog"
preset="card"
:title="$t('Add Permission')"
:style="modalStyle"
:mask-closable="true"
:close-on-esc="true"
class="add-permission-modal"
>
<template #header>
<span class="text-lg font-semibold">{{ $t('Add Permission') }}</span>
</template>
<n-space vertical :size="20">
<n-form-item :label="$t('Select {type}', { type: currentDropdownOption.pagetype === 'Release Group' ? $t('Release Group') : currentDropdownOption.pagetype })">
<!-- 这里需要使用 LinkControl 或类似的组件来选择文档 -->
<n-input
v-model:value="selectedDocument"
:placeholder="$t('Select {type}', { type: currentDropdownOption.pagetype === 'Release Group' ? $t('Release Group') : currentDropdownOption.pagetype })"
:size="inputSize"
class="w-full"
readonly
@click="showDocumentSelector = true"
/>
</n-form-item>
</n-space>
<template #action>
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
<n-button @click="showAddPermissionDialog = false" :block="isMobile" :size="buttonSize">
{{ $t('Cancel') }}
</n-button>
<n-button
type="primary"
:loading="addPermissionLoading"
:disabled="!selectedDocument"
@click="handleAddPermission"
:block="isMobile"
:size="buttonSize"
>
{{ $t('Add') }}
</n-button>
</n-space>
</template>
</n-modal>
</div> </div>
<ObjectList
:options="rolePermissions"
@update:selections="(e) => (selectedItems = e)"
>
<template #header-left="{ listResource }">
<Dropdown :options="getDropdownOptions(listResource)">
<Button>
<template #prefix>
<LucideAppWindow class="h-4 w-4 text-gray-500" />
</template>
{{ currentDropdownOption.label }}
<template #suffix>
<FeatherIcon name="chevron-down" class="h-4 w-4 text-gray-500" />
</template>
</Button>
</Dropdown>
</template>
</ObjectList>
</template> </template>
<script setup> <script setup>
import { import {
Button,
Dropdown,
createDocumentResource, createDocumentResource,
createResource, createResource,
} from 'jingrow-ui'; } from 'jingrow-ui';
import { computed, h, ref, getCurrentInstance } from 'vue'; import { computed, h, ref, getCurrentInstance, onMounted, onBeforeUnmount } from 'vue';
import LucideAppWindow from '~icons/lucide/app-window'; import {
NCard,
NButton,
NDropdown,
NTooltip,
NIcon,
NModal,
NSpace,
NFormItem,
NInput,
} from 'naive-ui';
import ArrowLeftIcon from '~icons/lucide/arrow-left';
import ShieldIcon from '~icons/lucide/shield';
import AppWindowIcon from '~icons/lucide/app-window';
import ChevronDownIcon from '~icons/lucide/chevron-down';
import ObjectList from '../ObjectList.vue'; import ObjectList from '../ObjectList.vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../../utils/toast'; import { getToastErrorMessage } from '../../utils/toast';
import { confirmDialog, icon, renderDialog } from '../../utils/components'; import { icon, renderDialog } from '../../utils/components';
import RoleConfigureDialog from './RoleConfigureDialog.vue'; import RoleConfigureDialog from './RoleConfigureDialog.vue';
import { useRouter } from 'vue-router';
const instance = getCurrentInstance(); const instance = getCurrentInstance();
const $t = instance?.appContext.config.globalProperties.$t || ((key) => key); const $t = instance?.appContext.config.globalProperties.$t || ((key) => key);
const router = useRouter();
let selectedItems = ref(new Set()); let selectedItems = ref(new Set());
const windowWidth = ref(window.innerWidth);
const showAddPermissionDialog = ref(false);
const showDocumentSelector = ref(false);
const selectedDocument = ref('');
const addPermissionLoading = ref(false);
const permissionsResourceRef = ref(null);
const isMobile = computed(() => windowWidth.value <= 768);
const modalStyle = computed(() => ({
width: isMobile.value ? '95vw' : '700px',
maxWidth: isMobile.value ? '95vw' : '90vw',
}));
const inputSize = computed(() => isMobile.value ? 'medium' : 'large');
const buttonSize = computed(() => isMobile.value ? 'medium' : 'medium');
function handleResize() {
windowWidth.value = window.innerWidth;
}
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
});
const props = defineProps({ const props = defineProps({
roleId: { type: String, required: true }, roleId: { type: String, required: true },
@ -86,7 +186,8 @@ const currentDropdownOption = ref(dropdownOptions[0]);
function getDropdownOptions(listResource) { function getDropdownOptions(listResource) {
return dropdownOptions.map((option) => { return dropdownOptions.map((option) => {
return { return {
...option, label: option.label,
key: option.pagetype,
onClick: () => { onClick: () => {
currentDropdownOption.value = option; currentDropdownOption.value = option;
let filters = { let filters = {
@ -179,59 +280,109 @@ const rolePermissions = ref({
prefix: icon('plus'), prefix: icon('plus'),
}, },
onClick() { onClick() {
const pagetypeLabel = currentDropdownOption.value.pagetype === 'Release Group' permissionsResourceRef.value = permissions;
? $t('Release Group') selectedDocument.value = '';
: currentDropdownOption.value.pagetype; showAddPermissionDialog.value = true;
confirmDialog({
title: $t('Add Permission'),
message: '',
fields: [
{
label: $t('Select {type}', { type: pagetypeLabel }),
type: 'link',
fieldname: 'document_name',
options: {
pagetype: currentDropdownOption.value.pagetype,
filters: {
name: [
'not in',
permissions.data.map(
(p) => p[currentDropdownOption.value.fieldname],
) || '',
],
status: ['!=', 'Archived'],
},
},
},
],
primaryAction: {
label: $t('Add'),
onClick({ values }) {
let key = currentDropdownOption.value.fieldname;
toast.promise(
docInsert.submit({
pg: {
pagetype: 'Jcloud Role Permission',
role: props.roleId,
[key]: values.document_name,
},
}),
{
loading: $t('Adding permission...'),
success() {
permissions.reload();
return $t('Permission added successfully');
},
error: (e) => getToastErrorMessage(e),
},
);
},
},
});
}, },
}, },
].filter((action) => (action.condition ? action.condition() : true)); ].filter((action) => (action.condition ? action.condition() : true));
}, },
}); });
</script>
async function handleAddPermission() {
if (!selectedDocument.value || !permissionsResourceRef.value) return;
if (addPermissionLoading.value) return;
const key = currentDropdownOption.value.fieldname;
addPermissionLoading.value = true;
try {
await toast.promise(
docInsert.submit({
pg: {
pagetype: 'Jcloud Role Permission',
role: props.roleId,
[key]: selectedDocument.value,
},
}),
{
loading: $t('Adding permission...'),
success() {
permissionsResourceRef.value.reload();
showAddPermissionDialog.value = false;
selectedDocument.value = '';
return $t('Permission added successfully');
},
error: (e) => getToastErrorMessage(e),
}
);
} finally {
addPermissionLoading.value = false;
}
}
</script>
<style scoped>
.role-permissions-container {
width: 100%;
padding: 0;
}
.settings-card {
border-radius: 12px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease;
}
.settings-card:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
:deep(.add-permission-modal .n-card) {
width: 700px;
max-width: 90vw;
}
:deep(.add-permission-modal .n-card-body) {
padding: 24px;
}
:deep(.add-permission-modal .n-form-item) {
margin-bottom: 0;
}
:deep(.add-permission-modal .n-input) {
width: 100%;
}
:deep(.add-permission-modal .n-card__action) {
padding: 16px 24px;
}
@media (max-width: 768px) {
:deep(.add-permission-modal .n-card) {
width: 95vw !important;
max-width: 95vw !important;
margin: 20px auto;
border-radius: 12px;
}
:deep(.add-permission-modal .n-card-body) {
padding: 20px 16px;
}
:deep(.add-permission-modal .n-card__header) {
padding: 16px;
font-size: 18px;
}
:deep(.add-permission-modal .n-card__action) {
padding: 12px 16px;
}
:deep(.add-permission-modal .n-button) {
width: 100%;
font-size: 16px;
height: 44px;
}
}
</style>

View File

@ -1,7 +1,14 @@
<template> <template>
<div class="p-5"> <div class="permissions-settings-container">
<router-view /> <router-view />
</div> </div>
</template> </template>
<script setup></script> <script setup></script>
<style scoped>
.permissions-settings-container {
width: 100%;
padding: 0;
}
</style>