Add shared dialog

This commit is contained in:
xue jiahao
2024-12-02 16:21:23 +08:00
parent 8a3aaec8d5
commit 660ffe36c2
7 changed files with 532 additions and 35 deletions

View File

@@ -0,0 +1,186 @@
<template>
<q-dialog :auto-close="false" :model-value="visible" persistent bordered>
<q-card style="min-width: 40vw; max-width: 80vw; max-height: 95vh;" >
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{domain.name}}のドメインを共有する</q-toolbar-title>
<q-space></q-space>
<slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="close" />
</q-toolbar>
<q-card-section>
{{ props.domain.url }}
<!-- <q-input value="props.domain.url" label="共有リンク" readonly outlined dense>
</q-input> -->
</q-card-section>
<q-card-section>
<!-- <q-select
filled
v-model="filterInput"
use-input
use-chips
multiple
input-debounce="0"
:options="filterUsers"
@filter="filterFn"
/> -->
<q-table :rows="canSharedUsers" :filter="filterUnshared" :columns="columns" row-key="id" :loading="loading" :pagination="paginationUnshared">
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="add" @click="shareTo(p.row)" />
</q-btn-group>
</q-td>
</template>
<template v-slot:top-right>
<q-input borderless dense filled debounce="300" v-model="filterUnshared" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
</q-table>
<!-- <q-btn flat label="共有する" @click="shareTo" /> -->
<q-table :rows="sharedUsers" :filter="filterShared" :columns="columns" row-key="id" :loading="loading" :pagination="paginationShared" >
<template v-slot:body-cell-actions="p">
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="person_off" @click="removeShareTo(p.row)" />
</q-btn-group>
</q-td>
</template>
<template v-slot:top-right>
<q-input borderless dense filled debounce="300" v-model="filterShared" placeholder="検索">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
</q-table>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" @click="close" />
<q-btn flat label="キャンセル" @click="close" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { IDomainDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios';
interface Props {
modelValue: boolean;
domain: IDomainDisplay;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
];
const paginationUnshared = ref({ sortBy: 'id', descending: true, rowsPerPage: 10 });
const filterUnshared = ref('');
const paginationShared = ref({ sortBy: 'id', descending: true, rowsPerPage: 10 });
const filterShared = ref('');
const loading = ref(true);
const visible = ref(props.modelValue);
const allUsers = ref<IUserDisplay[]>([]);
const sharedUsers = ref<IUserDisplay[]>([]);
const sharedUsersIdSet = new Set<number>();
const canSharedUsers = ref<IUserDisplay[]>([]);
watch(
() => props.modelValue,
async (newValue) => {
visible.value = newValue;
if (newValue) {
await loadShared();
}
}
);
watch(
() => visible.value,
(newValue) => {
emit('update:modelValue', newValue);
}
);
const close = () => {
emit('close');
};
const shareTo = async (user: IUserDisplay) => {
loading.value = true;
await api.post(`api/domain/${user.id}?domainid=${props.domain.id}`)
await loadShared();
loading.value = false;
}
const removeShareTo = async (user: IUserDisplay) => {
loading.value = true;
await api.delete(`api/domain/${props.domain.id}/${user.id}`)
await loadShared();
loading.value = false;
};
const loadShared = async () => {
loading.value = true;
sharedUsersIdSet.clear();
const { data } = await api.get(`/api/domainshareduser/${props.domain.id}`);
sharedUsers.value = data.data.map((item: IUser) => {
const val = itemToDisplay(item);
sharedUsersIdSet.add(val.id);
return val;
});
canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id));
loading.value = false;
}
onMounted(async () => {
await getUsers();
})
const getUsers = async () => {
if (Object.keys(allUsers.value).length > 0) {
return;
}
loading.value = true;
const result = await api.get(`api/v1/users`);
allUsers.value = result.data.map(itemToDisplay);
loading.value = false;
}
const itemToDisplay = (item: IUser) => {
return {
id: item.id,
firstName: item.first_name,
lastName: item.last_name,
fullNameSearch: (item.last_name + item.first_name).toLowerCase(),
fullName: item.last_name + ' ' + item.first_name,
email: item.email,
isSuperuser: item.is_superuser,
roles: item.roles,
isActive: item.is_active,
} as IUserDisplay
}
</script>
<style lang="scss"></style>

View File

