mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
fix: add date sorting support to table columns (#11198)
* fix: add date sorting support to table columns * fix: fixed lint errors, removed unnecessary non-null check * fix: European slash format should not always be preferred --------- Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
} from "prosemirror-tables";
|
||||
import { ProsemirrorHelper } from "../../utils/ProsemirrorHelper";
|
||||
import { CSVHelper } from "../../utils/csv";
|
||||
import { parseDate } from "../../utils/date";
|
||||
import { chainTransactions } from "../lib/chainTransactions";
|
||||
import {
|
||||
getAllSelectedColumns,
|
||||
@@ -297,12 +298,6 @@ export function sortTable({
|
||||
table.push(cells);
|
||||
}
|
||||
|
||||
// check if all the cells in the column are a number
|
||||
const compareAsText = table.some((row) => {
|
||||
const cell = row[index]?.textContent;
|
||||
return cell === "" ? false : isNaN(parseFloat(cell));
|
||||
});
|
||||
|
||||
const hasHeaderRow = table[0].every(
|
||||
(cell) => cell.type === state.schema.nodes.th
|
||||
);
|
||||
@@ -313,17 +308,48 @@ export function sortTable({
|
||||
// column data before sort
|
||||
const columnData = table.map((row) => row[index]?.textContent ?? "");
|
||||
|
||||
// determine sorting type: date, number, or text
|
||||
let compareAsDate = false;
|
||||
let compareAsNumber = false;
|
||||
|
||||
const nonEmptyCells = table
|
||||
.map((row) => row[index]?.textContent?.trim())
|
||||
.filter((cell): cell is string => !!cell && cell.length > 0);
|
||||
if (nonEmptyCells.length > 0) {
|
||||
// check if all non-empty cells are valid dates
|
||||
compareAsDate = nonEmptyCells.every((cell) => parseDate(cell) !== null);
|
||||
// if not dates, check if all non-empty cells are numbers
|
||||
if (!compareAsDate) {
|
||||
compareAsNumber = nonEmptyCells.every(
|
||||
(cell) => !isNaN(parseFloat(cell))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// sort table data based on column at index
|
||||
table.sort((a, b) => {
|
||||
if (compareAsText) {
|
||||
return (a[index]?.textContent ?? "").localeCompare(
|
||||
b[index]?.textContent ?? ""
|
||||
);
|
||||
const aContent = a[index]?.textContent ?? "";
|
||||
const bContent = b[index]?.textContent ?? "";
|
||||
|
||||
// empty cells always go to the end
|
||||
if (!aContent) {
|
||||
return bContent ? 1 : 0;
|
||||
}
|
||||
if (!bContent) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (compareAsDate) {
|
||||
const aDate = parseDate(aContent);
|
||||
const bDate = parseDate(bContent);
|
||||
if (aDate && bDate) {
|
||||
return aDate.getTime() - bDate.getTime();
|
||||
}
|
||||
return 0;
|
||||
} else if (compareAsNumber) {
|
||||
return parseFloat(aContent) - parseFloat(bContent);
|
||||
} else {
|
||||
return (
|
||||
parseFloat(a[index]?.textContent ?? "") -
|
||||
parseFloat(b[index]?.textContent ?? "")
|
||||
);
|
||||
return aContent.localeCompare(bContent);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
subMonths,
|
||||
subWeeks,
|
||||
subYears,
|
||||
isValid,
|
||||
parse,
|
||||
} from "date-fns";
|
||||
import {
|
||||
cs,
|
||||
@@ -33,6 +35,99 @@ import {
|
||||
zhTW,
|
||||
} from "date-fns/locale";
|
||||
import type { DateFilter } from "../types";
|
||||
import { isBrowser } from "./browser";
|
||||
|
||||
/**
|
||||
* Determines if the user's locale uses month-first date format (MM/dd).
|
||||
*
|
||||
* @returns true if locale uses MM/dd format, false for dd/MM format.
|
||||
*/
|
||||
export function usesMonthFirstFormat(): boolean {
|
||||
if (!isBrowser || typeof Intl === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format a known date and check if month comes before day
|
||||
const formatted = new Intl.DateTimeFormat(undefined, {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
}).format(new Date(2000, 11, 25)); // Dec 25, 2000
|
||||
|
||||
// If it starts with "12", month comes first
|
||||
return formatted.startsWith("12");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse a date string in various common formats.
|
||||
*
|
||||
* @param dateStr The date string to parse.
|
||||
* @returns a Date object if parsing is successful, null otherwise.
|
||||
*/
|
||||
export function parseDate(dateStr: string): Date | null {
|
||||
if (!dateStr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove any trailing alphabetic text (e.g., "Uhr", "at", "o'clock", etc.)
|
||||
const cleaned = dateStr.trim().replace(/\s*[a-zA-Z]+\s*$/, "");
|
||||
|
||||
const monthFirst = [
|
||||
"MM/dd/yyyy HH:mm:ss",
|
||||
"MM/dd/yyyy HH:mm",
|
||||
"MM/dd/yyyy",
|
||||
"MM/dd HH:mm:ss",
|
||||
"MM/dd HH:mm",
|
||||
"MM/dd",
|
||||
];
|
||||
|
||||
const dayFirst = [
|
||||
"dd/MM/yyyy HH:mm:ss",
|
||||
"dd/MM/yyyy HH:mm",
|
||||
"dd/MM/yyyy",
|
||||
"dd/MM HH:mm:ss",
|
||||
"dd/MM HH:mm",
|
||||
"dd/MM",
|
||||
];
|
||||
|
||||
// Ambiguous slash formats - order based on user's locale
|
||||
const slashFormats = usesMonthFirstFormat()
|
||||
? [...monthFirst, ...dayFirst]
|
||||
: [...dayFirst, ...monthFirst];
|
||||
|
||||
// Common date formats used in tables (with and without time, with and without year)
|
||||
const formats = [
|
||||
// ISO formats
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
"yyyy-MM-dd HH:mm",
|
||||
"yyyy-MM-dd",
|
||||
// European dot formats
|
||||
"dd.MM.yyyy HH:mm:ss",
|
||||
"dd.MM.yyyy HH:mm",
|
||||
"dd.MM.yyyy",
|
||||
"dd.MM. HH:mm:ss",
|
||||
"dd.MM. HH:mm",
|
||||
"dd.MM.",
|
||||
"d.M.yyyy HH:mm:ss",
|
||||
"d.M.yyyy HH:mm",
|
||||
"d.M.yyyy",
|
||||
"d.M. HH:mm:ss",
|
||||
"d.M. HH:mm",
|
||||
"d.M.",
|
||||
// Locale-dependent slash formats
|
||||
...slashFormats,
|
||||
];
|
||||
|
||||
const referenceDate = new Date();
|
||||
|
||||
for (const format of formats) {
|
||||
const date = parse(cleaned, format, referenceDate);
|
||||
if (isValid(date)) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function subtractDate(date: Date, period: DateFilter) {
|
||||
switch (period) {
|
||||
|
||||
Reference in New Issue
Block a user