Files
KintoneAppBuilder/frontend/src/pages/TenantDomain.vue
2025-03-18 11:45:22 +08:00

406 lines
15 KiB
Vue

<template>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="domain" label="接続先管理" />
</q-breadcrumbs>
</div>
<q-table :rows="rows" :columns="columns" row-key="id" :filter="filter" :loading="loading" :pagination="pagination">
<template v-slot:top>
<q-btn v-permissions="Actions.domain.add" color="primary" :disable="loading" label="新規" @click="addRow" />
<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:header-cell-active="p">
<q-th auto-width :props="p">
<q-select class="filter-header" v-model="activeFilter" :options="activeOptions" @update:model-value="activeFilterUpdate" borderless
dense options-dense hide-bottom-space/>
</q-th>
</template>
<template v-slot:body-cell-active="p">
<q-td auto-width :props="p">
<q-badge v-if="!p.row.domainActive" color="grey">未使用</q-badge>
<q-badge v-if="p.row.id == currentDomainId" color="primary">既定</q-badge>
</q-td>
</template>
<template v-slot:body-cell-owner="p">
<q-td auto-width :props="p">
<q-badge v-if="isOwner(p.row)" color="purple">自分</q-badge>
<span v-else>{{ p.row.owner.fullName }}</span>
</q-td>
</template>
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<table-action-menu :row="p.row" :actions="actionList" />
</q-td>
</template>
</q-table>
<q-dialog :model-value="show" persistent>
<q-card style="min-width: 36em">
<q-form class="q-gutter-md" @submit="onSubmit" autocomplete="off">
<q-card-section>
<div class="text-h6 q-ma-sm">Kintone Account</div>
</q-card-section>
<q-card-section class="q-pt-none q-mt-none">
<div class="q-gutter-lg">
<q-input filled v-model="name" label="環境名 *" hint="kintoneの環境名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'kintoneの環境名を入力してください。']" />
<q-input filled type="url" v-model.trim="url" label="Kintone url" hint="KintoneのURLを入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'KintoneのURLを入力してください']" />
<q-input filled v-model="kintoneuser" label="ログイン名 *" hint="Kintoneのログイン名を入力してください" lazy-rules
:rules="[val => val && val.length > 0 || 'Kintoneのログイン名を入力してください']" autocomplete="new-password" />
<q-input v-if="isCreate" v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'"
hint="パスワード" label="パスワード" :disable="!isCreate" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>ドメインの有効化</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="domainActive" />
</q-item-section>
</q-item>
<div class="q-gutter-y-md" v-if="!isCreate">
<q-separator />
<q-item tag="label" class="q-pl-sm q-pr-none q-py-xs">
<q-item-section>
<q-item-label>パスワードリセット</q-item-label>
</q-item-section>
<q-item-section avatar>
<q-toggle v-model="resetPsw" @update:model-value="updateResetPsw" />
</q-item-section>
</q-item>
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="パスワードを入力してください"
label="パスワード" :disable="!resetPsw" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" autocomplete="new-password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer"
@click="isPwd = !isPwd" />
</template>
</q-input>
<!-- <q-btn label="asdf"/> -->
</div>
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary q-mb-md q-mx-sm">
<q-btn :loading="addEditLoading" label="保存" type="submit" color="primary" />
<q-btn label="キャンセル" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()" />
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
<q-dialog v-model="confirm" persistent>
<q-card>
<!-- -1 loading -->
<q-card-section v-if="deleteLoadingState == -1" class="row items-center">
<q-spinner color="primary" size="2em"/>
<span class="q-ml-sm">ドメイン利用権限を確認中</span>
</q-card-section>
<!-- > 0 can't delete -->
<q-card-section v-else-if="deleteLoadingState > 0" class="row items-center">
<q-icon name="error" color="negative" size="2em" />
<span class="q-ml-sm">ドメインは使用中です。削除してもよろしいですか?</span>
</q-card-section>
<!-- 0/-2 can delete -->
<q-card-section v-else class="row items-center">
<q-icon name="warning" color="warning" size="2em" />
<span class="q-ml-sm">削除してもよろしいですか?</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="キャンセル" color="primary" v-close-popup />
<!-- > 0 can't delete -->
<q-btn v-if="deleteLoadingState > 0" label="実行" color="primary" v-close-popup @click="openShareDg(SHARE_USE, editId)" />
<!-- 0/-2 can delete -->
<q-btn flat v-else label="OK" :disabled="deleteLoadingState == -1" :loading="deleteLoadingState == -2" color="primary" @click="deleteDomain()" />
</q-card-actions>
</q-card>
</q-dialog>
<share-usage-dialog v-model="shareDg" :domain="shareDomain" @close="shareDg = false" />
<share-manage-dialog v-model="shareManageDg" :domain="shareDomain" @close="shareManageDg = false" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { api } from 'boot/axios';
import { Actions } from 'boot/permissions';
import { useAuthStore } from 'stores/useAuthStore';
import ShareUsageDialog from 'components/ShareDomain/ShareUsageDialog.vue';
import ShareManageDialog from 'components/ShareDomain/ShareManageDialog.vue';
import TableActionMenu from 'components/TableActionMenu.vue';
import { IDomain, IDomainDisplay, IDomainOwnerDisplay, IDomainSubmit } from '../types/DomainTypes';
const authStore = useAuthStore();
const inactiveRowClass = (row: IDomainOwnerDisplay) => row.domainActive ? '' : 'inactive-row';
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true, classes: inactiveRowClass },
// {
// name: 'tenantid',
// required: true,
// label: 'テナントID',
// field: 'tenantid',
// align: 'left',
// sortable: true,
// classes: inactiveRowClass
// },
{ name: 'name', label: '環境名', field: 'name', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'active', label: '', align: 'left', field: 'domainActive', classes: inactiveRowClass },
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true, classes: inactiveRowClass },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', classes: inactiveRowClass },
{ name: 'owner', label: '所有者', field: '', align: 'left', classes: inactiveRowClass },
{ name: 'actions', label: '', field: 'actions', classes: inactiveRowClass }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
const loading = ref(false);
const addEditLoading = ref(false);
const deleteLoadingState = ref<number>(-1); // -2: deleteLoading, -1: loading, 0: allow, > 0: user count
const filter = ref('');
const rows = ref<IDomainOwnerDisplay[]>([]);
const show = ref(false);
const confirm = ref(false);
const resetPsw = ref(false);
const currentDomainId = computed(() => authStore.currentDomain.id);
// const tenantid = ref(authStore.currentDomain.id);
const name = ref('');
const url = ref('');
const isPwd = ref(true);
const kintoneuser = ref('');
const kintonepwd = ref('');
const kintonepwdBK = ref('');
const domainActive = ref(true);
const isCreate = ref(true);
let editId = ref(0);
const shareDg = ref(false);
const shareManageDg = ref(false);
const shareDomain = ref<IDomainOwnerDisplay>({} as IDomainOwnerDisplay);
const activeOptions = [
{ value: 0, label: 'すべて' },
{ value: 1, label: '使用' },
{ value: 2, label: '未使用'}
]
const activeFilter = ref(activeOptions[0]);
const activeFilterUpdate = (option: {value: number}) => {
switch (option.value) {
case 1:
getDomain((row) => row.domainActive)
break;
case 2:
getDomain((row) => !row.domainActive)
break;
default:
getDomain()
break;
}
}
const SHARE_USE = 'use';
const SHARE_MANAGE = 'manage';
const actionList = [
{ label: '編集', icon: 'edit_note', permission: Actions.domain.edit, action: editRow },
{ label: '利用権限設定', icon: 'person_add_alt', permission: Actions.domain.grantUse,
action: (row: IDomainOwnerDisplay) => {openShareDg(SHARE_USE, row)} },
{ label: '管理権限設定', icon: 'add_moderator', permission: Actions.domain.grantManage,
disable: (row: IDomainOwnerDisplay) => !isOwner(row),
tooltip: (row: IDomainOwnerDisplay) => isOwner(row) ? '' : 'ドメイン所有者でないため、操作できません',
action: (row: IDomainOwnerDisplay) => {openShareDg(SHARE_MANAGE, row)}
},
{ separator: true },
{ label: '削除', icon: 'delete_outline', permission: Actions.domain.delete, class: 'text-red', action: removeRow },
];
const isOwner = (row: IDomainOwnerDisplay) => row.owner.id === Number(authStore.userId);
const getDomain = async (filter?: (row: IDomainOwnerDisplay) => boolean) => {
loading.value = true;
const { data } = await api.get<{data:IDomain[]}>('api/domains');
rows.value = data.data.map((item) => {
return {
id: item.id,
tenantid: item.tenantid,
domainActive: item.is_active,
name: item.name,
url: item.url,
user: item.kintoneuser,
password: item.kintonepwd,
owner: {
id: item.owner.id,
firstName: item.owner.first_name,
lastName: item.owner.last_name,
fullNameSearch: (item.owner.last_name + item.owner.first_name).toLowerCase(),
fullName: item.owner.last_name + ' ' + item.owner.first_name,
email: item.owner.email,
isActive: item.owner.is_active,
isSuperuser: item.owner.is_superuser,
}
}
}).filter(filter || (() => true));
loading.value = false;
}
onMounted(async () => {
await getDomain();
})
// emulate fetching data from server
const addRow = () => {
// editId.value
onReset();
show.value = true;
}
async function removeRow(row: IDomainOwnerDisplay) {
confirm.value = true;
deleteLoadingState.value = -1;
editId.value = row.id;
const { data } = await api.get(`/api/domainshareduser/${row.id}`);
deleteLoadingState.value = data.data.length;
}
const deleteDomain = () => {
deleteLoadingState.value = -2;
api.delete(`api/domain/${editId.value}`).then(({ data }) => {
if (!data.data) {
// TODO dialog
}
confirm.value = false;
deleteLoadingState.value = -1;
getDomain();
// authStore.setCurrentDomain();
})
editId.value = 0; // set in removeRow()
};
function editRow(row: any) {
isCreate.value = false
editId.value = row.id;
// tenantid.value = row.tenantid;
name.value = row.name;
url.value = row.url;
kintoneuser.value = row.user;
kintonepwd.value = row.password;
domainActive.value = row.domainActive;
isPwd.value = true;
show.value = true;
};
const updateResetPsw = (value: boolean) => {
if (value === true) {
kintonepwd.value = ''
isPwd.value = true
} else {
kintonepwd.value = kintonepwdBK.value
}
}
const closeDg = () => {
show.value = false;
onReset();
}
const onSubmit = () => {
addEditLoading.value = true;
const method = editId.value !== 0 ? 'put' : 'post';
const param: IDomainSubmit = {
'id': editId.value,
'tenantid': '1', // TODO: テナントIDを取得する
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': ((isCreate.value && editId.value == 0) || resetPsw.value) ? kintonepwd.value : '',
'is_active': domainActive.value,
'ownerid': authStore.userId || ''
}
// for search: api.put(`api/domain`)、api.post(`api/domain`)
api[method].apply(api, ['api/domain', param]).then(async (resp: any) => {
const res = resp.data;
if (res.data.id === currentDomainId.value && !res.data.is_active) {
await authStore.setCurrentDomain();
}
getDomain();
closeDg();
onReset();
addEditLoading.value = false;
})
}
function openShareDg(type: typeof SHARE_MANAGE|typeof SHARE_USE, row: IDomainOwnerDisplay|number) {
if (typeof row === 'number') {
row = rows.value.find(item => item.id === row) as IDomainOwnerDisplay;
}
shareDomain.value = row;
if (type === SHARE_USE) {
shareDg.value = true;
} else if (type === SHARE_MANAGE) {
shareManageDg.value = true;
}
};
const onReset = () => {
name.value = '';
url.value = '';
kintoneuser.value = '';
kintonepwd.value = '';
isPwd.value = true;
editId.value = 0;
isCreate.value = true;
domainActive.value = true;
resetPsw.value = false
addEditLoading.value = false;
}
</script>
<style lang="scss">
.filter-header .q-field__native {
font-size: 12px;
font-weight: 500;
}
.filter-header .q-icon {
width: 12px;
}
.q-table td.inactive-row {
color: #aaa;
background-color: #fafafa;
}
.q-table tr > td.inactive-row:last-child {
color: inherit;
}
</style>