@@ -0,0 +1,202 @@
<template>
<q-dialog :auto-close="false" :model-value="visible" persistent bordered>
<q-card class="dialog-content" >
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{domain.name}}のドメインを共有する</q-toolbar-title>
<q-space></q-space>
<slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="close" />
</q-toolbar>
<q-card-section class="q-ma-md" >
{{ props.domain.url }}
<!-- <q-input value="props.domain.url" label="共有リンク" readonly outlined dense>
</q-input> -->
<q-select
:disable="loading"
filled
dense
v-model="canSharedUserFilter"
use-input
input-debounce="0"
:options="canSharedUserFilteredOptions"
clearable
@filter="filterFn"
:display-value="canSharedUserFilter?`${canSharedUserFilter.fullName} ${canSharedUserFilter.email}`:''">
<template v-slot:after>
<q-btn :disable="!canSharedUserFilter" :loading="addLoading" label="共有する" color="primary" @click="shareTo(canSharedUserFilter as IUserDisplay)" />
</template>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section avatar>{{scope.opt.id}}</q-item-section>
<q-item-section>{{scope.opt.fullName}}</q-item-section>
<q-item-section>{{scope.opt.email}}</q-item-section>
</q-item>
</template>
</q-select>
<!-- <sharing-user-list :users="canSharedUsers" :loading="loading" title="Treats" >
<template v-slot:actions="{ row }">
<q-btn flat color="primary" padding="xs" size="1em" icon="add" @click="shareTo(row)" />
</template>
</sharing-user-list> -->
<!-- <q-btn label="共有する" color="primary" @click="shareTo" /> -->
<sharing-user-list class="q-mt-md" style="height: 330px" :users="sharedUsers" :loading="loading" title="現在の共有ユーザー">
<template v-slot:actions="{ row }">
<q-btn flat color="primary" padding="xs" size="1em" :loading="row.id == removingUser?.id" icon="person_off" @click="removeShareTo(row)" />
</template>
</sharing-user-list>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="確定" @click="close" />
<q-btn flat label="キャンセル" @click="close" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { IDomainDisplay } from '../../types/DomainTypes';
import { IUser, IUserDisplay } from '../../types/UserTypes';
import { api } from 'boot/axios';
import SharingUserList from 'components/ShareDomain/SharingUserList.vue';
interface Props {
modelValue: boolean;
domain: IDomainDisplay;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'close'): void;
}>();
const addLoading = ref(false);
const removingUser = ref<IUserDisplay>();
const loading = ref(true);
const visible = ref(props.modelValue);
const allUsers = ref<IUserDisplay[]>([]);
const sharedUsers = ref<IUserDisplay[]>([]);
const sharedUsersIdSet = new Set<number>();
const canSharedUsers = ref<IUserDisplay[]>([]);
const canSharedUserFilter = ref<IUserDisplay>();
const canSharedUserFilteredOptions = ref<IUserDisplay[]>([]);
const filterFn = (val:string, update: (cb: () => void) => void) => {
update(() => {
if (val === '') {
canSharedUserFilteredOptions.value = canSharedUsers.value;
return;
}
const needle = val.toLowerCase();
canSharedUserFilteredOptions.value = canSharedUsers.value.filter(v =>
v.email.toLowerCase().indexOf(needle) > -1 || v.fullNameSearch.toLowerCase().indexOf(needle) > -1);
})
}
watch(
() => props.modelValue,
async (newValue) => {
visible.value = newValue;
sharedUsers.value = [];
if (newValue) {
await loadShared();
}
}
);
watch(
() => visible.value,
(newValue) => {
emit('update:modelValue', newValue);
}
);
const close = () => {
emit('close');
};
const shareTo = async (user: IUserDisplay) => {
addLoading.value = true;
loading.value = true;
await api.post(`api/domain/${user.id}?domainid=${props.domain.id}`)
await loadShared();
canSharedUserFilter.value = undefined;
loading.value = false;
addLoading.value = false;
}
const removeShareTo = async (user: IUserDisplay) => {
removingUser.value = user;
loading.value = true;
await api.delete(`api/domain/${props.domain.id}/${user.id}`)
await loadShared();
loading.value = false;
removingUser.value = undefined;
};
const loadShared = async () => {
loading.value = true;
sharedUsersIdSet.clear();
const { data } = await api.get(`/api/domainshareduser/${props.domain.id}`);
sharedUsers.value = data.data.map((item: IUser) => {
const val = itemToDisplay(item);
sharedUsersIdSet.add(val.id);
return val;
});
canSharedUsers.value = allUsers.value.filter((item) => !sharedUsersIdSet.has(item.id));
canSharedUserFilteredOptions.value = canSharedUsers.value;
loading.value = false;
}
onMounted(async () => {
await getUsers();
})
const getUsers = async () => {
if (Object.keys(allUsers.value).length > 0) {
return;
}
loading.value = true;
const result = await api.get(`api/v1/users`);
allUsers.value = result.data.map(itemToDisplay);
loading.value = false;
}
const itemToDisplay = (item: IUser) => {
return {
id: item.id,
firstName: item.first_name,
lastName: item.last_name,
fullNameSearch: (item.last_name + item.first_name).toLowerCase(),
fullName: item.last_name + ' ' + item.first_name,
email: item.email,
isSuperuser: item.is_superuser,
roles: item.roles,
isActive: item.is_active,
} as IUserDisplay
}
</script>
<style lang="scss">
.dialog-content {
width: 60vw;
max-height: 80vh;
.q-select {
min-width: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<q-table :rows="users" :filter="filter" :columns="columns" row-key="id" :loading="loading" :pagination="pagination">
<template v-slot:top-right>
<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-actions="props">
<q-td :props="props">
<slot name="actions" :row="props.row"></slot>
</q-td>
</template>
</q-table>
</template>
<script setup lang="ts">
import { ref, PropType } from 'vue';
import { IUserDisplay } from '../../types/UserTypes';
const props = defineProps({
users: {
type: Array as PropType<IUserDisplay[]>,
required: true,
},
loading: {
type: Boolean,
default: false,
},
});
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'fullName', label: '名前', field: 'fullName', align: 'left', sortable: true },
{ name: 'email', label: '電子メール', field: 'email', align: 'left', sortable: true },
{ name: 'actions', label: '', field: 'actions', sortable: false },
];
const filter = ref('');
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 10 });
</script>

