feat(activities): step through the same desk list from a detail
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user