fix: keep list menus on screen and clip long activity subtitles
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import TableColumnsMenu from '@/components/TableColumnsMenu.vue';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board';
|
||||
@@ -26,6 +26,11 @@ const emit = defineEmits<{
|
||||
bulkDelete: [];
|
||||
}>();
|
||||
|
||||
const listRoot = ref<HTMLElement | null>(null);
|
||||
/** فقط یکی از جدول/کارت در DOM باشد تا منوی انتخاب گم نشود */
|
||||
const useCards = ref(false);
|
||||
let resizeObs: ResizeObserver | null = null;
|
||||
|
||||
const rows = computed(() =>
|
||||
props.stages.flatMap((stage) =>
|
||||
stage.deals.map((deal) => ({
|
||||
@@ -42,16 +47,32 @@ function money(deal: PipelineDeal) {
|
||||
|
||||
const hideAmount = computed(() => Boolean(props.copy.hideAmount));
|
||||
const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline.deals'));
|
||||
|
||||
onMounted(() => {
|
||||
const el = listRoot.value;
|
||||
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||
resizeObs = new ResizeObserver((entries) => {
|
||||
const width = entries[0]?.contentRect.width ?? el.clientWidth;
|
||||
useCards.value = width <= 720;
|
||||
});
|
||||
resizeObs.observe(el);
|
||||
useCards.value = el.clientWidth <= 720;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObs?.disconnect();
|
||||
resizeObs = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list" dir="rtl">
|
||||
<div ref="listRoot" class="list" dir="rtl">
|
||||
<p v-if="!rows.length" class="empty">{{ copy.empty }}</p>
|
||||
|
||||
<div v-if="rows.length" class="tc-row">
|
||||
<div v-if="rows.length && !useCards" class="tc-row">
|
||||
<TableColumnsMenu :table-key="tableKey" align="end" />
|
||||
</div>
|
||||
<div v-if="rows.length" class="table-wrap">
|
||||
<div v-if="rows.length && !useCards" class="table-wrap">
|
||||
<table v-table-cols="tableKey" class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -60,7 +81,7 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
<th>مرحله</th>
|
||||
<th v-if="!hideAmount">مبلغ</th>
|
||||
<th>تاریخ</th>
|
||||
<th></th>
|
||||
<th class="menu-col">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -155,7 +176,7 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="rows.length" class="cards">
|
||||
<div v-if="rows.length && useCards" class="cards">
|
||||
<article
|
||||
v-for="row in rows"
|
||||
:key="row.deal.id"
|
||||
@@ -412,16 +433,23 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-col,
|
||||
.menu-cell {
|
||||
width: 2.5rem;
|
||||
width: 3.2rem;
|
||||
min-width: 3.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
padding: 0.2rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border-radius: var(--radius);
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -442,7 +470,7 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: none;
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
@@ -479,13 +507,4 @@ const tableKey = computed(() => (hideAmount.value ? 'pipeline.leads' : 'pipeline
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@container pipeline (max-width: 720px) {
|
||||
.table-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,19 +7,33 @@ export function cardMenuStyle(
|
||||
const buttons = Array.from(
|
||||
document.querySelectorAll<HTMLElement>(`[data-card-id="${CSS.escape(cardId)}"] .menu`),
|
||||
);
|
||||
// در نمای لیست هم جدول و هم کارت در DOM هستند؛ دکمهٔ واقعاً دیده را بگیر
|
||||
const btn =
|
||||
buttons.find((el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.width > 0 && r.height > 0;
|
||||
}) ?? buttons[0];
|
||||
if (r.width < 1 || r.height < 1) return false;
|
||||
let node: HTMLElement | null = el;
|
||||
while (node) {
|
||||
const style = getComputedStyle(node);
|
||||
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
||||
node = node.parentElement;
|
||||
}
|
||||
return true;
|
||||
}) ?? null;
|
||||
if (!btn) return null;
|
||||
const r = btn.getBoundingClientRect();
|
||||
const gap = 4;
|
||||
const left = Math.min(Math.max(8, r.left), window.innerWidth - width - 8);
|
||||
const fitsBelow = r.bottom + gap + height <= window.innerHeight;
|
||||
const top = fitsBelow ? r.bottom + gap : Math.max(8, r.top - height - gap);
|
||||
return { inset: 'auto', top: `${top}px`, left: `${left}px`, minWidth: `${width}px` };
|
||||
// inset را ست نکن؛ shorthand مقدار top/left را خنثی میکند
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
right: 'auto',
|
||||
bottom: 'auto',
|
||||
minWidth: `${width}px`,
|
||||
};
|
||||
}
|
||||
|
||||
/** بستن منو با کلیک بیرون، Escape یا اسکرول */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
@@ -661,7 +661,11 @@ async function confirmDeleteActivities() {
|
||||
|
||||
function onBoardMenu(activityId: string) {
|
||||
boardMenuId.value = boardMenuId.value === activityId ? null : activityId;
|
||||
boardMenuStyle.value = boardMenuId.value ? cardMenuStyle(activityId) : null;
|
||||
boardMenuStyle.value = null;
|
||||
if (!boardMenuId.value) return;
|
||||
void nextTick(() => {
|
||||
boardMenuStyle.value = cardMenuStyle(activityId);
|
||||
});
|
||||
}
|
||||
|
||||
const unbindBoardMenuDismiss = bindMenuDismiss(
|
||||
@@ -1332,38 +1336,40 @@ onUnmounted(() => {
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="boardMenuId"
|
||||
class="menu-pop"
|
||||
role="menu"
|
||||
:style="boardMenuStyle ?? undefined"
|
||||
@click.stop
|
||||
>
|
||||
<button type="button" role="menuitem" @click="startSelect(boardMenuId)">انتخاب</button>
|
||||
<RouterLink
|
||||
role="menuitem"
|
||||
:to="{ name: 'activity-detail', params: { id: boardMenuId } }"
|
||||
@click="boardMenuId = null"
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="boardMenuId"
|
||||
class="menu-pop"
|
||||
role="menu"
|
||||
:style="boardMenuStyle ?? undefined"
|
||||
@click.stop
|
||||
>
|
||||
مشاهده
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
role="menuitem"
|
||||
:to="{ name: 'activity-edit', params: { id: boardMenuId } }"
|
||||
@click="boardMenuId = null"
|
||||
>
|
||||
ویرایش
|
||||
</RouterLink>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="danger"
|
||||
@click="askDeleteActivity(boardMenuId)"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="boardMenuId = null">بستن</button>
|
||||
</div>
|
||||
<button type="button" role="menuitem" @click="startSelect(boardMenuId)">انتخاب</button>
|
||||
<RouterLink
|
||||
role="menuitem"
|
||||
:to="{ name: 'activity-detail', params: { id: boardMenuId } }"
|
||||
@click="boardMenuId = null"
|
||||
>
|
||||
مشاهده
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
role="menuitem"
|
||||
:to="{ name: 'activity-edit', params: { id: boardMenuId } }"
|
||||
@click="boardMenuId = null"
|
||||
>
|
||||
ویرایش
|
||||
</RouterLink>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="danger"
|
||||
@click="askDeleteActivity(boardMenuId)"
|
||||
>
|
||||
حذف
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="boardMenuId = null">بستن</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<UiModal
|
||||
:open="Boolean(activityModal)"
|
||||
@@ -1887,17 +1893,29 @@ h1 {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title-cell > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.star {
|
||||
color: #cbd5e1;
|
||||
margin-top: 0.2rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
color: inherit;
|
||||
font-weight: 600;
|
||||
overflow-wrap: anywhere;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub {
|
||||
@@ -2148,9 +2166,7 @@ time.note {
|
||||
|
||||
.menu-pop {
|
||||
position: fixed;
|
||||
inset-inline-start: 1rem;
|
||||
bottom: calc(var(--mobile-tabbar-h, 0px) + 1.75rem);
|
||||
z-index: 40;
|
||||
z-index: 80;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line, #e2e8f0);
|
||||
border-radius: var(--radius);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
@@ -559,7 +559,11 @@ const menuStyle = ref<Record<string, string> | null>(null);
|
||||
|
||||
function onMenuDeal(dealId: string) {
|
||||
menuOpenId.value = menuOpenId.value === dealId ? null : dealId;
|
||||
menuStyle.value = menuOpenId.value ? cardMenuStyle(dealId) : null;
|
||||
menuStyle.value = null;
|
||||
if (!menuOpenId.value) return;
|
||||
void nextTick(() => {
|
||||
menuStyle.value = cardMenuStyle(dealId);
|
||||
});
|
||||
}
|
||||
|
||||
const unbindMenuDismiss = bindMenuDismiss(
|
||||
@@ -702,25 +706,27 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
|
||||
<button type="button" role="menuitem" @click="startSelect(menuOpenId)">
|
||||
انتخاب
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="onOpenDeal(menuOpenId)">
|
||||
مشاهده معامله
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@click="dealModal = { dealId: menuOpenId ?? undefined }; menuOpenId = null"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
<button type="button" role="menuitem" class="danger" @click="askDeleteDeal(menuOpenId)">
|
||||
حذف
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="menuOpenId = null">بستن</button>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
|
||||
<button type="button" role="menuitem" @click="startSelect(menuOpenId)">
|
||||
انتخاب
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="onOpenDeal(menuOpenId)">
|
||||
مشاهده معامله
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@click="dealModal = { dealId: menuOpenId ?? undefined }; menuOpenId = null"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
<button type="button" role="menuitem" class="danger" @click="askDeleteDeal(menuOpenId)">
|
||||
حذف
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="menuOpenId = null">بستن</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<UiModal
|
||||
:open="showCreate && canCreatePipeline"
|
||||
@@ -891,9 +897,7 @@ defineExpose({ updateDealStage: applyUpdateDealStage });
|
||||
|
||||
.menu-pop {
|
||||
position: fixed;
|
||||
inset-inline-start: 1rem;
|
||||
bottom: calc(var(--mobile-tabbar-h) + 1.75rem);
|
||||
z-index: 40;
|
||||
z-index: 80;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
@@ -445,7 +445,11 @@ const menuStyle = ref<Record<string, string> | null>(null);
|
||||
|
||||
function onMenuCard(cardId: string) {
|
||||
menuOpenId.value = menuOpenId.value === cardId ? null : cardId;
|
||||
menuStyle.value = menuOpenId.value ? cardMenuStyle(cardId) : null;
|
||||
menuStyle.value = null;
|
||||
if (!menuOpenId.value) return;
|
||||
void nextTick(() => {
|
||||
menuStyle.value = cardMenuStyle(cardId);
|
||||
});
|
||||
}
|
||||
|
||||
const unbindMenuDismiss = bindMenuDismiss(
|
||||
@@ -558,19 +562,21 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</Teleport>
|
||||
|
||||
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
|
||||
<button type="button" role="menuitem" @click="onOpenCard(menuOpenId)">
|
||||
مشاهده کارت
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@click="cardModal = { cardId: menuOpenId ?? undefined }; menuOpenId = null"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="menuOpenId = null">بستن</button>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div v-if="menuOpenId" class="menu-pop" role="menu" :style="menuStyle ?? undefined">
|
||||
<button type="button" role="menuitem" @click="onOpenCard(menuOpenId)">
|
||||
مشاهده کارت
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
@click="cardModal = { cardId: menuOpenId ?? undefined }; menuOpenId = null"
|
||||
>
|
||||
ویرایش
|
||||
</button>
|
||||
<button type="button" role="menuitem" @click="menuOpenId = null">بستن</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<UiModal
|
||||
:open="showCreate && canCreateBoard"
|
||||
@@ -720,9 +726,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
.menu-pop {
|
||||
position: fixed;
|
||||
inset-inline-start: 1rem;
|
||||
bottom: calc(var(--mobile-tabbar-h) + 1.75rem);
|
||||
z-index: 40;
|
||||
z-index: 80;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
|
||||
Reference in New Issue
Block a user