From 60ac3fe4d12f6b8afe60b8d5a631c0699d7884fd Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Sun, 1 Jun 2025 17:02:49 +0300 Subject: [PATCH] Added table sorting by clicking column heading Closes #4 --- src/views/Project.vue | 117 +++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 18 deletions(-) diff --git a/src/views/Project.vue b/src/views/Project.vue index 33fd55b..186ec4e 100644 --- a/src/views/Project.vue +++ b/src/views/Project.vue @@ -4,20 +4,23 @@
Загружаем карточки...
Нет карточек на доске.
-
+
- + - + - + @@ -47,6 +50,9 @@ const dataStore = useDataStore(); const isLoadingInitialData = ref(true); const isLoadingAttributesForAnyStory = ref(false); +const sortKey = ref(null); +const sortOrder = ref<"asc" | "desc">("asc"); + const tableHeaders = computed(() => { const headers: TableHeader[] = [ { key: "id", label: "ID", isAttribute: false }, @@ -56,11 +62,10 @@ const tableHeaders = computed(() => { const fields = dataStore.projectFieldsMap.get(props.projectData.id); if (fields) { - // Сортируем поля, возможно не нужно const sortedFields = [...fields].sort((a, b) => a.order - b.order); sortedFields.forEach((field) => { headers.push({ - key: field.name, + key: `attr_${field.id}`, label: field.name, isAttribute: true, attributeId: field.id, @@ -74,22 +79,80 @@ const userstoriesForProject = computed(() => { return dataStore.userstoriesMap.get(props.projectData.id); }); +const sortedUserstories = computed(() => { + if (!userstoriesForProject.value) { + return []; + } + if (!sortKey.value) { + return [...userstoriesForProject.value]; + } + + const sorted = [...userstoriesForProject.value]; + const currentSortKeyVal = sortKey.value; + const currentSortOrderVal = sortOrder.value; + + const headerToSortBy = tableHeaders.value.find((h) => h.key === currentSortKeyVal); + if (!headerToSortBy) { + return userstoriesForProject.value; + } + + sorted.sort((a, b) => { + let valA_raw = getCellValue(a, headerToSortBy); + let valB_raw = getCellValue(b, headerToSortBy); + + if (valA_raw === "...") valA_raw = ""; + if (valB_raw === "...") valB_raw = ""; + + const valA_is_null_or_undefined = valA_raw === null || valA_raw === undefined; + const valB_is_null_or_undefined = valB_raw === null || valB_raw === undefined; + + let comparisonResult = 0; + + if (typeof valA_raw === "number" && typeof valB_raw === "number") { + comparisonResult = valA_raw - valB_raw; + } else { + const strA = valA_is_null_or_undefined ? "" : String(valA_raw).toLowerCase(); + const strB = valB_is_null_or_undefined ? "" : String(valB_raw).toLowerCase(); + + if (strA < strB) { + comparisonResult = -1; + } else if (strA > strB) { + comparisonResult = 1; + } + } + + return currentSortOrderVal === "asc" ? comparisonResult : -comparisonResult; + }); + return sorted; +}); + watch( userstoriesForProject, async (newUserstories) => { if (newUserstories && newUserstories.length > 0) { - isLoadingAttributesForAnyStory.value = true; - const attributePromises = newUserstories - .filter((us) => !dataStore.userstoryAttributesMap.has(us.id)) - .map((us) => dataStore.fetchUserstoryAttributes(us.id)); + const storiesWithoutAttributes = newUserstories.filter((us) => !dataStore.userstoryAttributesMap.has(us.id)); + if (storiesWithoutAttributes.length > 0) { + isLoadingAttributesForAnyStory.value = true; + const attributePromises = storiesWithoutAttributes.map((us) => dataStore.fetchUserstoryAttributes(us.id)); - if (attributePromises.length > 0) { - await Promise.all(attributePromises); + try { + await Promise.all(attributePromises); + } catch (error) { + console.error(`Error loading attributes for project ${props.projectData.id}:`, error); + } finally { + const stillLoading = newUserstories.some( + (us) => !dataStore.userstoryAttributesMap.has(us.id) && storiesWithoutAttributes.find((s) => s.id === us.id), + ); + isLoadingAttributesForAnyStory.value = stillLoading; + } + } else { + isLoadingAttributesForAnyStory.value = false; } + } else { isLoadingAttributesForAnyStory.value = false; } }, - { immediate: false }, + { immediate: true, deep: true }, ); onMounted(async () => { @@ -103,21 +166,35 @@ onMounted(async () => { } }); -function getCellValue(userstory: Userstory, header: TableHeader): string | number | UserstoryStatusInfo | null { +function handleSort(headerKey: string) { + if (sortKey.value === headerKey) { + sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc"; + } else { + sortKey.value = headerKey; + sortOrder.value = "asc"; + } +} + +function getCellValue(userstory: Userstory, header: TableHeader): string | number | null { if (!header.isAttribute) { if (header.key === "status") { - return userstory.status_extra_info?.name || userstory.status.toString(); + return userstory.status_extra_info?.name || userstory.status?.toString() || ""; } - return userstory[header.key as keyof Userstory] ?? ""; + const value = userstory[header.key as keyof Userstory]; + return value ?? ""; } else { if (header.attributeId === undefined) return "N/A (no attr ID)"; const attributes = dataStore.userstoryAttributesMap.get(userstory.id); if (attributes) { // Ключи для кастомных полей приходят как строки - return attributes[header.attributeId.toString()] ?? ""; + const attrValue = attributes[header.attributeId.toString()]; + return attrValue ?? ""; } - return isLoadingAttributesForAnyStory.value ? "..." : ""; + if (isLoadingAttributesForAnyStory.value && !dataStore.userstoryAttributesMap.has(userstory.id)) { + return "..."; + } + return ""; } } @@ -136,5 +213,9 @@ function getCellValue(userstory: Userstory, header: TableHeader): string | numbe } table thead tr th { font-weight: bold; + /* cursor: pointer; is added inline for now */ +} +table thead tr th:hover { + background-color: #f2f2f2; }
{{ header.label }} + {{ header.label }} + {{ sortOrder === "asc" ? "▲" : "▼" }} +
{{ getCellValue(userstory, header) }}
Загружаем данные карточек...