mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 14:23:51 +02:00
rewrite tables
This commit is contained in:
@@ -1,206 +0,0 @@
|
||||
<template>
|
||||
<div class="data-table-wrapper">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="paginatedData"
|
||||
v-bind="mergedTableProps"
|
||||
:stripe="false"
|
||||
:default-sort="resolvedDefaultSort"
|
||||
@row-click="handleRowClick"
|
||||
@sort-change="handleSortChange">
|
||||
<slot></slot>
|
||||
</el-table>
|
||||
|
||||
<div v-if="showPagination" class="pagination-wrapper">
|
||||
<el-pagination
|
||||
size="small"
|
||||
:current-page="internalCurrentPage"
|
||||
:page-size="effectivePageSize"
|
||||
:total="totalItems"
|
||||
v-bind="mergedPaginationProps"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, toRaw, toRefs, watch } from 'vue';
|
||||
|
||||
import { useAppearanceSettingsStore, useVrcxStore } from '../stores';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tableProps: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
paginationProps: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
pageSizeLinked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
filters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'table, pagination'
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['row-click', 'sort-change']);
|
||||
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const vrcxStore = useVrcxStore();
|
||||
|
||||
const { data, pageSize, tableProps, paginationProps, filters } = toRefs(props);
|
||||
|
||||
const internalCurrentPage = ref(1);
|
||||
const internalPageSize = ref(pageSize.value);
|
||||
|
||||
const asRawArray = (value) => (Array.isArray(value) ? toRaw(value) : []);
|
||||
const isEmptyFilterValue = (value) => (Array.isArray(value) ? value.length === 0 : !value);
|
||||
|
||||
const showPagination = computed(() => {
|
||||
return props.layout.includes('pagination');
|
||||
});
|
||||
|
||||
const effectivePageSize = computed(() => {
|
||||
return props.pageSizeLinked ? appearanceSettingsStore.tablePageSize : internalPageSize.value;
|
||||
});
|
||||
|
||||
const resolvedDefaultSort = computed(() => {
|
||||
if (props.tableProps?.defaultSort === null) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
props.tableProps?.defaultSort ?? {
|
||||
prop: 'created_at',
|
||||
order: 'descending'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const mergedTableProps = computed(() => {
|
||||
const src = tableProps.value || {};
|
||||
const rest = { ...src };
|
||||
if ('defaultSort' in rest) {
|
||||
delete rest.defaultSort;
|
||||
}
|
||||
return {
|
||||
...rest
|
||||
};
|
||||
});
|
||||
|
||||
const mergedPaginationProps = computed(() => ({
|
||||
layout: 'sizes, prev, pager, next, total',
|
||||
...paginationProps.value,
|
||||
pageSizes: paginationProps.value?.pageSizes ?? appearanceSettingsStore.tablePageSizes
|
||||
}));
|
||||
|
||||
const applyFilter = function (row, filter) {
|
||||
if (Array.isArray(filter.prop)) {
|
||||
return filter.prop.some((propItem) => applyFilter(row, { prop: propItem, value: filter.value }));
|
||||
}
|
||||
|
||||
const cellValue = row[filter.prop];
|
||||
if (cellValue === undefined || cellValue === null) return false;
|
||||
|
||||
if (Array.isArray(filter.value)) {
|
||||
return filter.value.some((val) => String(cellValue).toLowerCase() === String(val).toLowerCase());
|
||||
} else {
|
||||
return String(cellValue).toLowerCase().includes(String(filter.value).toLowerCase());
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = computed(() => {
|
||||
const rawData = asRawArray(data.value);
|
||||
const rawFilters = Array.isArray(filters.value) ? filters.value : [];
|
||||
const activeFilters = rawFilters.filter((filter) => !isEmptyFilterValue(filter?.value));
|
||||
|
||||
if (activeFilters.length === 0) {
|
||||
return rawData;
|
||||
}
|
||||
|
||||
return rawData.filter((row) => {
|
||||
for (const filter of activeFilters) {
|
||||
if (filter.filterFn) {
|
||||
if (!filter.filterFn(row, filter)) return false;
|
||||
continue;
|
||||
}
|
||||
if (!applyFilter(row, filter)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const paginatedData = computed(() => {
|
||||
if (!showPagination.value) {
|
||||
return filteredData.value;
|
||||
}
|
||||
|
||||
const start = (internalCurrentPage.value - 1) * effectivePageSize.value;
|
||||
const end = start + effectivePageSize.value;
|
||||
return filteredData.value.slice(start, end);
|
||||
});
|
||||
|
||||
const totalItems = computed(() => {
|
||||
const length = filteredData.value.length;
|
||||
const max = vrcxStore.maxTableSize;
|
||||
return length > max && length < max + 51 ? max : length;
|
||||
});
|
||||
|
||||
const handleRowClick = (row, column, event) => {
|
||||
emit('row-click', row, column, event);
|
||||
};
|
||||
|
||||
const handleSortChange = (data) => {
|
||||
emit('sort-change', data);
|
||||
};
|
||||
|
||||
const handleSizeChange = (size) => {
|
||||
if (props.pageSizeLinked) {
|
||||
appearanceSettingsStore.setTablePageSize(size);
|
||||
return;
|
||||
}
|
||||
internalPageSize.value = size;
|
||||
};
|
||||
|
||||
const handleCurrentChange = (page) => {
|
||||
internalCurrentPage.value = page;
|
||||
};
|
||||
|
||||
watch(pageSize, (newVal) => {
|
||||
internalPageSize.value = newVal;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-table-wrapper {
|
||||
font-feature-settings:
|
||||
'tnum' 1,
|
||||
'lnum' 1;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
</style>
|
||||
@@ -117,94 +117,13 @@
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupMembers">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
<DataTable
|
||||
<DataTableLayout
|
||||
v-if="groupMemberModerationTable.data.length"
|
||||
v-bind="groupMemberModerationTable"
|
||||
style="margin-top: 10px">
|
||||
<el-table-column width="55" prop="$selected">
|
||||
<template #default="scope">
|
||||
<Checkbox
|
||||
v-model="scope.row.$selected"
|
||||
@update:modelValue="groupMemberModerationTableSelectionChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.avatar')"
|
||||
width="70"
|
||||
prop="photo">
|
||||
<template #default="scope">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
:src="userImage(scope.row.user)"
|
||||
class="friends-list-avatar"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(scope.row.user)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.display_name')"
|
||||
width="160"
|
||||
prop="$displayName"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span style="cursor: pointer" @click="showUserDialog(scope.row.userId)">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.user.$userColour }"
|
||||
v-text="scope.row.user.displayName"></span>
|
||||
<span v-else v-text="scope.row.user?.displayName"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
|
||||
<template #default="scope">
|
||||
<template v-for="(roleId, index) in scope.row.roleIds" :key="roleId">
|
||||
<template
|
||||
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
|
||||
:key="roleId + rIndex">
|
||||
<span v-if="role?.id === roleId"
|
||||
>{{ role.name
|
||||
}}<span v-if="index < scope.row.roleIds.length - 1">, </span></span
|
||||
></template
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.notes')"
|
||||
prop="managerNotes"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span @click.stop v-text="scope.row.managerNotes"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.joined_at')"
|
||||
width="170"
|
||||
prop="joinedAt"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDateFilter(scope.row.joinedAt, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.visibility')"
|
||||
width="120"
|
||||
prop="visibility"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span v-text="scope.row.visibility"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
style="margin-top: 10px"
|
||||
:table="groupMemberModerationTanstackTable"
|
||||
:loading="isGroupMembersLoading"
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupMemberModerationTotalItems" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -235,90 +154,12 @@
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupBans">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
<DataTable v-bind="groupBansModerationTable" style="margin-top: 10px">
|
||||
<el-table-column width="55" prop="$selected">
|
||||
<template #default="scope">
|
||||
<Checkbox
|
||||
v-model="scope.row.$selected"
|
||||
@update:modelValue="groupMemberModerationTableSelectionChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.avatar')"
|
||||
width="70"
|
||||
prop="photo">
|
||||
<template #default="scope">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
:src="userImage(scope.row.user)"
|
||||
class="friends-list-avatar"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(scope.row.user)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.display_name')"
|
||||
width="160"
|
||||
prop="$displayName"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span style="cursor: pointer" @click="showUserDialog(scope.row.userId)">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.user?.$userColour }"
|
||||
v-text="scope.row.user?.displayName"></span>
|
||||
<span v-else v-text="scope.row.user?.displayName"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('dialog.group_member_moderation.roles')" prop="roleIds" sortable>
|
||||
<template #default="scope">
|
||||
<template v-for="(roleId, index) in scope.row.roleIds" :key="roleId">
|
||||
<span
|
||||
v-for="(role, rIndex) in groupMemberModeration.groupRef.roles"
|
||||
v-if="role.id === roleId"
|
||||
:key="rIndex + roleId"
|
||||
>{{ role.name }}</span
|
||||
>
|
||||
<span v-if="index < scope.row.roleIds.length - 1">, </span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.notes')"
|
||||
prop="managerNotes"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span @click.stop v-text="scope.row.managerNotes"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.joined_at')"
|
||||
width="170"
|
||||
prop="joinedAt"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDateFilter(scope.row.joinedAt, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.banned_at')"
|
||||
width="170"
|
||||
prop="bannedAt"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDateFilter(scope.row.bannedAt, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="groupBansModerationTanstackTable"
|
||||
:loading="isGroupMembersLoading"
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupBansModerationTotalItems" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
@@ -349,61 +190,12 @@
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupInvites">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
<DataTable v-bind="groupInvitesModerationTable" style="margin-top: 10px">
|
||||
<el-table-column width="55" prop="$selected">
|
||||
<template #default="scope">
|
||||
<Checkbox
|
||||
v-model="scope.row.$selected"
|
||||
@update:modelValue="
|
||||
groupMemberModerationTableSelectionChange(scope.row)
|
||||
" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.avatar')"
|
||||
width="70"
|
||||
prop="photo">
|
||||
<template #default="scope">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
:src="userImage(scope.row.user)"
|
||||
class="friends-list-avatar"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(scope.row.user)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.display_name')"
|
||||
width="160"
|
||||
prop="$displayName"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span style="cursor: pointer" @click="showUserDialog(scope.row.userId)">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.user.$userColour }"
|
||||
v-text="scope.row.user.displayName"></span>
|
||||
<span v-else v-text="scope.row.user?.displayName"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.notes')"
|
||||
prop="managerNotes"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span @click.stop v-text="scope.row.managerNotes"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="groupInvitesModerationTanstackTable"
|
||||
:loading="isGroupMembersLoading"
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupInvitesModerationTotalItems" />
|
||||
<br />
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -430,61 +222,12 @@
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupJoinRequests">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
<DataTable v-bind="groupJoinRequestsModerationTable" style="margin-top: 10px">
|
||||
<el-table-column width="55" prop="$selected">
|
||||
<template #default="scope">
|
||||
<Checkbox
|
||||
v-model="scope.row.$selected"
|
||||
@update:modelValue="
|
||||
groupMemberModerationTableSelectionChange(scope.row)
|
||||
" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.avatar')"
|
||||
width="70"
|
||||
prop="photo">
|
||||
<template #default="scope">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
:src="userImage(scope.row.user)"
|
||||
class="friends-list-avatar"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(scope.row.user)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.display_name')"
|
||||
width="160"
|
||||
prop="$displayName"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span style="cursor: pointer" @click="showUserDialog(scope.row.userId)">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.user.$userColour }"
|
||||
v-text="scope.row.user.displayName"></span>
|
||||
<span v-else v-text="scope.row.user?.displayName"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.notes')"
|
||||
prop="managerNotes"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span @click.stop v-text="scope.row.managerNotes"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="groupJoinRequestsModerationTanstackTable"
|
||||
:loading="isGroupMembersLoading"
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupJoinRequestsModerationTotalItems" />
|
||||
<br />
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -533,61 +276,12 @@
|
||||
<Button size="sm" variant="outline" @click="selectAllGroupBlocked">{{
|
||||
t('dialog.group_member_moderation.select_all')
|
||||
}}</Button>
|
||||
<DataTable v-bind="groupBlockedModerationTable" style="margin-top: 10px">
|
||||
<el-table-column width="55" prop="$selected">
|
||||
<template #default="scope">
|
||||
<Checkbox
|
||||
v-model="scope.row.$selected"
|
||||
@update:modelValue="
|
||||
groupMemberModerationTableSelectionChange(scope.row)
|
||||
" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.avatar')"
|
||||
width="70"
|
||||
prop="photo">
|
||||
<template #default="scope">
|
||||
<el-popover placement="right" :width="500" trigger="hover">
|
||||
<template #reference>
|
||||
<img
|
||||
:src="userImage(scope.row.user)"
|
||||
class="friends-list-avatar"
|
||||
loading="lazy" />
|
||||
</template>
|
||||
<img
|
||||
:src="userImageFull(scope.row.user)"
|
||||
:class="['friends-list-avatar', 'x-popover-image']"
|
||||
style="cursor: pointer"
|
||||
@click="showFullscreenImageDialog(userImageFull(scope.row.user))"
|
||||
loading="lazy" />
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.display_name')"
|
||||
width="160"
|
||||
prop="$displayName"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span style="cursor: pointer" @click="showUserDialog(scope.row.userId)">
|
||||
<span
|
||||
v-if="randomUserColours"
|
||||
:style="{ color: scope.row.user.$userColour }"
|
||||
v-text="scope.row.user.displayName"></span>
|
||||
<span v-else v-text="scope.row.user?.displayName"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.notes')"
|
||||
prop="managerNotes"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span @click.stop v-text="scope.row.managerNotes"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="groupBlockedModerationTanstackTable"
|
||||
:loading="isGroupMembersLoading"
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupBlockedModerationTotalItems" />
|
||||
<br />
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -651,55 +345,12 @@
|
||||
:placeholder="t('dialog.group.members.search')"
|
||||
style="margin-top: 10px; margin-bottom: 10px" />
|
||||
<br />
|
||||
<DataTable v-bind="groupLogsModerationTable" style="margin-top: 10px">
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.created_at')"
|
||||
width="170"
|
||||
prop="created_at"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.type')"
|
||||
width="190"
|
||||
prop="eventType"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span v-text="scope.row.eventType"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.display_name')"
|
||||
width="160"
|
||||
prop="actorDisplayName"
|
||||
sortable>
|
||||
<template #default="scope">
|
||||
<span style="cursor: pointer" @click="showUserDialog(scope.row.actorId)">
|
||||
<span v-text="scope.row.actorDisplayName"></span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="t('dialog.group_member_moderation.description')"
|
||||
prop="description">
|
||||
<template #default="scope">
|
||||
<Location
|
||||
v-if="scope.row?.targetId.startsWith('wrld_')"
|
||||
:location="scope.row.targetId" />
|
||||
<span v-text="scope.row.description"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('dialog.group_member_moderation.data')" prop="data">
|
||||
<template #default="scope">
|
||||
<span v-if="Object.keys(scope.row.data).length">{{
|
||||
JSON.stringify(scope.row.data)
|
||||
}}</span>
|
||||
<span v-else></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="groupLogsModerationTanstackTable"
|
||||
:loading="isGroupMembersLoading"
|
||||
:page-sizes="pageSizes"
|
||||
:total-items="groupLogsModerationTotalItems" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -878,7 +529,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown, Loading, Refresh, Warning } from '@element-plus/icons-vue';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
@@ -894,13 +545,21 @@
|
||||
import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants';
|
||||
import { groupRequest, userRequest } from '../../../api';
|
||||
import { Badge } from '../../ui/badge';
|
||||
import { Checkbox } from '../../ui/checkbox';
|
||||
import { DataTableLayout } from '../../ui/data-table';
|
||||
import { createColumns as createMembersColumns } from './groupMemberModerationMembersColumns.jsx';
|
||||
import { createColumns as createBansColumns } from './groupMemberModerationBansColumns.jsx';
|
||||
import { createColumns as createInvitesColumns } from './groupMemberModerationInvitesColumns.jsx';
|
||||
import { createColumns as createJoinRequestsColumns } from './groupMemberModerationJoinRequestsColumns.jsx';
|
||||
import { createColumns as createBlockedColumns } from './groupMemberModerationBlockedColumns.jsx';
|
||||
import { createColumns as createLogsColumns } from './groupMemberModerationLogsColumns.jsx';
|
||||
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
|
||||
|
||||
import GroupMemberModerationExportDialog from './GroupMemberModerationExportDialog.vue';
|
||||
|
||||
import * as workerTimers from 'worker-timers';
|
||||
|
||||
const { randomUserColours } = storeToRefs(useAppearanceSettingsStore());
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const { randomUserColours } = storeToRefs(appearanceSettingsStore);
|
||||
const { showUserDialog } = useUserStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
const { groupDialog, groupMemberModeration } = storeToRefs(useGroupStore());
|
||||
@@ -923,6 +582,8 @@
|
||||
const members = ref([]);
|
||||
const memberSearch = ref('');
|
||||
|
||||
const pageSizes = computed(() => appearanceSettingsStore.tablePageSizes);
|
||||
|
||||
let loadMoreGroupMembersParams = ref({
|
||||
n: 100,
|
||||
offset: 0,
|
||||
@@ -1014,6 +675,188 @@
|
||||
}
|
||||
});
|
||||
|
||||
const rolesText = (roleIds) => {
|
||||
const ids = Array.isArray(roleIds) ? roleIds : [];
|
||||
const roles = groupMemberModeration.value?.groupRef?.roles ?? [];
|
||||
const names = [];
|
||||
for (const id of ids) {
|
||||
const role = roles.find((r) => r?.id === id);
|
||||
if (role?.name) {
|
||||
names.push(role.name);
|
||||
}
|
||||
}
|
||||
return names.join(', ');
|
||||
};
|
||||
|
||||
const groupMemberModerationColumns = computed(() =>
|
||||
createMembersColumns({
|
||||
randomUserColours,
|
||||
rolesText,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage: showFullscreenImageDialog,
|
||||
onShowUser: showUserDialog,
|
||||
onSelectionChange: groupMemberModerationTableSelectionChange
|
||||
})
|
||||
);
|
||||
|
||||
const { table: groupMemberModerationTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'group-moderation:members',
|
||||
data: computed(() => groupMemberModerationTable.data ?? []),
|
||||
columns: groupMemberModerationColumns,
|
||||
getRowId: (row) => String(row?.userId ?? ''),
|
||||
initialPagination: { pageIndex: 0, pageSize: groupMemberModerationTable.pageSize ?? 15 }
|
||||
});
|
||||
|
||||
const groupMemberModerationTotalItems = computed(
|
||||
() => groupMemberModerationTanstackTable.getFilteredRowModel().rows.length
|
||||
);
|
||||
|
||||
const bansSearch = computed(() =>
|
||||
String(groupBansModerationTable.filters?.[0]?.value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
const groupBansFilteredRows = computed(() => {
|
||||
const rows = Array.isArray(groupBansModerationTable.data) ? groupBansModerationTable.data : [];
|
||||
const q = bansSearch.value;
|
||||
if (!q) {
|
||||
return rows;
|
||||
}
|
||||
return rows.filter((r) => {
|
||||
const name = (r?.$displayName ?? r?.user?.displayName ?? '').toString().toLowerCase();
|
||||
return name.includes(q);
|
||||
});
|
||||
});
|
||||
|
||||
const groupBansModerationColumns = computed(() =>
|
||||
createBansColumns({
|
||||
randomUserColours,
|
||||
rolesText,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage: showFullscreenImageDialog,
|
||||
onShowUser: showUserDialog,
|
||||
onSelectionChange: groupMemberModerationTableSelectionChange
|
||||
})
|
||||
);
|
||||
|
||||
const { table: groupBansModerationTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'group-moderation:bans',
|
||||
data: groupBansFilteredRows,
|
||||
columns: groupBansModerationColumns,
|
||||
getRowId: (row) => String(row?.userId ?? row?.id ?? ''),
|
||||
initialPagination: { pageIndex: 0, pageSize: groupBansModerationTable.pageSize ?? 15 }
|
||||
});
|
||||
|
||||
const groupBansModerationTotalItems = computed(
|
||||
() => groupBansModerationTanstackTable.getFilteredRowModel().rows.length
|
||||
);
|
||||
|
||||
const groupInvitesModerationColumns = computed(() =>
|
||||
createInvitesColumns({
|
||||
randomUserColours,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage: showFullscreenImageDialog,
|
||||
onShowUser: showUserDialog,
|
||||
onSelectionChange: groupMemberModerationTableSelectionChange
|
||||
})
|
||||
);
|
||||
|
||||
const { table: groupInvitesModerationTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'group-moderation:invites',
|
||||
data: computed(() => groupInvitesModerationTable.data ?? []),
|
||||
columns: groupInvitesModerationColumns,
|
||||
getRowId: (row) => String(row?.userId ?? row?.id ?? ''),
|
||||
initialPagination: { pageIndex: 0, pageSize: groupInvitesModerationTable.pageSize ?? 15 }
|
||||
});
|
||||
|
||||
const groupInvitesModerationTotalItems = computed(
|
||||
() => groupInvitesModerationTanstackTable.getFilteredRowModel().rows.length
|
||||
);
|
||||
|
||||
const groupJoinRequestsModerationColumns = computed(() =>
|
||||
createJoinRequestsColumns({
|
||||
randomUserColours,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage: showFullscreenImageDialog,
|
||||
onShowUser: showUserDialog,
|
||||
onSelectionChange: groupMemberModerationTableSelectionChange
|
||||
})
|
||||
);
|
||||
|
||||
const { table: groupJoinRequestsModerationTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'group-moderation:join-requests',
|
||||
data: computed(() => groupJoinRequestsModerationTable.data ?? []),
|
||||
columns: groupJoinRequestsModerationColumns,
|
||||
getRowId: (row) => String(row?.userId ?? row?.id ?? ''),
|
||||
initialPagination: { pageIndex: 0, pageSize: groupJoinRequestsModerationTable.pageSize ?? 15 }
|
||||
});
|
||||
|
||||
const groupJoinRequestsModerationTotalItems = computed(
|
||||
() => groupJoinRequestsModerationTanstackTable.getFilteredRowModel().rows.length
|
||||
);
|
||||
|
||||
const groupBlockedModerationColumns = computed(() =>
|
||||
createBlockedColumns({
|
||||
randomUserColours,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage: showFullscreenImageDialog,
|
||||
onShowUser: showUserDialog,
|
||||
onSelectionChange: groupMemberModerationTableSelectionChange
|
||||
})
|
||||
);
|
||||
|
||||
const { table: groupBlockedModerationTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'group-moderation:blocked',
|
||||
data: computed(() => groupBlockedModerationTable.data ?? []),
|
||||
columns: groupBlockedModerationColumns,
|
||||
getRowId: (row) => String(row?.userId ?? row?.id ?? ''),
|
||||
initialPagination: { pageIndex: 0, pageSize: groupBlockedModerationTable.pageSize ?? 15 }
|
||||
});
|
||||
|
||||
const groupBlockedModerationTotalItems = computed(
|
||||
() => groupBlockedModerationTanstackTable.getFilteredRowModel().rows.length
|
||||
);
|
||||
|
||||
const logsSearch = computed(() =>
|
||||
String(groupLogsModerationTable.filters?.[0]?.value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
const groupLogsFilteredRows = computed(() => {
|
||||
const rows = Array.isArray(groupLogsModerationTable.data) ? groupLogsModerationTable.data : [];
|
||||
const q = logsSearch.value;
|
||||
if (!q) {
|
||||
return rows;
|
||||
}
|
||||
return rows.filter((r) => {
|
||||
const desc = (r?.description ?? '').toString().toLowerCase();
|
||||
return desc.includes(q);
|
||||
});
|
||||
});
|
||||
|
||||
const groupLogsModerationColumns = computed(() =>
|
||||
createLogsColumns({
|
||||
onShowUser: showUserDialog
|
||||
})
|
||||
);
|
||||
|
||||
const { table: groupLogsModerationTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'group-moderation:logs',
|
||||
data: groupLogsFilteredRows,
|
||||
columns: groupLogsModerationColumns,
|
||||
getRowId: (row) => String(row?.id ?? `${row?.created_at ?? ''}:${row?.eventType ?? ''}`),
|
||||
initialPagination: { pageIndex: 0, pageSize: groupLogsModerationTable.pageSize ?? 15 }
|
||||
});
|
||||
|
||||
const groupLogsModerationTotalItems = computed(
|
||||
() => groupLogsModerationTanstackTable.getFilteredRowModel().rows.length
|
||||
);
|
||||
|
||||
function deselectGroupMember(userId) {
|
||||
const deselectInTable = (tableData) => {
|
||||
if (userId) {
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { i18n } from '@/plugin';
|
||||
import { formatDateFilter } from '@/shared/utils';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({
|
||||
randomUserColours,
|
||||
rolesText,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage,
|
||||
onShowUser,
|
||||
onSelectionChange
|
||||
}) => [
|
||||
{
|
||||
id: 'selected',
|
||||
header: () => null,
|
||||
size: 55,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
modelValue={!!original?.$selected}
|
||||
onUpdate:modelValue={(value) => {
|
||||
original.$selected = value;
|
||||
onSelectionChange?.(original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'avatar',
|
||||
header: () => t('dialog.group_member_moderation.avatar'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const thumb = userImage?.(original?.user);
|
||||
const full = userImageFull?.(original?.user);
|
||||
|
||||
return (
|
||||
<el-popover
|
||||
placement="right"
|
||||
width={500}
|
||||
trigger="hover"
|
||||
v-slots={{
|
||||
reference: () => (
|
||||
<img
|
||||
src={thumb}
|
||||
class="friends-list-avatar"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={full}
|
||||
class={['friends-list-avatar', 'x-popover-image']}
|
||||
style="cursor: pointer"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (full) {
|
||||
onShowFullscreenImage?.(full);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</el-popover>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
accessorFn: (row) => row?.user?.displayName ?? row?.$displayName ?? '',
|
||||
header: () => t('dialog.group_member_moderation.display_name'),
|
||||
size: 160,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const useColors = !!(randomUserColours?.value ?? randomUserColours);
|
||||
const colorStyle = useColors ? { color: original?.user?.$userColour } : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
style="cursor: pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowUser?.(original?.userId);
|
||||
}}
|
||||
>
|
||||
<span style={colorStyle}>{original?.user?.displayName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
accessorFn: (row) => rolesText?.(row?.roleIds) ?? '',
|
||||
header: () => t('dialog.group_member_moderation.roles'),
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return <span>{rolesText?.(original?.roleIds) ?? ''}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'managerNotes',
|
||||
header: () => t('dialog.group_member_moderation.notes'),
|
||||
cell: ({ row }) => (
|
||||
<span onClick={(e) => e.stopPropagation()}>{row.original?.managerNotes}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'joinedAt',
|
||||
header: () => t('dialog.group_member_moderation.joined_at'),
|
||||
size: 170,
|
||||
cell: ({ row }) => <span>{formatDateFilter(row.original?.joinedAt, 'long')}</span>
|
||||
},
|
||||
{
|
||||
accessorKey: 'bannedAt',
|
||||
header: () => t('dialog.group_member_moderation.banned_at'),
|
||||
size: 170,
|
||||
cell: ({ row }) => <span>{formatDateFilter(row.original?.bannedAt, 'long')}</span>
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({
|
||||
randomUserColours,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage,
|
||||
onShowUser,
|
||||
onSelectionChange
|
||||
}) => [
|
||||
{
|
||||
id: 'selected',
|
||||
header: () => null,
|
||||
size: 55,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
modelValue={!!original?.$selected}
|
||||
onUpdate:modelValue={(value) => {
|
||||
original.$selected = value;
|
||||
onSelectionChange?.(original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'avatar',
|
||||
header: () => t('dialog.group_member_moderation.avatar'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const thumb = userImage?.(original?.user);
|
||||
const full = userImageFull?.(original?.user);
|
||||
|
||||
return (
|
||||
<el-popover
|
||||
placement="right"
|
||||
width={500}
|
||||
trigger="hover"
|
||||
v-slots={{
|
||||
reference: () => (
|
||||
<img
|
||||
src={thumb}
|
||||
class="friends-list-avatar"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={full}
|
||||
class={['friends-list-avatar', 'x-popover-image']}
|
||||
style="cursor: pointer"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (full) {
|
||||
onShowFullscreenImage?.(full);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</el-popover>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
accessorFn: (row) => row?.user?.displayName ?? row?.$displayName ?? '',
|
||||
header: () => t('dialog.group_member_moderation.display_name'),
|
||||
size: 160,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const useColors = !!(randomUserColours?.value ?? randomUserColours);
|
||||
const colorStyle = useColors ? { color: original?.user?.$userColour } : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
style="cursor: pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowUser?.(original?.userId);
|
||||
}}
|
||||
>
|
||||
<span style={colorStyle}>{original?.user?.displayName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'managerNotes',
|
||||
header: () => t('dialog.group_member_moderation.notes'),
|
||||
cell: ({ row }) => (
|
||||
<span onClick={(e) => e.stopPropagation()}>{row.original?.managerNotes}</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({
|
||||
randomUserColours,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage,
|
||||
onShowUser,
|
||||
onSelectionChange
|
||||
}) => [
|
||||
{
|
||||
id: 'selected',
|
||||
header: () => null,
|
||||
size: 55,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
modelValue={!!original?.$selected}
|
||||
onUpdate:modelValue={(value) => {
|
||||
original.$selected = value;
|
||||
onSelectionChange?.(original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'avatar',
|
||||
header: () => t('dialog.group_member_moderation.avatar'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const thumb = userImage?.(original?.user);
|
||||
const full = userImageFull?.(original?.user);
|
||||
|
||||
return (
|
||||
<el-popover
|
||||
placement="right"
|
||||
width={500}
|
||||
trigger="hover"
|
||||
v-slots={{
|
||||
reference: () => (
|
||||
<img
|
||||
src={thumb}
|
||||
class="friends-list-avatar"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={full}
|
||||
class={['friends-list-avatar', 'x-popover-image']}
|
||||
style="cursor: pointer"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (full) {
|
||||
onShowFullscreenImage?.(full);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</el-popover>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
accessorFn: (row) => row?.user?.displayName ?? row?.$displayName ?? '',
|
||||
header: () => t('dialog.group_member_moderation.display_name'),
|
||||
size: 160,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const useColors = !!(randomUserColours?.value ?? randomUserColours);
|
||||
const colorStyle = useColors ? { color: original?.user?.$userColour } : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
style="cursor: pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowUser?.(original?.userId);
|
||||
}}
|
||||
>
|
||||
<span style={colorStyle}>{original?.user?.displayName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'managerNotes',
|
||||
header: () => t('dialog.group_member_moderation.notes'),
|
||||
cell: ({ row }) => (
|
||||
<span onClick={(e) => e.stopPropagation()}>{row.original?.managerNotes}</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({
|
||||
randomUserColours,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage,
|
||||
onShowUser,
|
||||
onSelectionChange
|
||||
}) => [
|
||||
{
|
||||
id: 'selected',
|
||||
header: () => null,
|
||||
size: 55,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
modelValue={!!original?.$selected}
|
||||
onUpdate:modelValue={(value) => {
|
||||
original.$selected = value;
|
||||
onSelectionChange?.(original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'avatar',
|
||||
header: () => t('dialog.group_member_moderation.avatar'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const thumb = userImage?.(original?.user);
|
||||
const full = userImageFull?.(original?.user);
|
||||
|
||||
return (
|
||||
<el-popover
|
||||
placement="right"
|
||||
width={500}
|
||||
trigger="hover"
|
||||
v-slots={{
|
||||
reference: () => (
|
||||
<img
|
||||
src={thumb}
|
||||
class="friends-list-avatar"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={full}
|
||||
class={['friends-list-avatar', 'x-popover-image']}
|
||||
style="cursor: pointer"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (full) {
|
||||
onShowFullscreenImage?.(full);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</el-popover>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
accessorFn: (row) => row?.user?.displayName ?? row?.$displayName ?? '',
|
||||
header: () => t('dialog.group_member_moderation.display_name'),
|
||||
size: 160,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const useColors = !!(randomUserColours?.value ?? randomUserColours);
|
||||
const colorStyle = useColors ? { color: original?.user?.$userColour } : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
style="cursor: pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowUser?.(original?.userId);
|
||||
}}
|
||||
>
|
||||
<span style={colorStyle}>{original?.user?.displayName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'managerNotes',
|
||||
header: () => t('dialog.group_member_moderation.notes'),
|
||||
cell: ({ row }) => (
|
||||
<span onClick={(e) => e.stopPropagation()}>{row.original?.managerNotes}</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,72 @@
|
||||
import Location from '@/components/Location.vue';
|
||||
import { i18n } from '@/plugin';
|
||||
import { formatDateFilter } from '@/shared/utils';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({ onShowUser }) => [
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: () => t('dialog.group_member_moderation.created_at'),
|
||||
size: 170,
|
||||
cell: ({ row }) => (
|
||||
<span>{formatDateFilter(row.original?.created_at, 'long')}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'eventType',
|
||||
header: () => t('dialog.group_member_moderation.type'),
|
||||
size: 190,
|
||||
cell: ({ row }) => <span>{row.original?.eventType}</span>
|
||||
},
|
||||
{
|
||||
accessorKey: 'actorDisplayName',
|
||||
header: () => t('dialog.group_member_moderation.display_name'),
|
||||
size: 160,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<span
|
||||
style="cursor: pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowUser?.(original?.actorId);
|
||||
}}
|
||||
>
|
||||
<span>{original?.actorDisplayName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
accessorFn: (row) => row?.description ?? '',
|
||||
header: () => t('dialog.group_member_moderation.description'),
|
||||
meta: {
|
||||
stretch: true
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const targetId = original?.targetId ?? '';
|
||||
return (
|
||||
<span>
|
||||
{typeof targetId === 'string' && targetId.startsWith('wrld_') ? (
|
||||
<Location location={targetId} />
|
||||
) : null}
|
||||
<span>{original?.description}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
header: () => t('dialog.group_member_moderation.data'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const data = original?.data;
|
||||
const hasData = data && typeof data === 'object' && Object.keys(data).length;
|
||||
return <span>{hasData ? JSON.stringify(data) : ''}</span>;
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { i18n } from '@/plugin';
|
||||
import { formatDateFilter } from '@/shared/utils';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({
|
||||
randomUserColours,
|
||||
rolesText,
|
||||
userImage,
|
||||
userImageFull,
|
||||
onShowFullscreenImage,
|
||||
onShowUser,
|
||||
onSelectionChange
|
||||
}) => [
|
||||
{
|
||||
id: 'selected',
|
||||
header: () => null,
|
||||
size: 55,
|
||||
enableSorting: false,
|
||||
enableResizing: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return (
|
||||
<div
|
||||
class="flex items-center justify-center"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
modelValue={!!original?.$selected}
|
||||
onUpdate:modelValue={(value) => {
|
||||
original.$selected = value;
|
||||
onSelectionChange?.(original);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'avatar',
|
||||
header: () => t('dialog.group_member_moderation.avatar'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const thumb = userImage?.(original?.user);
|
||||
const full = userImageFull?.(original?.user);
|
||||
|
||||
return (
|
||||
<el-popover
|
||||
placement="right"
|
||||
width={500}
|
||||
trigger="hover"
|
||||
v-slots={{
|
||||
reference: () => (
|
||||
<img
|
||||
src={thumb}
|
||||
class="friends-list-avatar"
|
||||
loading="lazy"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={full}
|
||||
class={['friends-list-avatar', 'x-popover-image']}
|
||||
style="cursor: pointer"
|
||||
loading="lazy"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (full) {
|
||||
onShowFullscreenImage?.(full);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</el-popover>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'displayName',
|
||||
accessorFn: (row) => row?.user?.displayName ?? '',
|
||||
header: () => t('dialog.group_member_moderation.display_name'),
|
||||
size: 160,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const useColors = !!(randomUserColours?.value ?? randomUserColours);
|
||||
const colorStyle = useColors ? { color: original?.user?.$userColour } : null;
|
||||
|
||||
return (
|
||||
<span
|
||||
style="cursor: pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onShowUser?.(original?.userId);
|
||||
}}
|
||||
>
|
||||
<span style={colorStyle}>{original?.user?.displayName}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'roles',
|
||||
accessorFn: (row) => rolesText?.(row?.roleIds) ?? '',
|
||||
header: () => t('dialog.group_member_moderation.roles'),
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
return <span>{rolesText?.(original?.roleIds) ?? ''}</span>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'managerNotes',
|
||||
header: () => t('dialog.group_member_moderation.notes'),
|
||||
cell: ({ row }) => (
|
||||
<span onClick={(e) => e.stopPropagation()}>{row.original?.managerNotes}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'joinedAt',
|
||||
header: () => t('dialog.group_member_moderation.joined_at'),
|
||||
size: 170,
|
||||
cell: ({ row }) => <span>{formatDateFilter(row.original?.joinedAt, 'long')}</span>
|
||||
},
|
||||
{
|
||||
accessorKey: 'visibility',
|
||||
header: () => t('dialog.group_member_moderation.visibility'),
|
||||
size: 120,
|
||||
cell: ({ row }) => <span>{row.original?.visibility}</span>
|
||||
}
|
||||
];
|
||||
@@ -35,38 +35,12 @@
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
<DataTable
|
||||
v-bind="inviteMessageTable"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showSendInviteConfirmDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
:sortable="true"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
|
||||
<template #default="scope">
|
||||
<Button
|
||||
size="icon-sm"
|
||||
class="w-6 h-6"
|
||||
variant="ghost"
|
||||
@click.stop="showEditAndSendInviteDialog(scope.row)">
|
||||
<SquarePen />
|
||||
</Button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="inviteMessageTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleInviteMessageRowClick" />
|
||||
|
||||
<template #footer>
|
||||
<Button variant="secondary" @click="cancelSendInvite">
|
||||
@@ -92,13 +66,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVrcxVueTable } from '@/lib/table/useVrcxVueTable';
|
||||
|
||||
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
|
||||
import { createColumns } from './sendInviteColumns.jsx';
|
||||
|
||||
import EditAndSendInviteDialog from './EditAndSendInviteDialog.vue';
|
||||
import SendInviteConfirmDialog from './SendInviteConfirmDialog.vue';
|
||||
@@ -135,6 +111,26 @@
|
||||
newMessage: ''
|
||||
});
|
||||
|
||||
const inviteMessageRows = computed(() => inviteMessageTable.value?.data ?? []);
|
||||
const inviteMessageColumns = computed(() =>
|
||||
createColumns({
|
||||
onEdit: showEditAndSendInviteDialog
|
||||
})
|
||||
);
|
||||
|
||||
const { table: inviteMessageTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'invite-message',
|
||||
data: inviteMessageRows,
|
||||
columns: inviteMessageColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
function handleInviteMessageRowClick(row) {
|
||||
showSendInviteConfirmDialog(row?.original);
|
||||
}
|
||||
|
||||
function showSendInviteConfirmDialog(row) {
|
||||
emit('update:sendInviteDialog', { ...props.sendInviteDialog, messageSlot: row });
|
||||
isSendInviteConfirmDialogVisible.value = true;
|
||||
|
||||
54
src/components/dialogs/InviteDialog/sendInviteColumns.jsx
Normal file
54
src/components/dialogs/InviteDialog/sendInviteColumns.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { i18n } from '@/plugin';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({ onEdit }) => [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
header: () => t('table.profile.invite_messages.action'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="icon-sm"
|
||||
class="w-6 h-6"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -10,34 +10,12 @@
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
<DataTable
|
||||
v-bind="inviteRequestMessageTable"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showSendInviteConfirmDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
:sortable="true"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
|
||||
<template #default="scope">
|
||||
<Button size="icon-sm" variant="ghost" @click.stop="showEditAndSendInviteDialog(scope.row)">
|
||||
<SquarePen
|
||||
/></Button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="inviteRequestMessageTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleInviteRequestMessageRowClick" />
|
||||
|
||||
<template #footer>
|
||||
<Button variant="secondary" class="mr-2" @click="cancelSendInviteRequest">{{
|
||||
@@ -63,13 +41,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVrcxVueTable } from '@/lib/table/useVrcxVueTable';
|
||||
|
||||
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
|
||||
import { createColumns } from './sendInviteRequestColumns.jsx';
|
||||
|
||||
import EditAndSendInviteDialog from '../InviteDialog/EditAndSendInviteDialog.vue';
|
||||
import SendInviteConfirmDialog from '../InviteDialog/SendInviteConfirmDialog.vue';
|
||||
@@ -108,6 +88,26 @@
|
||||
newMessage: ''
|
||||
});
|
||||
|
||||
const inviteRequestMessageRows = computed(() => inviteRequestMessageTable.value?.data ?? []);
|
||||
const inviteRequestMessageColumns = computed(() =>
|
||||
createColumns({
|
||||
onEdit: showEditAndSendInviteDialog
|
||||
})
|
||||
);
|
||||
|
||||
const { table: inviteRequestMessageTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'invite-request-message',
|
||||
data: inviteRequestMessageRows,
|
||||
columns: inviteRequestMessageColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
function handleInviteRequestMessageRowClick(row) {
|
||||
showSendInviteConfirmDialog(row?.original);
|
||||
}
|
||||
|
||||
function showSendInviteConfirmDialog(row) {
|
||||
emit('update:sendInviteDialog', { ...props.sendInviteDialog, messageSlot: row });
|
||||
isSendInviteConfirmDialogVisible.value = true;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { i18n } from '@/plugin';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({ onEdit }) => [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
header: () => t('table.profile.invite_messages.action'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -2,7 +2,6 @@ import { TooltipWrapper } from '../components/ui/tooltip';
|
||||
|
||||
import AvatarInfo from '../components/AvatarInfo.vue';
|
||||
import CountdownTimer from '../components/CountdownTimer.vue';
|
||||
import DataTable from '../components/DataTable.vue';
|
||||
import DisplayName from '../components/DisplayName.vue';
|
||||
import InstanceInfo from '../components/InstanceInfo.vue';
|
||||
import InviteYourself from '../components/InviteYourself.vue';
|
||||
@@ -23,6 +22,5 @@ export function initComponents(app) {
|
||||
app.component('InviteYourself', InviteYourself);
|
||||
app.component('Launch', Launch);
|
||||
app.component('LocationWorld', LocationWorld);
|
||||
app.component('DataTable', DataTable);
|
||||
app.component('TooltipWrapper', TooltipWrapper);
|
||||
}
|
||||
|
||||
@@ -10,31 +10,12 @@
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
<DataTable
|
||||
v-bind="inviteRequestResponseMessageTable"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showSendInviteResponseConfirmDialog">
|
||||
<el-table-column :label="t('table.profile.invite_messages.slot')" prop="slot" :sortable="true" width="70">
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message"> </el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
|
||||
<template #default="scope">
|
||||
<Button size="icon-sm" variant="ghost" @click.stop="showEditAndSendInviteResponseDialog(scope.row)">
|
||||
<SquarePen
|
||||
/></Button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="inviteRequestResponseTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleInviteRequestResponseRowClick" />
|
||||
|
||||
<template #footer>
|
||||
<Button variant="secondary" class="mr-2" @click="cancelSendInviteRequestResponse">
|
||||
@@ -58,13 +39,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVrcxVueTable } from '@/lib/table/useVrcxVueTable';
|
||||
|
||||
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
|
||||
import { createColumns } from './sendInviteRequestResponseColumns.jsx';
|
||||
|
||||
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
|
||||
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
|
||||
@@ -99,6 +82,26 @@
|
||||
visible: false
|
||||
});
|
||||
|
||||
const inviteRequestResponseRows = computed(() => inviteRequestResponseMessageTable.value?.data ?? []);
|
||||
const inviteRequestResponseColumns = computed(() =>
|
||||
createColumns({
|
||||
onEdit: showEditAndSendInviteResponseDialog
|
||||
})
|
||||
);
|
||||
|
||||
const { table: inviteRequestResponseTable } = useVrcxVueTable({
|
||||
persistKey: 'invite-request-response-message',
|
||||
data: inviteRequestResponseRows,
|
||||
columns: inviteRequestResponseColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
function handleInviteRequestResponseRowClick(row) {
|
||||
showSendInviteResponseConfirmDialog(row?.original);
|
||||
}
|
||||
|
||||
function showEditAndSendInviteResponseDialog(row) {
|
||||
emit('update:sendInviteResponseDialog', { ...props.sendInviteResponseDialog, messageSlot: row });
|
||||
editAndSendInviteResponseDialog.value = {
|
||||
|
||||
@@ -10,30 +10,12 @@
|
||||
<input class="inviteImageUploadButton" type="file" accept="image/*" @change="inviteImageUpload" />
|
||||
</template>
|
||||
|
||||
<DataTable
|
||||
v-bind="inviteResponseMessageTable"
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showSendInviteResponseConfirmDialog">
|
||||
<el-table-column :label="t('table.profile.invite_messages.slot')" prop="slot" :sortable="true" width="70" />
|
||||
<el-table-column :label="t('table.profile.invite_messages.message')" prop="message" />
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('table.profile.invite_messages.action')" width="70" align="right">
|
||||
<template #default="scope">
|
||||
<Button size="icon-sm" variant="ghost" @click.stop="showEditAndSendInviteResponseDialog(scope.row)">
|
||||
<SquarePen
|
||||
/></Button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px"
|
||||
:table="inviteResponseTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleInviteResponseRowClick" />
|
||||
|
||||
<template #footer>
|
||||
<Button variant="secondary" class="mr-2" @click="cancelSendInviteResponse">{{
|
||||
@@ -57,13 +39,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
import { ref } from 'vue';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVrcxVueTable } from '@/lib/table/useVrcxVueTable';
|
||||
|
||||
import { useGalleryStore, useInviteStore, useUserStore } from '../../../stores';
|
||||
import { createColumns } from './sendInviteResponseColumns.jsx';
|
||||
|
||||
import EditAndSendInviteResponseDialog from './EditAndSendInviteResponseDialog.vue';
|
||||
import SendInviteResponseConfirmDialog from './SendInviteResponseConfirmDialog.vue';
|
||||
@@ -99,6 +83,26 @@
|
||||
visible: false
|
||||
});
|
||||
|
||||
const inviteResponseRows = computed(() => inviteResponseMessageTable.value?.data ?? []);
|
||||
const inviteResponseColumns = computed(() =>
|
||||
createColumns({
|
||||
onEdit: showEditAndSendInviteResponseDialog
|
||||
})
|
||||
);
|
||||
|
||||
const { table: inviteResponseTable } = useVrcxVueTable({
|
||||
persistKey: 'invite-response-message',
|
||||
data: inviteResponseRows,
|
||||
columns: inviteResponseColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
function handleInviteResponseRowClick(row) {
|
||||
showSendInviteResponseConfirmDialog(row?.original);
|
||||
}
|
||||
|
||||
function closeInviteDialog() {
|
||||
cancelSendInviteResponse();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { i18n } from '@/plugin';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({ onEdit }) => [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
header: () => t('table.profile.invite_messages.action'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,53 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { i18n } from '@/plugin';
|
||||
import { SquarePen } from 'lucide-vue-next';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const createColumns = ({ onEdit }) => [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
header: () => t('table.profile.invite_messages.action'),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(row.original);
|
||||
}}
|
||||
>
|
||||
<SquarePen />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -7,104 +7,36 @@
|
||||
@close="closeDialog">
|
||||
<el-tabs v-model="activeTab" style="margin-top: 10px">
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_message_tab')" name="message">
|
||||
<DataTable
|
||||
v-bind="inviteMessageTable"
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showEditInviteMessageDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
:sortable="true"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.message')"
|
||||
prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
:table="inviteMessageTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_request_tab')" name="request">
|
||||
<DataTable
|
||||
v-bind="inviteRequestMessageTable"
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showEditInviteMessageDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
:sortable="true"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.message')"
|
||||
prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
:table="inviteRequestTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_request_response_tab')" name="requestResponse">
|
||||
<DataTable
|
||||
v-bind="inviteRequestResponseMessageTable"
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showEditInviteMessageDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
:sortable="true"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.message')"
|
||||
prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
:table="inviteRequestResponseTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_response_tab')" name="response">
|
||||
<DataTable
|
||||
v-bind="inviteResponseMessageTable"
|
||||
<DataTableLayout
|
||||
style="margin-top: 10px; cursor: pointer"
|
||||
@row-click="showEditInviteMessageDialog">
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.slot')"
|
||||
prop="slot"
|
||||
:sortable="true"
|
||||
width="70"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.message')"
|
||||
prop="message"></el-table-column>
|
||||
<el-table-column
|
||||
:label="t('table.profile.invite_messages.cool_down')"
|
||||
prop="updatedAt"
|
||||
:sortable="true"
|
||||
width="110"
|
||||
align="right">
|
||||
<template #default="scope">
|
||||
<countdown-timer :datetime="scope.row.updatedAt" :hours="1"></countdown-timer>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</DataTable>
|
||||
:table="inviteResponseTanstackTable"
|
||||
:loading="false"
|
||||
:show-pagination="false"
|
||||
:on-row-click="handleEditInviteMessageRowClick" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
@@ -118,14 +50,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { DataTableLayout } from '@/components/ui/data-table';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVrcxVueTable } from '@/lib/table/useVrcxVueTable';
|
||||
|
||||
import { columns as inviteMessageColumns } from './editInviteMessagesMessageColumns.jsx';
|
||||
import { columns as inviteRequestColumns } from './editInviteMessagesRequestColumns.jsx';
|
||||
import { columns as inviteRequestResponseColumns } from './editInviteMessagesRequestResponseColumns.jsx';
|
||||
import { columns as inviteResponseColumns } from './editInviteMessagesResponseColumns.jsx';
|
||||
import { useInviteStore } from '../../../stores';
|
||||
|
||||
import DataTable from '../../../components/DataTable.vue';
|
||||
import EditInviteMessageDialog from './EditInviteMessageDialog.vue';
|
||||
|
||||
const {
|
||||
@@ -167,6 +104,51 @@
|
||||
emit('close');
|
||||
}
|
||||
|
||||
const inviteMessageRows = computed(() => inviteMessageTable.value?.data ?? []);
|
||||
const inviteRequestRows = computed(() => inviteRequestMessageTable.value?.data ?? []);
|
||||
const inviteRequestResponseRows = computed(() => inviteRequestResponseMessageTable.value?.data ?? []);
|
||||
const inviteResponseRows = computed(() => inviteResponseMessageTable.value?.data ?? []);
|
||||
|
||||
const { table: inviteMessageTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'edit-invite-messages:message',
|
||||
data: inviteMessageRows,
|
||||
columns: inviteMessageColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
const { table: inviteRequestTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'edit-invite-messages:request',
|
||||
data: inviteRequestRows,
|
||||
columns: inviteRequestColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
const { table: inviteRequestResponseTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'edit-invite-messages:request-response',
|
||||
data: inviteRequestResponseRows,
|
||||
columns: inviteRequestResponseColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
const { table: inviteResponseTanstackTable } = useVrcxVueTable({
|
||||
persistKey: 'edit-invite-messages:response',
|
||||
data: inviteResponseRows,
|
||||
columns: inviteResponseColumns,
|
||||
getRowId: (row) => String(row?.slot ?? ''),
|
||||
enablePagination: false,
|
||||
initialSorting: [{ id: 'slot', desc: false }]
|
||||
});
|
||||
|
||||
function handleEditInviteMessageRowClick(row) {
|
||||
showEditInviteMessageDialog(row?.original);
|
||||
}
|
||||
|
||||
function showEditInviteMessageDialog(row) {
|
||||
if (row.updatedAt) {
|
||||
const cooldownEnd = new Date(row.updatedAt);
|
||||
|
||||
30
src/views/Tools/dialogs/editInviteMessagesMessageColumns.jsx
Normal file
30
src/views/Tools/dialogs/editInviteMessagesMessageColumns.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
}
|
||||
];
|
||||
30
src/views/Tools/dialogs/editInviteMessagesRequestColumns.jsx
Normal file
30
src/views/Tools/dialogs/editInviteMessagesRequestColumns.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
import CountdownTimer from '@/components/CountdownTimer.vue';
|
||||
import { i18n } from '@/plugin';
|
||||
|
||||
const { t } = i18n.global;
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
accessorKey: 'slot',
|
||||
header: () => t('table.profile.invite_messages.slot'),
|
||||
size: 70
|
||||
},
|
||||
{
|
||||
accessorKey: 'message',
|
||||
header: () => t('table.profile.invite_messages.message'),
|
||||
meta: {
|
||||
stretch: true
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'updatedAt',
|
||||
header: () => t('table.profile.invite_messages.cool_down'),
|
||||
size: 110,
|
||||
meta: {
|
||||
tdClass: 'text-right'
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<CountdownTimer datetime={row.original?.updatedAt} hours={1} />
|
||||
)
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user