Merge branch 'dev3' of https://dev.azure.com/alicorn-dev/KintoneAppBuilder/_git/KintoneAppBuilder into dev3
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { boot } from 'quasar/wrappers';
|
import { boot } from 'quasar/wrappers';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
import {router} from 'src/router';
|
import {router} from 'src/router';
|
||||||
|
import { IResponse } from 'src/types/BaseTypes';
|
||||||
|
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
|
|||||||
71
frontend/src/components/dialog/UserSelectBox.vue
Normal file
71
frontend/src/components/dialog/UserSelectBox.vue
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<detail-field-table
|
||||||
|
detailField="description"
|
||||||
|
:name="name"
|
||||||
|
:type="type"
|
||||||
|
:filter="filter"
|
||||||
|
:columns="columns"
|
||||||
|
:fetchData="fetchUsers"
|
||||||
|
@update:selected="(item) => { selected = item }">
|
||||||
|
|
||||||
|
<template v-slot:body-cell-status="props">
|
||||||
|
<div class="row">
|
||||||
|
<div v-if="props.row.isActive">
|
||||||
|
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
|
||||||
|
label="システム管理者" size="sm" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</detail-field-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ref, PropType } from 'vue';
|
||||||
|
import { IUser } from 'src/types/UserTypes';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import DetailFieldTable from './DetailFieldTable.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserSelectBox',
|
||||||
|
components: {
|
||||||
|
DetailFieldTable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
filter: String,
|
||||||
|
filterInitRowsFunc: {
|
||||||
|
type: Function as PropType<(user: IUser) => boolean>,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
const selected = ref<IUser[]>([]);
|
||||||
|
const columns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
||||||
|
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
|
||||||
|
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
|
||||||
|
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
|
||||||
|
{ name: 'status', label: '状況', field: 'status', align: 'left' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
const result = await api.get(`api/v1/users`);
|
||||||
|
return result.data.data.map((item: any) => {
|
||||||
|
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.map(role => role.id) }
|
||||||
|
}).filter(user => !props.filterInitRowsFunc || props.filterInitRowsFunc(user));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
fetchUsers,
|
||||||
|
selected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -125,6 +125,13 @@ const adminLinks: EssentialLinkProps[] = reactive([
|
|||||||
link: '/#/user',
|
link: '/#/user',
|
||||||
target: '_self'
|
target: '_self'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'ロール管理',
|
||||||
|
caption: 'ロールを管理する',
|
||||||
|
icon: 'work',
|
||||||
|
link: '/#/role',
|
||||||
|
target: '_self'
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const version = process.env.version;
|
const version = process.env.version;
|
||||||
|
|||||||
259
frontend/src/pages/RoleManagement.vue
Normal file
259
frontend/src/pages/RoleManagement.vue
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<div class="q-gutter-sm row items-start">
|
||||||
|
<q-breadcrumbs>
|
||||||
|
<q-breadcrumbs-el icon="work" label="ロール管理" />
|
||||||
|
</q-breadcrumbs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" style="min-height: 80vh;">
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="q-pa-md" style="width: 250px">
|
||||||
|
<q-list bordered separator class="rounded-borders">
|
||||||
|
<q-item active v-if="allLoading" active-class="menu-active">
|
||||||
|
<q-item-section class="text-weight-bold"> 読み込み中... </q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-else v-for="item in roles" :key="item.id" clickable v-ripple :active="selected?.id === item.id" @click="roleClicked(item)" active-class="menu-active">
|
||||||
|
<q-item-section class="text-weight-bold"> {{ item.label }} </q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
|
||||||
|
<q-table title="ユーザーリスト" :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading"
|
||||||
|
:pagination="pagination" >
|
||||||
|
|
||||||
|
<template v-slot:top>
|
||||||
|
<q-btn color="primary" :disable="loading || selected?.id == -2" label="追加" @click="showAddRoleDialog" />
|
||||||
|
<q-space />
|
||||||
|
<q-input borderless dense filled debounce="300" v-model="filter" placeholder="検索">
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body-cell-status="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<div class="row">
|
||||||
|
<div v-if="props.row.isActive">
|
||||||
|
<q-chip square color="positive" text-color="white" icon="done" label="使用可能" size="sm" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
|
||||||
|
label="システム管理者" size="sm" />
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:header-cell-status="p">
|
||||||
|
<q-th :props="p">
|
||||||
|
<div class="row items-center">
|
||||||
|
<label class="q-mr-md">{{ p.col.label }}</label>
|
||||||
|
<q-select v-model="statusFilter" :options="options" @update:model-value="updateStatusFilter" borderless
|
||||||
|
dense options-dense style="font-size: 12px; padding-top: 1px;" />
|
||||||
|
</div>
|
||||||
|
</q-th>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body-cell-actions="p">
|
||||||
|
<q-td auto-width :props="p">
|
||||||
|
<table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<show-dialog v-model:visible="showSelectUser" name="ユーザー選択" @close="closeSelectUserDialog" min-width="50vw" min-height="50vh" :ok-btn-auto-close="false" :ok-btn-loading="isAdding">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="dgFilter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<user-select-box ref="userDialog" name="ユーザー" type="single" :filter="dgFilter" :filterInitRowsFunc="filterInitRows" />
|
||||||
|
</show-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="deleteDialog" persistent>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="row items-center">
|
||||||
|
<q-icon name="warning" color="warning" size="2em" />
|
||||||
|
<div class="q-ml-sm text-weight-bold">ロールメンバーを削除</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section class="q-py-none">
|
||||||
|
<!-- <span class="q-ml-sm">この役割を与えられたユーザーは、メンバー役に再配置されます。</span> -->
|
||||||
|
<div class="q-mx-sm">ユーザー「{{targetRow.email}}」を「{{selected.label}}」の役割から</div>
|
||||||
|
<div class="q-mx-sm">本当に外しますか?</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Cancel" color="primary" v-close-popup />
|
||||||
|
<q-btn flat label="OK" color="primary" :loading="deleteUserRoleLoading" @click="deleteUserRole" />
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { IUser } from 'src/types/UserTypes';
|
||||||
|
import ShowDialog from 'src/components/ShowDialog.vue';
|
||||||
|
import UserSelectBox from 'src/components/dialog/UserSelectBox.vue';
|
||||||
|
import TableActionMenu from 'components/TableActionMenu.vue';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
||||||
|
{ name: 'lastName', label: '氏名', field: 'lastName', align: 'left', sortable: true },
|
||||||
|
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
|
||||||
|
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
|
||||||
|
{ name: 'status', label: '状況', field: 'status', align: 'left' },
|
||||||
|
{ name: 'actions', label: '操作', field: 'actions' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const roles = ref([])
|
||||||
|
const selected = ref();
|
||||||
|
|
||||||
|
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
|
||||||
|
|
||||||
|
const allLoading = ref(true);
|
||||||
|
const loading = ref(false);
|
||||||
|
const filter = ref('');
|
||||||
|
const statusFilter = ref('全データ');
|
||||||
|
const dgFilter = ref('');
|
||||||
|
const allRows = ref([]);
|
||||||
|
const targetRow = ref<IUser>();
|
||||||
|
|
||||||
|
const userDialog = ref();
|
||||||
|
const showSelectUser=ref(false);
|
||||||
|
const isAdding = ref(false);
|
||||||
|
const deleteDialog = ref(false);
|
||||||
|
const deleteUserRoleLoading = ref(false);
|
||||||
|
|
||||||
|
const actionList = [
|
||||||
|
// { label: '移動', icon: 'account_tree', action: toEditFlowPage },
|
||||||
|
{ separator: true },
|
||||||
|
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
|
||||||
|
];
|
||||||
|
|
||||||
|
const options = ['全データ', 'システム管理者のみ', '使用可能', '使用不可']
|
||||||
|
|
||||||
|
const roleClicked = async (role) => {
|
||||||
|
selected.value = role;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = computed(() => allRows.value.filter((item) => {
|
||||||
|
if (selected.value.id == -2) {
|
||||||
|
return !item.roles || item.roles.length == 0;
|
||||||
|
}
|
||||||
|
return item.roles?.includes(selected.value.id);
|
||||||
|
}));
|
||||||
|
const rowIds = computed(() => new Set(rows.value?.map((item) => item.id)));
|
||||||
|
|
||||||
|
const getUsers = async (filter = () => true) => {
|
||||||
|
loading.value = true;
|
||||||
|
allRows.value = []
|
||||||
|
const result = await api.get(`api/v1/users`);
|
||||||
|
allRows.value = result.data.data.map((item) => {
|
||||||
|
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active, roles: item.roles.map(role => role.id) }
|
||||||
|
}).filter(filter);
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoles = async () => {
|
||||||
|
const result = await api.get(`api/v1/roles`);
|
||||||
|
roles.value = result.data.data.map((item) => {
|
||||||
|
return { id: item.id, label: item.description }
|
||||||
|
}).concat({ id: -2, label: 'ロールなし' })
|
||||||
|
selected.value = roles.value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateStatusFilter = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'システム管理者のみ':
|
||||||
|
getUsers((row) => row.isSuperuser)
|
||||||
|
break;
|
||||||
|
case '使用可能':
|
||||||
|
getUsers((row) => row.isActive)
|
||||||
|
break;
|
||||||
|
case '使用不可':
|
||||||
|
getUsers((row) => !row.isActive)
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
getUsers()
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
allLoading.value = true;
|
||||||
|
await Promise.all([
|
||||||
|
getRoles(),
|
||||||
|
getUsers()
|
||||||
|
]);
|
||||||
|
allLoading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterInitRows = (row: {id: string}) => {
|
||||||
|
return !rowIds.value.has(row.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const showAddRoleDialog = () => {
|
||||||
|
showSelectUser.value = true;
|
||||||
|
isAdding.value = false;
|
||||||
|
dgFilter.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeSelectUserDialog = async (val: 'OK'|'Cancel') => {
|
||||||
|
showSelectUser.value = true;
|
||||||
|
if (val == 'OK' && userDialog.value.selected[0]) {
|
||||||
|
isAdding.value = true;
|
||||||
|
const user = userDialog.value.selected[0];
|
||||||
|
const result = await api.post(`api/v1/userrole`, {
|
||||||
|
'userid': user.id,
|
||||||
|
'roleids': user.roles.concat(selected.value.id)
|
||||||
|
});
|
||||||
|
await getUsers();
|
||||||
|
}
|
||||||
|
showSelectUser.value = false;
|
||||||
|
isAdding.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRow(user:IUser) {
|
||||||
|
targetRow.value = user;
|
||||||
|
deleteDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUserRole = async () => {
|
||||||
|
if (targetRow.value?.id) {
|
||||||
|
deleteUserRoleLoading.value = true;
|
||||||
|
// TODO 返回不需要重新读
|
||||||
|
const result = await api.post(`api/v1/userrole`, {
|
||||||
|
'userid': targetRow.value.id,
|
||||||
|
'roleids': targetRow.value.roles.filter(e => e !== selected.value.id)
|
||||||
|
});
|
||||||
|
await getUsers();
|
||||||
|
deleteUserRoleLoading.value = false;
|
||||||
|
deleteDialog.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.menu-active {
|
||||||
|
color: white;
|
||||||
|
background: var(--q-primary)
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,30 +27,34 @@
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
|
<q-chip square color="negative" text-color="white" icon="block" label="使用不可" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-chip v-if="props.row.isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
|
|
||||||
label="システム管理者" size="sm" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body-cell-roles="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<div class="row">
|
||||||
|
<q-chip v-if="(props.row as IUserRolesDisplay).isSuperuser" square color="accent" text-color="white" icon="admin_panel_settings"
|
||||||
|
label="システム管理者" size="sm" />
|
||||||
|
<span v-else>{{ (props.row as IUserRolesDisplay).roles.map(r => r.name).join('、') }}</span>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template v-slot:header-cell-status="p">
|
<template v-slot:header-cell-status="p">
|
||||||
<q-th :props="p">
|
<q-th :props="p">
|
||||||
<div class="row items-center">
|
<div class="row items-center">
|
||||||
<label class="q-mr-md">{{ p.col.label }}</label>
|
<label class="q-mr-md">{{ p.col.label }}</label>
|
||||||
<q-select v-model="statusFilter" :options="options" @update:model-value="updateStatusFilter" borderless
|
<q-select v-model="statusFilter" :options="statusFilterOptions" borderless
|
||||||
dense options-dense style="font-size: 12px; padding-top: 1px;" />
|
dense options-dense style="font-size: 12px; padding-top: 1px;" />
|
||||||
</div>
|
</div>
|
||||||
</q-th>
|
</q-th>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-slot:body-cell-actions="p">
|
<template v-slot:body-cell-actions="p">
|
||||||
<q-td :props="p">
|
<q-td auto-width :props="p">
|
||||||
<q-btn-group flat>
|
<table-action-menu :row="p.row" minWidth="180px" max-width="200px" :actions="actionList" />
|
||||||
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
|
|
||||||
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
|
|
||||||
</q-btn-group>
|
|
||||||
</q-td>
|
</q-td>
|
||||||
</template>
|
</template>
|
||||||
</q-table>
|
</q-table>
|
||||||
@@ -91,6 +95,35 @@
|
|||||||
<q-toggle v-model="isSuperuser" />
|
<q-toggle v-model="isSuperuser" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<!--
|
||||||
|
<q-select
|
||||||
|
class="q-mt-xs"
|
||||||
|
filled
|
||||||
|
v-model="roles"
|
||||||
|
multiple
|
||||||
|
:options="roleOptions"
|
||||||
|
label="ロール" emit-value
|
||||||
|
map-options
|
||||||
|
:option-disable="opt => opt.value == -1 ? false : isSuperuser"
|
||||||
|
@add="(opt) => { if (opt.value.value == -1) { } }"
|
||||||
|
hint="各役割には固有の権限があります。">
|
||||||
|
<template v-slot:option="{ itemProps, opt, selected, toggleOption }">
|
||||||
|
<q-item v-bind="itemProps" v-if="opt.value == -1" >
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{opt.label}}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-toggle :model-value="selected" @update:model-value="() => { toggleOption(opt); }" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-separator v-if="opt.value == -1"/>
|
||||||
|
<q-item v-else v-bind="itemProps">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{opt.label}}</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select> -->
|
||||||
|
|
||||||
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
|
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
@@ -153,8 +186,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { api } from 'boot/axios';
|
import { api } from 'boot/axios';
|
||||||
|
import TableActionMenu from 'components/TableActionMenu.vue';
|
||||||
|
import { useUserStore } from 'stores/useUserStore';
|
||||||
|
import { IUserDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
|
||||||
@@ -162,16 +200,25 @@ const columns = [
|
|||||||
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
|
{ name: 'firstName', label: '苗字', field: 'firstName', align: 'left', sortable: true },
|
||||||
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
|
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
|
||||||
{ name: 'status', label: '状況', field: 'status', align: 'left' },
|
{ name: 'status', label: '状況', field: 'status', align: 'left' },
|
||||||
|
{ name: 'roles', label: 'ロール', field: '', align: 'left' },
|
||||||
{ name: 'actions', label: '操作', field: 'actions' }
|
{ name: 'actions', label: '操作', field: 'actions' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const statusFilterOptions = [
|
||||||
|
{ label: '全データ', filter: () => true },
|
||||||
|
{ label: 'システム管理者のみ', filter: (row: IUserRolesDisplay) => row.isSuperuser },
|
||||||
|
{ label: '使用可能', filter: (row: IUserRolesDisplay) => row.isActive },
|
||||||
|
{ label: '使用不可', filter: (row: IUserRolesDisplay) => !row.isActive },
|
||||||
|
]
|
||||||
|
|
||||||
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
|
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const addEditLoading = ref(false);
|
const addEditLoading = ref(false);
|
||||||
const filter = ref('');
|
const filter = ref('');
|
||||||
const statusFilter = ref('全データ');
|
const statusFilter = ref(statusFilterOptions[0]);
|
||||||
const rows = ref([]);
|
const rows = computed(() => userStore.users.filter(statusFilter.value.filter));
|
||||||
|
|
||||||
const show = ref(false);
|
const show = ref(false);
|
||||||
const confirm = ref(false);
|
const confirm = ref(false);
|
||||||
const resetPsw = ref(false);
|
const resetPsw = ref(false);
|
||||||
@@ -184,42 +231,30 @@ const isActive = ref(true);
|
|||||||
|
|
||||||
const isPwd = ref(true);
|
const isPwd = ref(true);
|
||||||
const pwd = ref('');
|
const pwd = ref('');
|
||||||
|
// const roles = ref([]);
|
||||||
const isCreate = ref(true);
|
const isCreate = ref(true);
|
||||||
let editId = ref(0);
|
let editId = ref(0);
|
||||||
|
|
||||||
const getUsers = async (filter = () => true) => {
|
// const roleOptions = ref([
|
||||||
|
// {'value': -1, 'label': 'システム管理者'}
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
const actionList = [
|
||||||
|
{ label: '編集', icon: 'edit_note', action: editRow },
|
||||||
|
{ separator: true },
|
||||||
|
{ label: '削除', icon: 'delete_outline', class: 'text-red', action: removeRow },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getUsers = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const result = await api.get(`api/v1/users`);
|
await userStore.loadUsers();
|
||||||
rows.value = result.data.data.map((item) => {
|
|
||||||
return { id: item.id, firstName: item.first_name, lastName: item.last_name, email: item.email, isSuperuser: item.is_superuser, isActive: item.is_active }
|
|
||||||
}).filter(filter);
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateStatusFilter = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case 'システム管理者のみ':
|
|
||||||
getUsers((row) => row.isSuperuser)
|
|
||||||
break;
|
|
||||||
case '使用可能':
|
|
||||||
getUsers((row) => row.isActive)
|
|
||||||
break;
|
|
||||||
case '使用不可':
|
|
||||||
getUsers((row) => !row.isActive)
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
getUsers()
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await getUsers();
|
await getUsers();
|
||||||
})
|
})
|
||||||
|
|
||||||
const options = ['全データ', 'システム管理者のみ', '使用可能', '使用不可']
|
|
||||||
|
|
||||||
// emulate fetching data from server
|
// emulate fetching data from server
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
// editId.value
|
// editId.value
|
||||||
@@ -227,28 +262,27 @@ const addRow = () => {
|
|||||||
show.value = true;
|
show.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeRow = (row) => {
|
function removeRow(row: IUserDisplay) {
|
||||||
confirm.value = true;
|
confirm.value = true;
|
||||||
editId.value = row.id;
|
editId.value = row.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteUser = () => {
|
const deleteUser = async () => {
|
||||||
api.delete(`api/v1/users/${editId.value}`).then(() => {
|
await userStore.deleteUser(editId.value);
|
||||||
getUsers();
|
getUsers();
|
||||||
})
|
|
||||||
editId.value = 0;
|
editId.value = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const editRow = (row) => {
|
function editRow(row: IUserDisplay) {
|
||||||
isCreate.value = false
|
isCreate.value = false
|
||||||
editId.value = row.id;
|
editId.value = row.id;
|
||||||
|
|
||||||
firstName.value = row.firstName;
|
firstName.value = row.firstName;
|
||||||
lastName.value = row.lastName;
|
lastName.value = row.lastName;
|
||||||
email.value = row.email;
|
email.value = row.email;
|
||||||
pwd.value = row.password;
|
|
||||||
|
|
||||||
isSuperuser.value = row.isSuperuser;
|
isSuperuser.value = row.isSuperuser;
|
||||||
|
// roles.value = row.roles.map(item => item.id);
|
||||||
isActive.value = row.isActive;
|
isActive.value = row.isActive;
|
||||||
|
|
||||||
isPwd.value = true;
|
isPwd.value = true;
|
||||||
@@ -269,6 +303,7 @@ const onSubmit = () => {
|
|||||||
'is_superuser': isSuperuser.value,
|
'is_superuser': isSuperuser.value,
|
||||||
'is_active': isActive.value,
|
'is_active': isActive.value,
|
||||||
'email': email.value,
|
'email': email.value,
|
||||||
|
// "roles": roles.value,
|
||||||
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {})
|
...(isCreate.value || resetPsw.value ? { password: pwd.value } : {})
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
getUsers();
|
getUsers();
|
||||||
@@ -284,6 +319,7 @@ const onSubmit = () => {
|
|||||||
'is_superuser': isSuperuser.value,
|
'is_superuser': isSuperuser.value,
|
||||||
'is_active': isActive.value,
|
'is_active': isActive.value,
|
||||||
'email': email.value,
|
'email': email.value,
|
||||||
|
// "roles": roles.value,
|
||||||
'password': pwd.value
|
'password': pwd.value
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
getUsers();
|
getUsers();
|
||||||
@@ -304,6 +340,7 @@ const onReset = () => {
|
|||||||
isPwd.value = true;
|
isPwd.value = true;
|
||||||
editId.value = 0;
|
editId.value = 0;
|
||||||
isCreate.value = true;
|
isCreate.value = true;
|
||||||
|
// roles.value = [];
|
||||||
resetPsw.value = false;
|
resetPsw.value = false;
|
||||||
addEditLoading.value = false;
|
addEditLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{ path: 'user', component: () => import('pages/UserManagement.vue')},
|
{ path: 'user', component: () => import('pages/UserManagement.vue')},
|
||||||
{ path: 'app', component: () => import('pages/AppManagement.vue')},
|
{ path: 'app', component: () => import('pages/AppManagement.vue')},
|
||||||
{ path: 'app/version/:id', component: () => import('pages/AppVersionManagement.vue')},
|
{ path: 'app/version/:id', component: () => import('pages/AppVersionManagement.vue')},
|
||||||
|
{ path: 'role', component: () => import('pages/RoleManagement.vue')},
|
||||||
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
|
{ path: 'condition', component: () => import('pages/conditionPage.vue') }
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { api } from 'boot/axios';
|
import { api } from 'boot/axios';
|
||||||
import { IAppDisplay, IAppVersion, IAppVersionDisplay, IManagedApp, IVersionSubmit } from 'src/types/AppTypes';
|
import { IAppDisplay, IAppVersion, IAppVersionDisplay, IManagedApp, IVersionSubmit } from 'src/types/AppTypes';
|
||||||
import { IUser } from 'src/types/UserTypes';
|
|
||||||
import { date, Notify } from 'quasar'
|
import { date, Notify } from 'quasar'
|
||||||
|
import { userToUserDisplay } from './useUserStore';
|
||||||
|
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', {
|
export const useAppStore = defineStore('app', {
|
||||||
@@ -104,16 +104,3 @@ function appToAppDisplay(app: IManagedApp) {
|
|||||||
function formatDate(data: string) {
|
function formatDate(data: string) {
|
||||||
return date.formatDate(data, 'YYYY/MM/DD HH:mm');
|
return date.formatDate(data, 'YYYY/MM/DD HH:mm');
|
||||||
}
|
}
|
||||||
|
|
||||||
function userToUserDisplay(user: IUser) {
|
|
||||||
return {
|
|
||||||
id: user.id,
|
|
||||||
firstName: user.first_name,
|
|
||||||
lastName: user.last_name,
|
|
||||||
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
|
|
||||||
fullName: user.last_name + ' ' + user.first_name,
|
|
||||||
email: user.email,
|
|
||||||
isActive: user.is_active,
|
|
||||||
isSuperuser: user.is_superuser,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import { router } from 'src/router';
|
|||||||
import { IDomainInfo } from '../types/DomainTypes';
|
import { IDomainInfo } from '../types/DomainTypes';
|
||||||
import { jwtDecode } from 'jwt-decode';
|
import { jwtDecode } from 'jwt-decode';
|
||||||
import { useAppStore } from './useAppStore';
|
import { useAppStore } from './useAppStore';
|
||||||
|
import { useUserStore } from './useUserStore';
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -92,6 +93,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
this.token = '';
|
this.token = '';
|
||||||
this.currentDomain = {} as IDomainInfo; // 清空当前域
|
this.currentDomain = {} as IDomainInfo; // 清空当前域
|
||||||
useAppStore().reset();
|
useAppStore().reset();
|
||||||
|
useUserStore().reset();
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
},
|
},
|
||||||
async setCurrentDomain(domain?: IDomainInfo) {
|
async setCurrentDomain(domain?: IDomainInfo) {
|
||||||
|
|||||||
112
frontend/src/stores/useUserStore.ts
Normal file
112
frontend/src/stores/useUserStore.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { api } from 'boot/axios';
|
||||||
|
import { IRoles, IRolesDisplay, IUser, IUserDisplay, IUserRolesDisplay } from 'src/types/UserTypes';
|
||||||
|
import { Notify } from 'quasar'
|
||||||
|
import { IResponse } from 'src/types/BaseTypes';
|
||||||
|
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
users: [] as IUserRolesDisplay[],
|
||||||
|
userIds: new Set<number>(),
|
||||||
|
roles: [] as IRolesDisplay[],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async loadUsers() {
|
||||||
|
this.reset('users');
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<IResponse<IUser[]>>('api/v1/users');
|
||||||
|
this.users = data.data.map((item) => {
|
||||||
|
this.userIds.add(item.id);
|
||||||
|
return userToUserRolesDisplay(item)
|
||||||
|
}).sort((a, b) => a.id - b.id); // set default order
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
icon: 'error',
|
||||||
|
color: 'negative',
|
||||||
|
message: 'ユーザー一覧の読み込みに失敗しました'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadRoles() {
|
||||||
|
this.reset('roles');
|
||||||
|
try {
|
||||||
|
const { data } = await api.get<IResponse<IRoles[]>>('api/v1/roles');
|
||||||
|
this.roles = data.data.map((item) => {
|
||||||
|
return roleToRoleDisplay(item)
|
||||||
|
}).sort((a, b) => a.id - b.id); // set default order
|
||||||
|
} catch (error) {
|
||||||
|
Notify.create({
|
||||||
|
icon: 'error',
|
||||||
|
color: 'negative',
|
||||||
|
message: 'ロール一覧の読み込みに失敗しました'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserById(id: number) {
|
||||||
|
if (!this.userIds.has(id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.users.find((item: IUserDisplay) => item.id === id);
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteUser(userId: number) {
|
||||||
|
try {
|
||||||
|
await api.delete(`api/v1/users/${userId}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
Notify.create({
|
||||||
|
icon: 'error',
|
||||||
|
color: 'negative',
|
||||||
|
message: 'ユーザーの削除に失敗しました'
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset(target?: 'users'|'roles') {
|
||||||
|
if (!target) {
|
||||||
|
this.reset('users');
|
||||||
|
this.reset('roles');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target == 'roles') {
|
||||||
|
this.roles = [];
|
||||||
|
} else if (target == 'users') {
|
||||||
|
this.users = [];
|
||||||
|
this.userIds.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function userToUserDisplay(user: IUser): IUserDisplay {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
firstName: user.first_name,
|
||||||
|
lastName: user.last_name,
|
||||||
|
fullNameSearch: (user.last_name + user.first_name).toLowerCase(),
|
||||||
|
fullName: user.last_name + ' ' + user.first_name,
|
||||||
|
email: user.email,
|
||||||
|
isActive: user.is_active,
|
||||||
|
isSuperuser: user.is_superuser,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userToUserRolesDisplay(user: IUser): IUserRolesDisplay {
|
||||||
|
const userRolesDisplay = userToUserDisplay(user) as IUserRolesDisplay;
|
||||||
|
userRolesDisplay.roles = user.roles.map((role) => roleToRoleDisplay(role)).sort((a, b) => a.level - b.level);
|
||||||
|
return userRolesDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function roleToRoleDisplay(roles: IRoles): IRolesDisplay {
|
||||||
|
return {
|
||||||
|
id: roles.id,
|
||||||
|
name: roles.description,
|
||||||
|
key: roles.name,
|
||||||
|
level: roles.level
|
||||||
|
};
|
||||||
|
}
|
||||||
12
frontend/src/types/BaseTypes.ts
Normal file
12
frontend/src/types/BaseTypes.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface IResponse<T = any> {
|
||||||
|
code: number;
|
||||||
|
data: T;
|
||||||
|
msg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IResponsePage<T = any> extends IResponse<T> {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ export interface IUser {
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
is_active: boolean,
|
is_active: boolean;
|
||||||
is_superuser: boolean,
|
is_superuser: boolean;
|
||||||
roles: object[]
|
roles: IRoles[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserDisplay {
|
export interface IUserDisplay {
|
||||||
@@ -15,10 +15,24 @@ export interface IUserDisplay {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
fullNameSearch: string;
|
fullNameSearch: string;
|
||||||
email: string;
|
email: string;
|
||||||
isActive: boolean,
|
isActive: boolean;
|
||||||
isSuperuser: boolean,
|
isSuperuser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserRolesDisplay extends IUserDisplay {
|
export interface IUserRolesDisplay extends IUserDisplay {
|
||||||
roles: object[]
|
roles: IRolesDisplay[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRoles {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
level: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRolesDisplay {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
level: number;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user