fix(web): keep the activity desk usable on a phone
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -68,6 +68,8 @@ defineProps<{
|
||||
| 'pulse'
|
||||
| 'sliders'
|
||||
| 'camera'
|
||||
| 'download'
|
||||
| 'upload'
|
||||
| 'arrow-right'
|
||||
| 'arrow-left'
|
||||
| 'chevron-right'
|
||||
@@ -211,6 +213,16 @@ defineProps<{
|
||||
<path d="M14.5 4.5V9H19" />
|
||||
<path d="M9 13h6M9 16.5h4" />
|
||||
</template>
|
||||
<template v-else-if="name === 'download'">
|
||||
<path d="M12 4.5v9" />
|
||||
<path d="M8.5 10.5 12 14l3.5-3.5" />
|
||||
<path d="M6 17.5h12" />
|
||||
</template>
|
||||
<template v-else-if="name === 'upload'">
|
||||
<path d="M12 14.5v-9" />
|
||||
<path d="M8.5 8.5 12 5l3.5 3.5" />
|
||||
<path d="M6 17.5h12" />
|
||||
</template>
|
||||
<template v-else-if="name === 'bag'">
|
||||
<path d="M6.5 8h11l1 12.5H5.5L6.5 8Z" />
|
||||
<path d="M9 8V6.5A3 3 0 0 1 12 3.5 3 3 0 0 1 15 6.5V8" />
|
||||
|
||||
@@ -163,6 +163,12 @@ const board = computed<string>(() => {
|
||||
});
|
||||
const currentBoard = computed(() => boards.value.find((b) => b.id === board.value) ?? null);
|
||||
const boardMenuOpen = ref(false);
|
||||
const actionsMenuOpen = ref(false);
|
||||
const importBtn = ref<HTMLButtonElement | null>(null);
|
||||
|
||||
function triggerImport() {
|
||||
importBtn.value?.click();
|
||||
}
|
||||
const boardModal = ref<{ mode: 'create' | 'edit'; name: string; memberIds: string[] } | null>(null);
|
||||
const boardMemberQuery = ref('');
|
||||
const boardSaving = ref(false);
|
||||
@@ -965,9 +971,29 @@ function migrateLegacyDoneFilter() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const timeChipsEl = ref<HTMLElement | null>(null);
|
||||
|
||||
function revealActiveTimeChip() {
|
||||
if (!isMobile.value) return;
|
||||
const root = timeChipsEl.value;
|
||||
const active = root?.querySelector<HTMLElement>('button.on');
|
||||
if (!root || !active) return;
|
||||
const rootRect = root.getBoundingClientRect();
|
||||
const activeRect = active.getBoundingClientRect();
|
||||
const fullyVisible =
|
||||
activeRect.left >= rootRect.left - 1 && activeRect.right <= rootRect.right + 1;
|
||||
if (fullyVisible) return;
|
||||
const delta =
|
||||
activeRect.left < rootRect.left
|
||||
? activeRect.left - rootRect.left
|
||||
: activeRect.right - rootRect.right;
|
||||
root.scrollBy({ left: delta });
|
||||
}
|
||||
|
||||
let mobileQuery: MediaQueryList | null = null;
|
||||
function syncMobile() {
|
||||
isMobile.value = mobileQuery?.matches ?? false;
|
||||
void nextTick(revealActiveTimeChip);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -992,6 +1018,14 @@ watch(viewMode, () => {
|
||||
menuId.value = null;
|
||||
});
|
||||
|
||||
watch(filter, () => {
|
||||
void nextTick(revealActiveTimeChip);
|
||||
});
|
||||
|
||||
watch(counts, () => {
|
||||
void nextTick(revealActiveTimeChip);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
mobileQuery?.removeEventListener('change', syncMobile);
|
||||
unbindBoardMenuDismiss();
|
||||
@@ -1016,29 +1050,44 @@ onUnmounted(() => {
|
||||
</p>
|
||||
</div>
|
||||
<div class="desk-actions">
|
||||
<div class="desk-actions-desktop">
|
||||
<UiButton
|
||||
v-if="canManageStages"
|
||||
class="manage-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
title="انواع فعالیت و فیلدها"
|
||||
@click="typesManagerOpen = true"
|
||||
>
|
||||
<UiIcon name="sliders" :size="14" />
|
||||
انواع فعالیت و فیلدها
|
||||
<span class="manage-label">انواع فعالیت و فیلدها</span>
|
||||
</UiButton>
|
||||
<UiButton
|
||||
v-if="canManageStages"
|
||||
class="manage-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
title="مدیریت ستونها"
|
||||
@click="stagesManagerOpen = true"
|
||||
>
|
||||
<UiIcon name="settings" :size="14" />
|
||||
مدیریت ستونها
|
||||
<span class="manage-label">مدیریت ستونها</span>
|
||||
</UiButton>
|
||||
<UiButton variant="ghost" size="sm" type="button" :disabled="busy" :loading="pageBusy" @click.stop="exportExcel">
|
||||
<UiButton
|
||||
class="excel-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
title="خروجی اکسل"
|
||||
aria-label="خروجی اکسل"
|
||||
:disabled="busy"
|
||||
:loading="pageBusy"
|
||||
@click.stop="exportExcel"
|
||||
>
|
||||
<UiIcon class="excel-icon" name="download" :size="16" />
|
||||
<span class="btn-full">خروجی اکسل</span>
|
||||
<span class="btn-short">خروجی</span>
|
||||
</UiButton>
|
||||
<ExcelImportFlow
|
||||
preview-path="/activities/import/preview"
|
||||
@@ -1054,9 +1103,18 @@ onUnmounted(() => {
|
||||
@settled="onImportSettled"
|
||||
>
|
||||
<template #default="{ loading }">
|
||||
<UiButton variant="ghost" size="sm" type="button" :disabled="busy" :loading="loading">
|
||||
<UiButton
|
||||
class="excel-btn"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
title="ورود از اکسل"
|
||||
aria-label="ورود از اکسل"
|
||||
:disabled="busy"
|
||||
:loading="loading"
|
||||
>
|
||||
<UiIcon class="excel-icon" name="upload" :size="16" />
|
||||
<span class="btn-full">ورود از اکسل</span>
|
||||
<span class="btn-short">ورود</span>
|
||||
</UiButton>
|
||||
</template>
|
||||
</ExcelImportFlow>
|
||||
@@ -1105,10 +1163,100 @@ onUnmounted(() => {
|
||||
<UiIcon name="grid" :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions-more">
|
||||
<UiDropdownMenu
|
||||
:open="actionsMenuOpen"
|
||||
align="start"
|
||||
@update:open="(v) => (actionsMenuOpen = v)"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<button
|
||||
type="button"
|
||||
class="more-trigger"
|
||||
aria-label="گزینههای میز کار"
|
||||
title="گزینههای میز کار"
|
||||
@click.stop="toggle"
|
||||
>
|
||||
<UiIcon name="dots-v" :size="16" />
|
||||
</button>
|
||||
</template>
|
||||
<div class="actions-menu">
|
||||
<button type="button" class="menu-row" @click="activityModal = {}">
|
||||
<UiIcon name="plus" :size="16" />
|
||||
فعالیت جدید
|
||||
</button>
|
||||
<RouterLink class="menu-row" :to="{ name: 'activities-calendar' }">
|
||||
<UiIcon name="calendar" :size="16" />
|
||||
تقویم
|
||||
</RouterLink>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-row"
|
||||
:class="{ on: viewMode === 'tabs' }"
|
||||
@click="setViewMode('tabs')"
|
||||
>
|
||||
<UiIcon name="list" :size="16" />
|
||||
نمای تب
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="menu-row"
|
||||
:class="{ on: viewMode === 'board' }"
|
||||
@click="setViewMode('board')"
|
||||
>
|
||||
<UiIcon name="grid" :size="16" />
|
||||
نمای ستونی
|
||||
</button>
|
||||
<button type="button" class="menu-row" :disabled="busy" @click="exportExcel">
|
||||
<UiIcon name="download" :size="16" />
|
||||
خروجی اکسل
|
||||
</button>
|
||||
<button type="button" class="menu-row" :disabled="busy" @click="triggerImport">
|
||||
<UiIcon name="upload" :size="16" />
|
||||
ورود از اکسل
|
||||
</button>
|
||||
<template v-if="canManageStages">
|
||||
<hr class="board-menu-sep" />
|
||||
<button type="button" class="menu-row" @click="typesManagerOpen = true">
|
||||
<UiIcon name="sliders" :size="16" />
|
||||
انواع فعالیت و فیلدها
|
||||
</button>
|
||||
<button type="button" class="menu-row" @click="stagesManagerOpen = true">
|
||||
<UiIcon name="settings" :size="16" />
|
||||
مدیریت ستونها
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</UiDropdownMenu>
|
||||
</div>
|
||||
<div v-if="isMobile" class="import-proxy" aria-hidden="true">
|
||||
<ExcelImportFlow
|
||||
preview-path="/activities/import/preview"
|
||||
import-path="/activities/import"
|
||||
rows-import-path="/activities/import/rows"
|
||||
:client-aliases="ACTIVITY_EXCEL_ALIASES"
|
||||
:client-fields="ACTIVITY_EXCEL_FIELDS"
|
||||
:client-read-options="activityClientReadOptions"
|
||||
:disabled="busy"
|
||||
@start="onImportStart"
|
||||
@done="onImportDone"
|
||||
@fail="onImportFail"
|
||||
@settled="onImportSettled"
|
||||
>
|
||||
<template #default="{ loading }">
|
||||
<button ref="importBtn" type="button" tabindex="-1" :disabled="busy || loading">
|
||||
ورود از اکسل
|
||||
</button>
|
||||
</template>
|
||||
</ExcelImportFlow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="desk-bar">
|
||||
<div class="desk-break" aria-hidden="true"></div>
|
||||
|
||||
<div class="desk-bar" :class="{ 'has-tabs': showDeskTabs }">
|
||||
<div v-if="showDeskTabs" class="desk-tabs" role="tablist" aria-label="محدوده میز کار">
|
||||
<button
|
||||
v-for="tab in DESK_TABS"
|
||||
@@ -1128,7 +1276,7 @@ onUnmounted(() => {
|
||||
<input
|
||||
v-model="q"
|
||||
type="search"
|
||||
placeholder="جستجو در عنوان، مشتری، کارشناس، نوع فعالیت..."
|
||||
:placeholder="isMobile ? 'جستجو…' : 'جستجو در عنوان، مشتری، کارشناس، نوع فعالیت...'"
|
||||
/>
|
||||
</form>
|
||||
<div class="filter-wrap" @click.stop>
|
||||
@@ -1136,10 +1284,12 @@ onUnmounted(() => {
|
||||
class="btn ghost filter-btn"
|
||||
type="button"
|
||||
:class="{ on: activeFacetCount > 0 }"
|
||||
:title="filterBtnLabel"
|
||||
:aria-label="filterBtnLabel"
|
||||
@click="filterOpen = !filterOpen"
|
||||
>
|
||||
<UiIcon name="filter" :size="15" />
|
||||
{{ filterBtnLabel }}
|
||||
<span class="filter-label">{{ filterBtnLabel }}</span>
|
||||
<UiIcon name="chevron" :size="13" class="chev" />
|
||||
</button>
|
||||
<div v-if="filterOpen" class="popover filter-popover">
|
||||
@@ -1196,7 +1346,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-chips" role="tablist" aria-label="فیلتر زمانی">
|
||||
<div ref="timeChipsEl" class="time-chips" role="tablist" aria-label="فیلتر زمانی">
|
||||
<button
|
||||
v-for="tab in TIME_TABS"
|
||||
:key="tab.key"
|
||||
@@ -2950,6 +3100,81 @@ time.note {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.excel-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.actions-more {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desk-break {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desk-actions-desktop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.more-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actions-menu {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.actions-menu .menu-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-align: right;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border-radius: var(--radius, 10px);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
color: var(--ink, #0f172a);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.actions-menu .menu-row:hover,
|
||||
.actions-menu .menu-row.on {
|
||||
background: var(--accent-soft, #ecfdf8);
|
||||
color: var(--accent-deep, #0f766e);
|
||||
}
|
||||
|
||||
.actions-menu .menu-row:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.import-proxy {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
.crumbs,
|
||||
.desktop-only,
|
||||
@@ -2962,16 +3187,145 @@ time.note {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.desk-top,
|
||||
.page-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.desk-actions {
|
||||
width: 100%;
|
||||
margin-inline-start: 0;
|
||||
.desk-chrome {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.desk-top,
|
||||
.desk-bar {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.desk-title {
|
||||
order: 1;
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
|
||||
.desk-title .lead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desk-actions-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.actions-more {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.desk-tabs {
|
||||
order: 2;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.time-chips {
|
||||
order: 3;
|
||||
flex: 1 1 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.desk-bar :deep(.ui-dd-anchor) {
|
||||
order: 7;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
max-width: 8.5rem;
|
||||
}
|
||||
|
||||
.desk-break {
|
||||
display: block;
|
||||
order: 5;
|
||||
flex: 1 0 100%;
|
||||
height: 0;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
|
||||
.desk-actions {
|
||||
order: 8;
|
||||
flex: 0 0 30px;
|
||||
width: 30px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.desk-bar .tools,
|
||||
.desk-bar:not(.has-tabs) .tools {
|
||||
order: 6;
|
||||
flex: 1 1 0;
|
||||
grid-area: auto;
|
||||
grid-column: auto;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.desk-tabs button {
|
||||
padding: 0.16rem 0.35rem;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.desk-bar .search {
|
||||
flex: 1 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.desk-bar .filter-btn {
|
||||
flex: 0 0 30px;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.desk-bar .filter-label,
|
||||
.desk-bar .filter-btn .chev {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-chips {
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.time-chips::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-chips button {
|
||||
flex: 0 0 auto;
|
||||
height: 28px;
|
||||
padding: 0 0.25rem;
|
||||
font-size: 0.68rem;
|
||||
gap: 0.15rem;
|
||||
white-space: nowrap;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.board-trigger {
|
||||
width: auto;
|
||||
max-width: 8.5rem;
|
||||
padding-inline: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
|
||||
Reference in New Issue
Block a user