Let people change activity fields and their own notes with photos from the detail page, open full detail from the card title, and toggle the side peek with a chevron. Co-authored-by: Cursor <cursoragent@cursor.com>
191 lines
5.4 KiB
Vue
191 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
import { nextTick, ref } from 'vue';
|
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
|
import {
|
|
Button as AButton,
|
|
Card as ACard,
|
|
Flex as AFlex,
|
|
Input as AInput,
|
|
Space as ASpace,
|
|
} from 'ant-design-vue';
|
|
import PipelineColumn from './PipelineColumn.vue';
|
|
import type { PipelineDeal, PipelineStage, PipelineUiCopy } from '@/lib/pipeline-board';
|
|
import { DEAL_BOARD_COPY } from '@/lib/pipeline-board';
|
|
|
|
withDefaults(
|
|
defineProps<{
|
|
stages: PipelineStage[];
|
|
draggingId: string | null;
|
|
dropStageId: string | null;
|
|
canManage?: boolean;
|
|
copy?: PipelineUiCopy;
|
|
selectMode?: boolean;
|
|
selectedIds?: Set<string>;
|
|
peekId?: string | null;
|
|
previewId?: string | null;
|
|
}>(),
|
|
{ copy: () => DEAL_BOARD_COPY, selectedIds: () => new Set(), peekId: null, previewId: null },
|
|
);
|
|
|
|
const emit = defineEmits<{
|
|
dragStart: [deal: PipelineDeal, event: DragEvent];
|
|
dragEnd: [];
|
|
dragOver: [stageId: string, event: DragEvent];
|
|
drop: [stageId: string, event: DragEvent, index?: number];
|
|
addDeal: [stageId: string];
|
|
openDeal: [dealId: string];
|
|
detailDeal: [dealId: string];
|
|
menuDeal: [dealId: string];
|
|
toggleSelect: [dealId: string];
|
|
bulkDelete: [];
|
|
renameStage: [stageId: string, title: string];
|
|
deleteStage: [stageId: string];
|
|
moveStage: [stageId: string, dir: -1 | 1];
|
|
columnDragStart: [stageId: string, event: DragEvent];
|
|
columnDragEnd: [];
|
|
addStage: [name: string];
|
|
loadMore: [stageId: string];
|
|
}>();
|
|
|
|
const adding = ref(false);
|
|
const newName = ref('');
|
|
const addInput = ref<{ focus: () => void } | null>(null);
|
|
|
|
const shellStyle = {
|
|
boxSizing: 'border-box' as const,
|
|
flex: '1 1 auto',
|
|
height: 0,
|
|
minHeight: 0,
|
|
display: 'flex',
|
|
flexDirection: 'column' as const,
|
|
padding: 0,
|
|
minWidth: 0,
|
|
position: 'relative' as const,
|
|
overflow: 'hidden' as const,
|
|
};
|
|
|
|
const trackStyle = {
|
|
boxSizing: 'border-box' as const,
|
|
flex: '1 1 auto',
|
|
height: 0,
|
|
minHeight: 0,
|
|
display: 'flex',
|
|
flexDirection: 'row' as const,
|
|
alignItems: 'stretch' as const,
|
|
gap: '16px',
|
|
width: '100%',
|
|
overflowX: 'auto' as const,
|
|
overflowY: 'hidden' as const,
|
|
};
|
|
|
|
async function startAdd() {
|
|
adding.value = true;
|
|
newName.value = '';
|
|
await nextTick();
|
|
addInput.value?.focus?.();
|
|
}
|
|
|
|
function cancelAdd() {
|
|
adding.value = false;
|
|
newName.value = '';
|
|
}
|
|
|
|
function submitAdd() {
|
|
const name = newName.value.trim();
|
|
if (!name) return;
|
|
emit('addStage', name);
|
|
adding.value = false;
|
|
newName.value = '';
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="pipeline-board-shell" dir="rtl" :style="shellStyle">
|
|
<div class="pipeline-board-track" :style="trackStyle">
|
|
<PipelineColumn
|
|
v-for="(stage, index) in stages"
|
|
:key="stage.id"
|
|
:stage="stage"
|
|
:dragging-id="draggingId"
|
|
:drop-active="dropStageId === stage.id"
|
|
:can-manage="canManage"
|
|
:copy="copy"
|
|
:select-mode="selectMode"
|
|
:selected-ids="selectedIds"
|
|
:peek-id="peekId"
|
|
:preview-id="previewId"
|
|
:is-first="index === 0"
|
|
:is-last="index === stages.length - 1"
|
|
@drag-start="(d, e) => emit('dragStart', d, e)"
|
|
@drag-end="emit('dragEnd')"
|
|
@drag-over="(id, e) => emit('dragOver', id, e)"
|
|
@drop="(id, e, index) => emit('drop', id, e, index)"
|
|
@add-deal="emit('addDeal', $event)"
|
|
@open-deal="emit('openDeal', $event)"
|
|
@detail-deal="emit('detailDeal', $event)"
|
|
@menu-deal="emit('menuDeal', $event)"
|
|
@toggle-select="emit('toggleSelect', $event)"
|
|
@bulk-delete="emit('bulkDelete')"
|
|
@rename="(id, title) => emit('renameStage', id, title)"
|
|
@delete="emit('deleteStage', $event)"
|
|
@move="(id, dir) => emit('moveStage', id, dir)"
|
|
@column-drag-start="(id, e) => emit('columnDragStart', id, e)"
|
|
@column-drag-end="emit('columnDragEnd')"
|
|
@load-more="emit('loadMore', $event)"
|
|
/>
|
|
|
|
<div
|
|
v-if="canManage"
|
|
:style="{
|
|
flex: '0 0 310px',
|
|
width: '310px',
|
|
alignSelf: 'stretch',
|
|
display: 'flex',
|
|
alignItems: 'flex-start',
|
|
}"
|
|
>
|
|
<a-card v-if="adding" :style="{ width: '100%' }" :body-style="{ padding: '12px' }">
|
|
<a-flex vertical :gap="12">
|
|
<a-input
|
|
ref="addInput"
|
|
v-model:value="newName"
|
|
:maxlength="80"
|
|
placeholder="نام ستون جدید…"
|
|
aria-label="نام ستون جدید"
|
|
@pressEnter="submitAdd"
|
|
@keydown.esc.prevent="cancelAdd"
|
|
/>
|
|
<a-space>
|
|
<a-button
|
|
type="primary"
|
|
:disabled="newName.trim().length < 1"
|
|
@click="submitAdd"
|
|
>
|
|
افزودن
|
|
</a-button>
|
|
<a-button @click="cancelAdd">انصراف</a-button>
|
|
</a-space>
|
|
</a-flex>
|
|
</a-card>
|
|
<a-button v-else type="dashed" block :style="{ minHeight: '52px' }" @click="startAdd">
|
|
<template #icon><PlusOutlined /></template>
|
|
افزودن ستون
|
|
</a-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pipeline-board-track {
|
|
scrollbar-width: thin;
|
|
}
|
|
.pipeline-board-track::-webkit-scrollbar {
|
|
height: 8px;
|
|
}
|
|
.pipeline-board-track::-webkit-scrollbar-thumb {
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, currentColor 28%, transparent);
|
|
}
|
|
</style>
|