View File

@@ -0,0 +1,56 @@
<template>
<div class="q-pa-sm">
<q-card class='domain-card'>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-h6">{{ item.name }} <q-icon v-if="isNotOwnerFunc(item.id)" name="account_circle" color="teal" class="q-mb-xs"></q-icon> </div>
<div class="text-subtitle2">{{ item.url }}</div>
</div>
<!-- <div v-if="isNotOwnerFunc(item.id)" class="col-auto">
<q-icon name="account_circle" color="teal" size="2em"></q-icon>
</div> -->
</div>
</q-card-section>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-grey-7 text-caption text-weight-medium">
アカウント
</div>
<div class="smaller-font-size">{{ item.kintoneuser }}</div>
</div>
<div v-if="isNotOwnerFunc(item.id)" class="col-auto">
<div class="text-grey-7 text-caption text-weight-medium">
所有者
</div>
<div class="smaller-font-size">{{ item.owner }}</div>
</div>
</div>
</q-card-section>
<q-separator />
<slot name="actions" :item="item"></slot>
</q-card>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import { IDomain } from 'src/types/DomainTypes';
const props = defineProps<{
item: IDomain;
isNotOwnerFunc: (ownerId: any) => boolean;
}>();
</script>
<style lang="scss" scoped>
.domain-card {
width: 22rem;
word-break: break-word;
.smaller-font-size {
font-size: 13px;
}
}
</style>

View File

