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 @@
Загружаем карточки...
Нет карточек на доске.
-
+
- | {{ header.label }} |
+
+ {{ header.label }}
+ {{ sortOrder === "asc" ? "▲" : "▼" }}
+ |
-
+
|
{{ getCellValue(userstory, header) }}
|
-
+
| Загружаем данные карточек... |
@@ -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;
}