feat(activities): step through the same desk list from a detail

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 14:30:17 +03:30
co-authored by Cursor
parent a0936bf19f
commit e6e90aabf3
5 changed files with 318 additions and 36 deletions
+122 -1
View File
@@ -7,6 +7,7 @@ import {
formatClockFa,
formatJalaliDay,
formatJalaliNumeric,
toFaDigits,
} from '@/lib/jalali';
import UiIcon from '@/components/UiIcon.vue';
import UserAvatar from '@/components/UserAvatar.vue';
@@ -28,7 +29,8 @@ import { brand } from '@mento/shared';
const route = useRoute();
const router = useRouter();
const ACTIVITY_LIST_QUERY = ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId'] as const;
const ACTIVITY_LIST_QUERY = ['board', 'view', 'desk', 'filter', 'stage', 'q', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due'] as const;
const NAV_API_QUERY = ['q', 'desk', 'type', 'typeDefId', 'status', 'assigneeId', 'title', 'customer', 'due'] as const;
function activitiesListLocation() {
const query: Record<string, string> = {};
@@ -296,11 +298,91 @@ async function remove() {
}
}
const navIds = ref<string[]>([]);
const deskBrowse = computed(
() => route.query.from === 'desk' && typeof route.query.nav === 'string' && route.query.nav.length > 0,
);
const navIndex = computed(() => navIds.value.indexOf(id.value));
const prevNavId = computed(() => (navIndex.value > 0 ? navIds.value[navIndex.value - 1]! : ''));
const nextNavId = computed(() =>
navIndex.value >= 0 && navIndex.value < navIds.value.length - 1
? navIds.value[navIndex.value + 1]!
: '',
);
const showNav = computed(() => deskBrowse.value && navIndex.value >= 0);
function navStorageKey(token: string) {
return `activity-nav:${token}`;
}
function readNavCache(token: string): string[] | null {
try {
const raw = sessionStorage.getItem(navStorageKey(token));
if (!raw) return null;
const parsed = JSON.parse(raw) as { ids?: unknown };
if (!Array.isArray(parsed.ids) || parsed.ids.some((item) => typeof item !== 'string')) return null;
return parsed.ids as string[];
} catch {
return null;
}
}
let navSeq = 0;
async function loadNav() {
const seq = ++navSeq;
const token = typeof route.query.nav === 'string' ? route.query.nav : '';
const columnMissing =
route.query.view === 'board' &&
(typeof route.query.stage !== 'string' || !route.query.stage || route.query.stage === 'all');
if (!deskBrowse.value || !token || columnMissing) {
navIds.value = [];
return;
}
const cached = readNavCache(token);
if (cached) {
if (seq === navSeq) navIds.value = cached;
return;
}
const params: Record<string, string> = {
filter: typeof route.query.filter === 'string' && route.query.filter ? route.query.filter : 'today',
};
for (const key of NAV_API_QUERY) {
const value = route.query[key];
if (typeof value === 'string' && value) params[key] = value;
}
if (typeof route.query.board === 'string' && route.query.board) params.boardId = route.query.board;
if (typeof route.query.stage === 'string' && route.query.stage && route.query.stage !== 'all') {
params.stageId = route.query.stage;
}
try {
const { data } = await api.get<{ ids: string[] }>('/activities/nav-ids', { params });
if (seq !== navSeq) return;
const ids = Array.isArray(data.ids) ? data.ids : [];
try {
sessionStorage.setItem(navStorageKey(token), JSON.stringify({ ids }));
} catch {
/* حالت خصوصی مرورگر */
}
navIds.value = ids;
} catch {
if (seq === navSeq) navIds.value = [];
}
}
function goNeighbor(targetId: string) {
if (!targetId || targetId === id.value) return;
commentOpen.value = false;
postponeOpen.value = false;
moreOpen.value = false;
void router.push({ name: 'activity-detail', params: { id: targetId }, query: route.query });
}
watch(id, () => {
commentOpen.value = false;
postponeOpen.value = false;
void load();
});
watch(() => route.query.nav, () => void loadNav(), { immediate: true });
onMounted(() => {
void load();
void loadUsers();
@@ -327,6 +409,15 @@ onMounted(() => {
</header>
<h1>جزئیات فعالیت</h1>
<nav v-if="showNav" class="neighbors" aria-label="فعالیت‌های همین فهرست">
<button type="button" class="neighbor" :disabled="!prevNavId" @click="goNeighbor(prevNavId)">
قبلی
</button>
<span>{{ toFaDigits(navIndex + 1) }} از {{ toFaDigits(navIds.length) }}</span>
<button type="button" class="neighbor" :disabled="!nextNavId" @click="goNeighbor(nextNavId)">
بعدی
</button>
</nav>
<p v-if="loading" class="muted">در حال بارگذاری…</p>
<p v-else-if="error && !activity" class="error">{{ error }}</p>
@@ -609,6 +700,36 @@ h1 {
font-size: 1.35rem;
}
.neighbors {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.neighbors span {
color: #64748b;
font-size: 0.82rem;
font-weight: 600;
}
.neighbor {
border: 0;
background: #fff;
color: #0f766e;
font: inherit;
font-weight: 700;
border-radius: var(--radius);
padding: 0.35rem 0.85rem;
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.05);
cursor: pointer;
}
.neighbor:disabled {
color: #94a3b8;
cursor: default;
}
.muted,
.error {
margin: 0;