@@ -19,7 +19,10 @@
<template v-slot:body-cell-name="p">
<q-td class="flex justify-between items-center" :props="p">
{{ p.row.name }}
<div>
{{ p.row.name }}
<!-- <q-icon class="q-ml-xs" title="共有中です" name="people" :color="p.row.domainActive ? 'grey-8' : 'grey-4'" /> -->
</div>
<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>
@@ -29,6 +32,7 @@
<q-td :props="p">
<q-btn-group flat>
<q-btn flat color="primary" padding="xs" size="1em" icon="edit_note" @click="editRow(p.row)" />
<q-btn flat color="primary" :disable="!p.row.domainActive" padding="xs" size="1em" icon="person_add_alt" @click="openShareDg(p.row)" />
<q-btn flat color="negative" padding="xs" size="1em" icon="delete_outline" @click="removeRow(p.row)" />
</q-btn-group>
</q-td>
@@ -121,6 +125,8 @@
</q-card>
</q-dialog>
<share-domain-dialog v-model="shareDg" :domain="shareDomain" @close="closeShareDg()" />
</div>
</template>
@@ -129,6 +135,7 @@ import { ref, onMounted, computed } from 'vue';
import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import { useDomainStore } from 'stores/useDomainStore';
import ShareDomainDialog from 'components/ShareDomain/ShareDomainDialog.vue';
import { IDomain, IDomainDisplay, IDomainSubmit } from '../types/DomainTypes';
const authStore = useAuthStore();
@@ -150,7 +157,7 @@ const columns = [
{ name: 'url', label: 'URL', field: 'url', align: 'left', sortable: true, classes: inactiveRowClass },
// { name: 'owner', label: '所有者', field: 'owner', align: 'left', classes: inactiveRowClass },
{ name: 'user', label: 'ログイン名', field: 'user', align: 'left', classes: inactiveRowClass },
{ name: 'actions', label: '操作', field: 'actions' }
{ name: 'actions', label: '操作', field: 'actions', classes: inactiveRowClass }
];
const pagination = ref({ sortBy: 'id', descending: true, rowsPerPage: 20 });
@@ -174,6 +181,8 @@ const domainActive = ref(true);
const isCreate = ref(true);
let editId = ref(0);
let ownerid = ref('');
const shareDg = ref(false);
const shareDomain = ref<IDomainDisplay|object>({});
const getDomain = async () => {
loading.value = true;
@@ -209,7 +218,10 @@ const removeRow = (row: IDomainDisplay) => {
}
const deleteDomain = () => {
api.delete(`api/domain/${editId.value}`).then(() => {
api.delete(`api/domain/${editId.value}`).then(({ data }) => {
if (!data.data) {
// TODO dialog
}
getDomain();
// authStore.setCurrentDomain();
})
@@ -266,6 +278,15 @@ const onSubmit = () => {
})
}
const openShareDg = (row: IDomainDisplay) => {
shareDomain.value = row;
shareDg.value = true;
};
const closeShareDg = () => {
shareDg.value = false;
}
const onReset = () => {
name.value = '';
url.value = '';
@@ -283,5 +304,6 @@ const onReset = () => {
<style lang="scss">
.q-table td.inactive-row {
color: #aaa;
background-color: #fafafa;
}
</style>

View File

@@ -28,32 +28,8 @@
</template>
<template v-slot:item="props">
<div class="q-pa-sm">
<q-card :class="['domain-card', isNotOwner(props.row.id) ? 'bg-grey-2': '']">
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-h6">{{ props.row.name }}</div>
<div class="text-subtitle2">{{ props.row.url }}</div>
</div>
<div v-if="isNotOwner(props.row.id)" class="col-auto">
<q-icon name="account_circle" color="teal" size="2em" ></q-icon>
</div>
</div>
</q-card-section>
<q-card-section>
<div class="row items-center no-wrap">
<div class="col">
<div class="text-grey-7 text-caption text-weight-medium">アカウント</div>
<div class="smaller-font-size">{{ props.row.kintoneuser }}</div>
</div>
<div v-if="isNotOwner(props.row.id)" class="col-auto">
<div class="text-grey-7 text-caption text-weight-medium">所有者</div>
<div class="smaller-font-size">{{ props.row.owner }}</div>
</div>
</div>
</q-card-section>
<q-separator />
<domain-card :item="props.row" :isNotOwnerFunc="isNotOwner">
<template v-slot:actions>
<q-card-actions align="right">
<q-chip v-if="isActive(props.row.id)" outline color="teal" text-color="white" icon="done">
既定
@@ -61,8 +37,8 @@
<q-btn flat v-else :loading="activeDomainLoadingId === props.row.id" :disable="deleteDomainLoadingId === props.row.id" @click="activeDomain(props.row)">既定にする</q-btn>
<q-btn flat :disable="isNotOwner(props.row.id) || activeDomainLoadingId === props.row.id" :text-color="isNotOwner(props.row.id)?'grey':''" :loading="deleteDomainLoadingId === props.row.id" @click="clickDeleteConfirm(props.row)">削除</q-btn>
</q-card-actions>
</q-card>
</div>
</template>
</domain-card>
</template>
</q-table>
@@ -94,6 +70,7 @@ import { api } from 'boot/axios';
import { useAuthStore } from 'stores/useAuthStore';
import ShowDialog from 'components/ShowDialog.vue';
import DomainCard from 'components/UserDomain/DomainCard.vue';
import DomainSelect from 'components/DomainSelect.vue';
const authStore = useAuthStore();
@@ -129,9 +106,9 @@ const clickAddDomain = () => {
const addUserDomainFinished = async (val: string) => {
showAddDomainDg.value = true;
addUserDomainLoading.value = true;
const selected = addDomainRef.value.selected;
if (val == 'OK' && selected.length > 0) {
addUserDomainLoading.value = true;
const { data } = await api.post(`api/domain/${authStore.userId}?domainid=${selected[0].id}`)
if (rows.value.length === 0 && data.data) {
const domain = data.data;
@@ -142,9 +119,9 @@ const addUserDomainFinished = async (val: string) => {
});
}
await getDomain();
addUserDomainLoading.value = false;
showAddDomainDg.value = false;
}
addUserDomainLoading.value = false;
showAddDomainDg.value = false;
};
const showDeleteConfirm = ref(false);

View File

@@ -1,8 +1,21 @@
export interface IUser {
id: number;
first_name: string;
last_name: string;
email: string;
is_active: boolean,
is_superuser: boolean,
roles: string[]
roles: object[]
}
export interface IUserDisplay {
id: number;
firstName: string;
lastName: string;
fullName: string;
fullNameSearch: string;
email: string;
isActive: boolean,
isSuperuser: boolean,
roles: object[]
}