fix(activities): show form photos in the note instead of the raw file link
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,6 +25,13 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import { canAssignToOthers } from '@/lib/assign-access';
|
||||
import { activityTypeView, typeColorStyle, useActivityTypes } from '@/lib/activity-types';
|
||||
import { brand } from '@mento/shared';
|
||||
import {
|
||||
leadNoteFileIsImage,
|
||||
noteChips,
|
||||
parseLeadFormNotes,
|
||||
type LeadNoteFile,
|
||||
type LeadNoteRow,
|
||||
} from '@/lib/lead-form-notes';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -62,6 +69,51 @@ const moreOpen = ref(false);
|
||||
const commentOpen = ref(false);
|
||||
const postponeOpen = ref(false);
|
||||
const noteOpen = ref(true);
|
||||
const noteModel = computed(() => parseLeadFormNotes(activity.value?.description));
|
||||
const noteRows = computed(() =>
|
||||
noteModel.value.mode === 'answers' ? noteModel.value.rows : ([] as LeadNoteRow[]),
|
||||
);
|
||||
const noteExtra = computed(() =>
|
||||
noteChips(
|
||||
noteModel.value.mode === 'answers' ? noteModel.value.prose : activity.value?.description,
|
||||
).filter(
|
||||
(chip) => !(chip.kind === 'text' && chip.text.replace(/\s+/g, '').trim() === 'پاسخهایفرم:'),
|
||||
),
|
||||
);
|
||||
const recoveredImages = ref<Record<string, string>>({});
|
||||
|
||||
function noteImageSrc(file: LeadNoteFile): string | null {
|
||||
if (!leadNoteFileIsImage(file)) return null;
|
||||
return file.dataUrl || recoveredImages.value[file.name] || null;
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => activity.value?.id, () => activity.value?.lead?.id, noteRows],
|
||||
async ([activityId, leadId]) => {
|
||||
const missing = noteRows.value
|
||||
.map((row) => row.file)
|
||||
.filter((file): file is LeadNoteFile => Boolean(file && !file.dataUrl && leadNoteFileIsImage(file)))
|
||||
.map((file) => file.name);
|
||||
recoveredImages.value = {};
|
||||
if (!activityId || !leadId || !missing.length) return;
|
||||
try {
|
||||
const { data } = await api.get<{ notes?: string | null }>(`/leads/${leadId}`);
|
||||
if (activity.value?.id !== activityId) return;
|
||||
const wanted = new Set(missing);
|
||||
const next: Record<string, string> = {};
|
||||
for (const row of parseLeadFormNotes(data.notes).rows) {
|
||||
const file = row.file;
|
||||
if (file?.dataUrl && wanted.has(file.name) && leadNoteFileIsImage(file)) {
|
||||
next[file.name] = file.dataUrl;
|
||||
}
|
||||
}
|
||||
recoveredImages.value = next;
|
||||
} catch {
|
||||
recoveredImages.value = {};
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const users = ref<OrgUserDto[]>([]);
|
||||
const assignUserIds = ref<string[]>([]);
|
||||
|
||||
@@ -565,8 +617,61 @@ onMounted(() => {
|
||||
<UiIcon name="chevron" :size="16" :class="{ open: noteOpen }" />
|
||||
</button>
|
||||
<div v-show="noteOpen">
|
||||
<p v-if="activity.description?.trim()">{{ activity.description.trim() }}</p>
|
||||
<p v-else-if="!imageUrls.length" class="empty">یادداشتی ثبت نشده.</p>
|
||||
<dl v-if="noteRows.length" class="note-rows">
|
||||
<div v-for="(row, index) in noteRows" :key="`${row.label}-${index}`">
|
||||
<dt>{{ row.label }}</dt>
|
||||
<dd>
|
||||
<template v-if="row.file && noteImageSrc(row.file)">
|
||||
<a class="note-shot" :href="noteImageSrc(row.file)!" target="_blank" rel="noopener">
|
||||
<img :src="noteImageSrc(row.file)!" :alt="row.file.name" />
|
||||
</a>
|
||||
<span>{{ row.file.name }}</span>
|
||||
</template>
|
||||
<span v-else-if="row.file">{{ row.file.name }}</span>
|
||||
<template v-else>
|
||||
<template v-for="(chip, chipIndex) in noteChips(row.text)" :key="chipIndex">
|
||||
<a
|
||||
v-if="chip.kind === 'link'"
|
||||
:href="chip.href"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ chip.label }}</a>
|
||||
<a
|
||||
v-else-if="chip.kind === 'image'"
|
||||
class="note-shot"
|
||||
:href="chip.src"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<img :src="chip.src" :alt="chip.name" />
|
||||
</a>
|
||||
<span v-else>{{ chip.text }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<p v-else-if="!noteExtra.length && !imageUrls.length" class="empty">یادداشتی ثبت نشده.</p>
|
||||
<p v-if="noteExtra.length" class="note-extra">
|
||||
<template v-for="(chip, index) in noteExtra" :key="index">
|
||||
<a
|
||||
v-if="chip.kind === 'link'"
|
||||
:href="chip.href"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ chip.label }}</a>
|
||||
<a
|
||||
v-else-if="chip.kind === 'image'"
|
||||
class="note-shot"
|
||||
:href="chip.src"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<img :src="chip.src" :alt="chip.name" />
|
||||
</a>
|
||||
<span v-else>{{ chip.text }}</span>
|
||||
</template>
|
||||
</p>
|
||||
<div v-if="imageUrls.some((image) => image.kind === 'image')" class="note-photos">
|
||||
<a
|
||||
v-for="image in imageUrls.filter((item) => item.kind === 'image')"
|
||||
@@ -993,6 +1098,46 @@ dd {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.note-rows {
|
||||
margin: 0.75rem 0 0;
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
}
|
||||
|
||||
.note-rows > div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, 12rem) minmax(0, 1fr);
|
||||
gap: 0.45rem 0.75rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.note-rows dt {
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.note-rows dd {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.note-shot {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.note-shot img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: min(280px, 100%);
|
||||
max-height: 220px;
|
||||
object-fit: contain;
|
||||
border-radius: 0.6rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.note-photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user