feat(frontend): replace native confirms and enhance form controls
Add ConfirmDialog to replace window.confirm for delete actions Enhance SwitchGroup with grid layout, descriptions, and disabled state Update AdminView to use TagsSelect and SwitchGroup for better UX
This commit is contained in:
@@ -7,6 +7,8 @@ import PageHeader from '../components/PageHeader.vue';
|
||||
import ReorderableList from '../components/ReorderableList.vue';
|
||||
import Skeleton from '../components/Skeleton.vue';
|
||||
import StatusMessage from '../components/StatusMessage.vue';
|
||||
import SwitchGroup, { type SwitchGroupOption } from '../components/SwitchGroup.vue';
|
||||
import TagsSelect, { type TagsSelectOption } from '../components/TagsSelect.vue';
|
||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||
import TranslationFields from '../components/TranslationFields.vue';
|
||||
import {
|
||||
@@ -369,6 +371,31 @@ const dishModalTitle = computed(() => (dishForm.value.id ? t('pages.dish.editDis
|
||||
const dishRows = computed(() => dishCategoryRows.value.flatMap((category) => category.dishes));
|
||||
const selectedDishFormCategory = computed(() => dishCategoryRows.value.find((category) => String(category.id) === dishForm.value.categoryId) ?? null);
|
||||
const dishAllowsSecondSecondaryMaterial = computed(() => (selectedDishFormCategory.value?.totalMaterialQuantity ?? 0) > 2);
|
||||
const dishItemSelectOptions = computed<TagsSelectOption[]>(() => dishItemRows.value.map((item) => ({ id: item.id, name: item.name })));
|
||||
const optionalDishItemSelectOptions = computed<TagsSelectOption[]>(() => [{ id: '', name: t('common.none') }, ...dishItemSelectOptions.value]);
|
||||
const dishCategorySelectOptions = computed<TagsSelectOption[]>(() =>
|
||||
dishCategoryRows.value.map((category) => ({ id: category.id, name: category.name }))
|
||||
);
|
||||
const dishFlavorSelectOptions = computed<TagsSelectOption[]>(() => dishFlavorRows.value.map((flavor) => ({ id: flavor.id, name: flavor.name })));
|
||||
const optionalDishSkillSelectOptions = computed<TagsSelectOption[]>(() => [
|
||||
{ id: '', name: t('common.none') },
|
||||
...dishSkillRows.value.map((skill) => ({ id: skill.id, name: skill.name }))
|
||||
]);
|
||||
const dishCategoryFormValid = computed(
|
||||
() =>
|
||||
dishCategoryForm.value.name.trim() !== '' &&
|
||||
dishCategoryForm.value.effect.trim() !== '' &&
|
||||
dishCategoryForm.value.cookwareItemId !== '' &&
|
||||
dishCategoryForm.value.mainMaterialItemId !== '' &&
|
||||
Number(dishCategoryForm.value.totalMaterialQuantity) >= 2
|
||||
);
|
||||
const dishFormValid = computed(
|
||||
() =>
|
||||
dishForm.value.categoryId !== '' &&
|
||||
dishForm.value.itemId !== '' &&
|
||||
dishForm.value.flavorId !== '' &&
|
||||
dishForm.value.mosslaxEffect.trim() !== ''
|
||||
);
|
||||
const languageModalTitle = computed(() => (editingLanguageCode.value ? t('pages.admin.editLanguage') : t('pages.admin.newLanguage')));
|
||||
const wordingModalTitle = computed(() => t('pages.admin.editWording'));
|
||||
const roleModalTitle = computed(() => (roleForm.value.id ? t('pages.admin.editRole') : t('pages.admin.newRole')));
|
||||
@@ -386,6 +413,26 @@ const permissionGroups = computed(() => {
|
||||
}
|
||||
return [...groups.entries()].map(([category, permissions]) => ({ category, permissions }));
|
||||
});
|
||||
const userRoleSwitchOptions = computed<SwitchGroupOption[]>(() =>
|
||||
roleRows.value.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name,
|
||||
description: role.description,
|
||||
disabled: busy.value || !role.enabled
|
||||
}))
|
||||
);
|
||||
const userRoleSwitchValue = computed<Array<string | number>>({
|
||||
get: () => userRoleForm.value.roleIds,
|
||||
set: (values) => {
|
||||
userRoleForm.value.roleIds = values.map((value) => Number(value)).sort((a, b) => a - b);
|
||||
}
|
||||
});
|
||||
const rolePermissionSwitchValue = computed<Array<string | number>>({
|
||||
get: () => rolePermissionForm.value.permissionIds,
|
||||
set: (values) => {
|
||||
rolePermissionForm.value.permissionIds = values.map((value) => Number(value)).sort((a, b) => a - b);
|
||||
}
|
||||
});
|
||||
const wordingLocaleOptions = computed(() =>
|
||||
languageRows.value.length
|
||||
? languageRows.value
|
||||
@@ -525,24 +572,13 @@ function rolePermissionCount(role: RoleDetail) {
|
||||
return t('pages.admin.permissionCount', { count: role.permissionIds.length });
|
||||
}
|
||||
|
||||
function toggleUserRole(roleId: number) {
|
||||
const roleIds = new Set(userRoleForm.value.roleIds);
|
||||
if (roleIds.has(roleId)) {
|
||||
roleIds.delete(roleId);
|
||||
} else {
|
||||
roleIds.add(roleId);
|
||||
}
|
||||
userRoleForm.value.roleIds = [...roleIds].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function toggleRolePermission(permissionId: number) {
|
||||
const permissionIds = new Set(rolePermissionForm.value.permissionIds);
|
||||
if (permissionIds.has(permissionId)) {
|
||||
permissionIds.delete(permissionId);
|
||||
} else {
|
||||
permissionIds.add(permissionId);
|
||||
}
|
||||
rolePermissionForm.value.permissionIds = [...permissionIds].sort((a, b) => a - b);
|
||||
function permissionSwitchOptions(permissions: Permission[]): SwitchGroupOption[] {
|
||||
return permissions.map((permission) => ({
|
||||
value: permission.id,
|
||||
label: permission.name,
|
||||
description: permission.key,
|
||||
disabled: busy.value || !permission.enabled
|
||||
}));
|
||||
}
|
||||
|
||||
function errorText(error: unknown, fallback: string) {
|
||||
@@ -1129,6 +1165,10 @@ function dishPayloadForSave() {
|
||||
}
|
||||
|
||||
async function saveDishCategory() {
|
||||
if (!dishCategoryFormValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await run(async () => {
|
||||
const payload = dishCategoryPayloadForSave();
|
||||
if (dishCategoryForm.value.id) {
|
||||
@@ -1142,6 +1182,10 @@ async function saveDishCategory() {
|
||||
}
|
||||
|
||||
async function saveDish() {
|
||||
if (!dishFormValid.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await run(async () => {
|
||||
const payload = dishPayloadForSave();
|
||||
if (dishForm.value.id) {
|
||||
@@ -2537,20 +2581,7 @@ onMounted(() => {
|
||||
<strong>{{ editingUser.displayName }}</strong>
|
||||
<span class="meta-line">{{ editingUser.email }}</span>
|
||||
</div>
|
||||
<div class="permission-grid" role="group" :aria-label="t('pages.admin.roles')">
|
||||
<label v-for="role in roleRows" :key="role.id" class="permission-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="userRoleForm.roleIds.includes(role.id)"
|
||||
:disabled="busy || !role.enabled"
|
||||
@change="toggleUserRole(role.id)"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ role.name }}</strong>
|
||||
<small>{{ role.description }}</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<SwitchGroup id="admin-user-roles" v-model="userRoleSwitchValue" :label="t('pages.admin.roles')" :options="userRoleSwitchOptions" layout="grid" />
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
@@ -2607,22 +2638,14 @@ onMounted(() => {
|
||||
<span class="meta-line">{{ editingRole.description }}</span>
|
||||
</div>
|
||||
<div class="permission-groups">
|
||||
<section v-for="group in permissionGroups" :key="group.category" class="permission-group">
|
||||
<h3>{{ group.category }}</h3>
|
||||
<div class="permission-grid" role="group" :aria-label="group.category">
|
||||
<label v-for="permission in group.permissions" :key="permission.id" class="permission-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="rolePermissionForm.permissionIds.includes(permission.id)"
|
||||
:disabled="busy || !permission.enabled"
|
||||
@change="toggleRolePermission(permission.id)"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ permission.name }}</strong>
|
||||
<small>{{ permission.key }}</small>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<section v-for="(group, index) in permissionGroups" :key="group.category" class="permission-group">
|
||||
<SwitchGroup
|
||||
:id="`admin-role-permissions-${index}`"
|
||||
v-model="rolePermissionSwitchValue"
|
||||
:label="group.category"
|
||||
:options="permissionSwitchOptions(group.permissions)"
|
||||
layout="grid"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
@@ -2713,10 +2736,14 @@ onMounted(() => {
|
||||
/>
|
||||
<div class="field">
|
||||
<label for="dish-category-cookware">{{ t('pages.dish.cookware') }}</label>
|
||||
<select id="dish-category-cookware" v-model="dishCategoryForm.cookwareItemId" required>
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="item in dishItemRows" :key="`cookware-${item.id}`" :value="String(item.id)">{{ item.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-category-cookware"
|
||||
v-model="dishCategoryForm.cookwareItemId"
|
||||
:options="dishItemSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.select')"
|
||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dish-category-total-material-quantity">{{ t('pages.dish.totalMaterialQuantity') }}</label>
|
||||
@@ -2724,10 +2751,14 @@ onMounted(() => {
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dish-category-main-material">{{ t('pages.dish.mainMaterial') }}</label>
|
||||
<select id="dish-category-main-material" v-model="dishCategoryForm.mainMaterialItemId" required>
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="item in dishItemRows" :key="`category-main-material-${item.id}`" :value="String(item.id)">{{ item.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-category-main-material"
|
||||
v-model="dishCategoryForm.mainMaterialItemId"
|
||||
:options="dishItemSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.select')"
|
||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TranslationFields
|
||||
@@ -2742,7 +2773,7 @@ onMounted(() => {
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button type="submit" form="admin-dish-category-form" class="link-button" :disabled="busy">
|
||||
<button type="submit" form="admin-dish-category-form" class="link-button" :disabled="busy || !dishCategoryFormValid">
|
||||
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||
{{ busy ? t('common.saving') : t('common.save') }}
|
||||
</button>
|
||||
@@ -2758,47 +2789,71 @@ onMounted(() => {
|
||||
<div class="dish-form-row dish-form-row--3">
|
||||
<div class="field">
|
||||
<label for="dish-category">{{ t('pages.dish.category') }}</label>
|
||||
<select id="dish-category" v-model="dishForm.categoryId" required>
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="category in dishCategoryRows" :key="`dish-category-option-${category.id}`" :value="String(category.id)">{{ category.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-category"
|
||||
v-model="dishForm.categoryId"
|
||||
:options="dishCategorySelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.select')"
|
||||
:search-placeholder="t('pages.dish.category')"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dish-item">{{ t('pages.dish.dishItem') }}</label>
|
||||
<select id="dish-item" v-model="dishForm.itemId" required>
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="item in dishItemRows" :key="`dish-item-${item.id}`" :value="String(item.id)">{{ item.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-item"
|
||||
v-model="dishForm.itemId"
|
||||
:options="dishItemSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.select')"
|
||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dish-flavor">{{ t('pages.dish.flavor') }}</label>
|
||||
<select id="dish-flavor" v-model="dishForm.flavorId" required>
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="flavor in dishFlavorRows" :key="`dish-flavor-${flavor.id}`" :value="String(flavor.id)">{{ flavor.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-flavor"
|
||||
v-model="dishForm.flavorId"
|
||||
:options="dishFlavorSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.select')"
|
||||
:search-placeholder="t('pages.dish.flavor')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dish-form-row dish-form-row--3">
|
||||
<div class="field">
|
||||
<label for="dish-secondary-material-1">{{ t('pages.dish.secondaryMaterial') }}</label>
|
||||
<select id="dish-secondary-material-1" v-model="dishForm.secondaryMaterialItemIds[0]">
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="item in dishItemRows" :key="`dish-secondary-material-1-${item.id}`" :value="String(item.id)">{{ item.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-secondary-material-1"
|
||||
v-model="dishForm.secondaryMaterialItemIds[0]"
|
||||
:options="optionalDishItemSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.none')"
|
||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="dishAllowsSecondSecondaryMaterial" class="field">
|
||||
<label for="dish-secondary-material-2">{{ t('pages.dish.secondSecondaryMaterial') }}</label>
|
||||
<select id="dish-secondary-material-2" v-model="dishForm.secondaryMaterialItemIds[1]">
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="item in dishItemRows" :key="`dish-secondary-material-2-${item.id}`" :value="String(item.id)">{{ item.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-secondary-material-2"
|
||||
v-model="dishForm.secondaryMaterialItemIds[1]"
|
||||
:options="optionalDishItemSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.none')"
|
||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dish-pokemon-skill">{{ t('pages.dish.pokemonSkill') }}</label>
|
||||
<select id="dish-pokemon-skill" v-model="dishForm.pokemonSkillId">
|
||||
<option value="">{{ t('common.none') }}</option>
|
||||
<option v-for="skill in dishSkillRows" :key="`dish-skill-${skill.id}`" :value="String(skill.id)">{{ skill.name }}</option>
|
||||
</select>
|
||||
<TagsSelect
|
||||
id="dish-pokemon-skill"
|
||||
v-model="dishForm.pokemonSkillId"
|
||||
:options="optionalDishSkillSelectOptions"
|
||||
:multiple="false"
|
||||
:placeholder="t('common.none')"
|
||||
:search-placeholder="t('pages.dish.pokemonSkill')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<TranslationFields
|
||||
@@ -2813,7 +2868,7 @@ onMounted(() => {
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button type="submit" form="admin-dish-form" class="link-button" :disabled="busy">
|
||||
<button type="submit" form="admin-dish-form" class="link-button" :disabled="busy || !dishFormValid">
|
||||
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||
{{ busy ? t('common.saving') : t('common.save') }}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Icon } from '@iconify/vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import LifeRatingControl from '../components/LifeRatingControl.vue';
|
||||
import LifeReactionUsersModal from '../components/LifeReactionUsersModal.vue';
|
||||
import LoadMoreSentinel from '../components/LoadMoreSentinel.vue';
|
||||
@@ -68,6 +69,8 @@ const ratingErrors = ref<Record<number, string>>({});
|
||||
const moderationBusyPostId = ref<number | null>(null);
|
||||
const moderationErrors = ref<Record<number, string>>({});
|
||||
const reactionUsersModal = ref<{ postId: number; reactionType: LifeReactionType | null } | null>(null);
|
||||
const pendingDeleteComment = ref<LifeComment | null>(null);
|
||||
const deleteConfirmBusy = ref(false);
|
||||
const lifeCommentPageSize = 20;
|
||||
const commentMaxLength = 1000;
|
||||
let removeAuthListener: (() => void) | null = null;
|
||||
@@ -707,10 +710,6 @@ function markOwnCommentDeleted(items: LifeComment[], id: number): boolean {
|
||||
}
|
||||
|
||||
async function deleteComment(comment: LifeComment) {
|
||||
if (!window.confirm(t('pages.life.deleteCommentConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = replyKey(comment.id);
|
||||
clearCommentError(key);
|
||||
|
||||
@@ -737,6 +736,33 @@ async function deleteComment(comment: LifeComment) {
|
||||
}
|
||||
}
|
||||
|
||||
function requestDeleteComment(comment: LifeComment) {
|
||||
pendingDeleteComment.value = comment;
|
||||
}
|
||||
|
||||
function closeDeleteConfirm() {
|
||||
if (deleteConfirmBusy.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingDeleteComment.value = null;
|
||||
}
|
||||
|
||||
async function confirmDeleteComment() {
|
||||
const comment = pendingDeleteComment.value;
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteConfirmBusy.value = true;
|
||||
try {
|
||||
await deleteComment(comment);
|
||||
pendingDeleteComment.value = null;
|
||||
} finally {
|
||||
deleteConfirmBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreComment(comment: LifeComment) {
|
||||
const key = replyKey(comment.id);
|
||||
commentBusyKey.value = key;
|
||||
@@ -1160,7 +1186,7 @@ onUnmounted(() => {
|
||||
class="life-icon-button life-icon-button--flat life-icon-button--danger"
|
||||
type="button"
|
||||
:aria-label="t('pages.life.deleteComment')"
|
||||
@click="deleteComment(comment)"
|
||||
@click="requestDeleteComment(comment)"
|
||||
>
|
||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||
<span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deleteComment') }}</span>
|
||||
@@ -1277,7 +1303,7 @@ onUnmounted(() => {
|
||||
class="life-icon-button life-icon-button--flat life-icon-button--danger"
|
||||
type="button"
|
||||
:aria-label="t('pages.life.deleteComment')"
|
||||
@click="deleteComment(reply)"
|
||||
@click="requestDeleteComment(reply)"
|
||||
>
|
||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||
<span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deleteComment') }}</span>
|
||||
@@ -1333,6 +1359,18 @@ onUnmounted(() => {
|
||||
<h2>{{ t('pages.life.empty') }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-if="pendingDeleteComment"
|
||||
:title="t('pages.life.deleteComment')"
|
||||
:message="t('pages.life.deleteCommentConfirm')"
|
||||
:confirm-label="t('common.delete')"
|
||||
:cancel-label="t('common.cancel')"
|
||||
:close-label="t('common.close')"
|
||||
:busy="deleteConfirmBusy"
|
||||
@cancel="closeDeleteConfirm"
|
||||
@confirm="confirmDeleteComment"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import FilterPanel from '../components/FilterPanel.vue';
|
||||
import LifeRatingControl from '../components/LifeRatingControl.vue';
|
||||
import LifeReactionUsersModal from '../components/LifeReactionUsersModal.vue';
|
||||
@@ -63,6 +64,7 @@ type LifeCommentPageState = {
|
||||
|
||||
type LifePostSort = 'latest' | 'oldest' | 'top-rated';
|
||||
type LifeFeedScope = 'all' | 'following';
|
||||
type PendingLifeDelete = { type: 'post'; post: LifePost } | { type: 'comment'; post: LifePost; comment: LifeComment };
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
const posts = ref<LifePost[]>([]);
|
||||
@@ -105,6 +107,8 @@ const ratingErrors = ref<Record<number, string>>({});
|
||||
const moderationBusyPostId = ref<number | null>(null);
|
||||
const moderationErrors = ref<Record<number, string>>({});
|
||||
const reactionUsersModal = ref<{ postId: number; reactionType: LifeReactionType | null } | null>(null);
|
||||
const pendingDelete = ref<PendingLifeDelete | null>(null);
|
||||
const deleteConfirmBusy = ref(false);
|
||||
const bodyInput = ref<HTMLTextAreaElement | null>(null);
|
||||
const loadMoreSentinel = ref<HTMLElement | null>(null);
|
||||
const lifePostPageSize = 20;
|
||||
@@ -122,6 +126,12 @@ const loadMorePaused = ref(false);
|
||||
const allCategoryValue = 'all';
|
||||
const allLanguageValue = 'all';
|
||||
const allGameVersionValue = 'all';
|
||||
const deleteConfirmTitle = computed(() =>
|
||||
pendingDelete.value?.type === 'comment' ? t('pages.life.deleteComment') : t('pages.life.deletePost')
|
||||
);
|
||||
const deleteConfirmMessage = computed(() =>
|
||||
pendingDelete.value?.type === 'comment' ? t('pages.life.deleteCommentConfirm') : t('pages.life.deleteConfirm')
|
||||
);
|
||||
|
||||
type LifeInitialData = {
|
||||
options: { lifeCategories: LifeCategory[]; gameVersions: GameVersion[] } | null;
|
||||
@@ -1049,10 +1059,6 @@ function startEdit(post: LifePost) {
|
||||
}
|
||||
|
||||
async function deletePost(post: LifePost) {
|
||||
if (!window.confirm(t('pages.life.deleteConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadError.value = '';
|
||||
|
||||
try {
|
||||
@@ -1067,6 +1073,10 @@ async function deletePost(post: LifePost) {
|
||||
}
|
||||
}
|
||||
|
||||
function requestDeletePost(post: LifePost) {
|
||||
pendingDelete.value = { type: 'post', post };
|
||||
}
|
||||
|
||||
function startReply(comment: LifeComment) {
|
||||
replyTargetId.value = comment.id;
|
||||
clearCommentError(replyKey(comment.id));
|
||||
@@ -1191,10 +1201,6 @@ function markOwnCommentDeleted(comments: LifeComment[], id: number): boolean {
|
||||
}
|
||||
|
||||
async function deleteComment(post: LifePost, comment: LifeComment) {
|
||||
if (!window.confirm(t('pages.life.deleteCommentConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = replyKey(comment.id);
|
||||
clearCommentError(key);
|
||||
|
||||
@@ -1226,6 +1232,37 @@ async function deleteComment(post: LifePost, comment: LifeComment) {
|
||||
}
|
||||
}
|
||||
|
||||
function requestDeleteComment(post: LifePost, comment: LifeComment) {
|
||||
pendingDelete.value = { type: 'comment', post, comment };
|
||||
}
|
||||
|
||||
function closeDeleteConfirm() {
|
||||
if (deleteConfirmBusy.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
const target = pendingDelete.value;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteConfirmBusy.value = true;
|
||||
try {
|
||||
if (target.type === 'post') {
|
||||
await deletePost(target.post);
|
||||
} else {
|
||||
await deleteComment(target.post, target.comment);
|
||||
}
|
||||
pendingDelete.value = null;
|
||||
} finally {
|
||||
deleteConfirmBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreComment(post: LifePost, comment: LifeComment) {
|
||||
const key = replyKey(comment.id);
|
||||
commentBusyKey.value = key;
|
||||
@@ -1601,7 +1638,7 @@ onUnmounted(() => {
|
||||
class="life-icon-button life-icon-button--danger"
|
||||
type="button"
|
||||
:aria-label="t('pages.life.deletePost')"
|
||||
@click="deletePost(post)"
|
||||
@click="requestDeletePost(post)"
|
||||
>
|
||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||
<span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deletePost') }}</span>
|
||||
@@ -1896,7 +1933,7 @@ onUnmounted(() => {
|
||||
class="life-icon-button life-icon-button--flat life-icon-button--danger"
|
||||
type="button"
|
||||
:aria-label="t('pages.life.deleteComment')"
|
||||
@click="deleteComment(post, comment)"
|
||||
@click="requestDeleteComment(post, comment)"
|
||||
>
|
||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||
<span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deleteComment') }}</span>
|
||||
@@ -2013,7 +2050,7 @@ onUnmounted(() => {
|
||||
class="life-icon-button life-icon-button--flat life-icon-button--danger"
|
||||
type="button"
|
||||
:aria-label="t('pages.life.deleteComment')"
|
||||
@click="deleteComment(post, reply)"
|
||||
@click="requestDeleteComment(post, reply)"
|
||||
>
|
||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||
<span class="life-action-tooltip" role="tooltip">{{ t('pages.life.deleteComment') }}</span>
|
||||
@@ -2104,6 +2141,18 @@ onUnmounted(() => {
|
||||
{{ t('pages.life.newPost') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
v-if="pendingDelete"
|
||||
:title="deleteConfirmTitle"
|
||||
:message="deleteConfirmMessage"
|
||||
:confirm-label="t('common.delete')"
|
||||
:cancel-label="t('common.cancel')"
|
||||
:close-label="t('common.close')"
|
||||
:busy="deleteConfirmBusy"
|
||||
@cancel="closeDeleteConfirm"
|
||||
@confirm="confirmDelete"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user