rewrite tables

This commit is contained in:
pa
2026-01-13 19:28:40 +09:00
committed by Natsumi
parent 6e3aa44710
commit 69921ed54e
22 changed files with 1419 additions and 803 deletions

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>
}
];

View File

@@ -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>
)
}
];

View File

@@ -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>
)
}
];

View File

@@ -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>
)
}
];

View File

@@ -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>;
}
}
];

View File

@@ -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>
}
];

View File

@@ -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;

View 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>
)
}
];

View File

@@ -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;

View File

@@ -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>
)
}
];

View File

@@ -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);
}

View File

@@ -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 = {

View File

@@ -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();
}

View File

@@ -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>
)
}
];

View File

@@ -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>
)
}
];

View File

@@ -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);

View 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} />
)
}
];

View 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} />
)
}
];

View 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} />
)
}
];

View 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} />
)
}
];