mirror of
https://github.com/zadam/trilium.git
synced 2026-02-02 04:29:17 +01:00
Compare commits
7 Commits
renovate/w
...
week-note
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19781bb14c | ||
|
|
7fd2bc30cc | ||
|
|
bd7e47d8f0 | ||
|
|
457a7a03fb | ||
|
|
6e486c64f1 | ||
|
|
4f3575d765 | ||
|
|
f5f1b27754 |
@@ -7,7 +7,6 @@ import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
|
||||
@@ -28,6 +27,7 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
|
||||
@@ -148,6 +148,7 @@ export default class MobileLayout {
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
@@ -170,7 +171,6 @@ export default class MobileLayout {
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(<MobileEditorToolbar />)
|
||||
.child(new FindWidget())
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { t } from "./i18n";
|
||||
import options from "./options";
|
||||
import { isMobile } from "./utils";
|
||||
|
||||
export interface ExperimentalFeature {
|
||||
id: string;
|
||||
@@ -22,7 +21,7 @@ let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||
|
||||
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||
if (featureId === "new-layout") {
|
||||
return (isMobile() || options.is("newLayout"));
|
||||
return options.is("newLayout");
|
||||
}
|
||||
|
||||
return getEnabledFeatures().has(featureId);
|
||||
@@ -30,7 +29,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId):
|
||||
|
||||
export function getEnabledExperimentalFeatureIds() {
|
||||
const values = [ ...getEnabledFeatures().values() ];
|
||||
if (isMobile() || options.is("newLayout")) {
|
||||
if (options.is("newLayout")) {
|
||||
values.push("new-layout");
|
||||
}
|
||||
return values;
|
||||
|
||||
@@ -454,7 +454,7 @@ body.desktop .tabulator-popup-container,
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
|
||||
body.desktop .dropdown-menu .dropdown-toggle,
|
||||
body #context-menu-container .dropdown-item > span,
|
||||
body.mobile .dropdown .dropdown-submenu > span {
|
||||
@@ -462,15 +462,6 @@ body.mobile .dropdown .dropdown-submenu > span {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
body.mobile .dropdown .dropdown-submenu {
|
||||
flex-wrap: wrap;
|
||||
|
||||
& > span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item span.keyboard-shortcut,
|
||||
.dropdown-item *:not(.keyboard-shortcut) > kbd {
|
||||
flex-grow: 1;
|
||||
@@ -1539,8 +1530,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
|
||||
@media (max-width: 991px) {
|
||||
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show,
|
||||
body.mobile .dropdown.note-actions > .dropdown-menu.show {
|
||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
|
||||
position: fixed !important;
|
||||
bottom: var(--dropdown-bottom) !important;
|
||||
|
||||
@@ -29,9 +29,7 @@
|
||||
"widget-render-error": {
|
||||
"title": "فشل عرض عنصر واجهة مستخدم React مخصص"
|
||||
},
|
||||
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك.",
|
||||
"open-script-note": "فتح ملاحظة برمجية",
|
||||
"scripting-error": "خطأ في النص البرمجي المخصص: {{title}}"
|
||||
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك."
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "أضافة رابط",
|
||||
|
||||
@@ -1782,8 +1782,8 @@
|
||||
"desktop-application": "桌面应用程序",
|
||||
"native-title-bar": "原生标题栏",
|
||||
"native-title-bar-description": "对于 Windows 和 macOS,关闭原生标题栏可使应用程序看起来更紧凑。在 Linux 上,保留原生标题栏可以更好地与系统集成。",
|
||||
"background-effects": "启用背景效果",
|
||||
"background-effects-description": "为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
|
||||
"background-effects": "启用背景效果(仅适用于 Windows 11)",
|
||||
"background-effects-description": "Mica 效果为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
|
||||
"restart-app-button": "重启应用程序以查看更改",
|
||||
"zoom-factor": "缩放系数"
|
||||
},
|
||||
@@ -1802,8 +1802,7 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "创建一个新的子笔记并将其添加到地图中",
|
||||
"create-child-note-instruction": "单击地图以在该位置创建新笔记,或按 Escape 以取消。",
|
||||
"unable-to-load-map": "无法加载地图。",
|
||||
"create-child-note-text": "添加标记"
|
||||
"unable-to-load-map": "无法加载地图。"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "打开位置",
|
||||
@@ -2118,7 +2117,7 @@
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "背景效果现已推出稳定版本",
|
||||
"background_effects_message": "在 Windows 和 macOS 设备上,背景效果现在已稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。",
|
||||
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
|
||||
"background_effects_button": "启用背景效果",
|
||||
"next_theme_title": "试用新 Trilium 主题",
|
||||
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
|
||||
@@ -2254,12 +2253,5 @@
|
||||
"pages_alt": "第{{pageNumber}}页",
|
||||
"pages_loading": "加载中...",
|
||||
"layers_other": "{{count}} 层"
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "在 {{platform}} 上可用"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_other": "{{count}} 选项卡",
|
||||
"more_options": "更多选项"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1771,8 +1771,7 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "Neue Unternotiz anlegen und zur Karte hinzufügen",
|
||||
"create-child-note-instruction": "Auf die Karte klicken, um eine neue Notiz an der Stelle zu erstellen oder Escape drücken um abzubrechen.",
|
||||
"unable-to-load-map": "Karte konnte nicht geladen werden.",
|
||||
"create-child-note-text": "Marker hinzufügen"
|
||||
"unable-to-load-map": "Karte konnte nicht geladen werden."
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "Ort öffnen",
|
||||
@@ -2271,10 +2270,5 @@
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Verfügbar auf {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} Tab",
|
||||
"title_other": "{{count}} Tabs",
|
||||
"more_options": "Weitere Optionen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2285,11 +2285,5 @@
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "Disponible en {{platform}}"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_one": "{{count}} pestaña",
|
||||
"title_many": "{{count}} pestañas",
|
||||
"title_other": "{{count}} pestañas",
|
||||
"more_options": "Más opciones"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -2008,8 +2008,7 @@
|
||||
"geo-map": {
|
||||
"create-child-note-title": "新しい子ノートを作成し、マップに追加する",
|
||||
"create-child-note-instruction": "地図をクリックしてその場所に新しいノートを作成するか、Esc キーを押して閉じます。",
|
||||
"unable-to-load-map": "マップを読み込めません。",
|
||||
"create-child-note-text": "マーカーを追加"
|
||||
"unable-to-load-map": "マップを読み込めません。"
|
||||
},
|
||||
"geo-map-context": {
|
||||
"open-location": "現在位置を表示",
|
||||
@@ -2257,9 +2256,5 @@
|
||||
},
|
||||
"platform_indicator": {
|
||||
"available_on": "{{platform}} で利用可能"
|
||||
},
|
||||
"mobile_tab_switcher": {
|
||||
"title_other": "{{count}} タブ",
|
||||
"more_options": "その他のオプション"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import clsx from "clsx";
|
||||
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
import FAttribute from "../entities/fattribute";
|
||||
import FNote from "../entities/fnote";
|
||||
import { Attribute } from "../services/attribute_parser";
|
||||
@@ -41,8 +40,8 @@ type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | J
|
||||
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
|
||||
|
||||
export default function PromotedAttributes() {
|
||||
const { note, componentId, noteContext } = useNoteContext();
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
|
||||
const { note, componentId } = useNoteContext();
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
|
||||
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
|
||||
}
|
||||
|
||||
@@ -75,12 +74,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells }
|
||||
*
|
||||
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
|
||||
*/
|
||||
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
|
||||
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
|
||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||
const [ cells, setCells ] = useState<Cell[]>();
|
||||
|
||||
function refresh() {
|
||||
if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") {
|
||||
if (!note || viewType === "table") {
|
||||
setCells([]);
|
||||
return;
|
||||
}
|
||||
@@ -125,7 +124,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone
|
||||
setCells(cells);
|
||||
}
|
||||
|
||||
useEffect(refresh, [ note, viewType, noteContext ]);
|
||||
useEffect(refresh, [ note, viewType ]);
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
|
||||
refresh();
|
||||
|
||||
@@ -29,6 +29,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
const isVerticalLayout = !isHorizontalLayout;
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||
const isMobileLocal = isMobile();
|
||||
const logoRef = useRef<SVGSVGElement>(null);
|
||||
useStaticTooltip(logoRef);
|
||||
|
||||
@@ -43,7 +44,8 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
||||
</div>}
|
||||
</>}
|
||||
noDropdownListStyle
|
||||
mobileBackdrop
|
||||
onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined}
|
||||
onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined}
|
||||
>
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import clsx from "clsx";
|
||||
import server from "../../services/server";
|
||||
import { TargetedMouseEvent, VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Dayjs } from "@triliumnext/commons";
|
||||
import { Dayjs, getWeekInfo, WeekSettings } from "@triliumnext/commons";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
interface DateNotesForMonth {
|
||||
@@ -22,6 +22,7 @@ const DAYS_OF_WEEK = [
|
||||
|
||||
interface DateRangeInfo {
|
||||
weekNumbers: number[];
|
||||
weekYears: number[];
|
||||
dates: Dayjs[];
|
||||
}
|
||||
|
||||
@@ -36,19 +37,27 @@ export interface CalendarArgs {
|
||||
|
||||
export default function Calendar(args: CalendarArgs) {
|
||||
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
|
||||
const [ firstWeekOfYear ] = useTriliumOptionInt("firstWeekOfYear");
|
||||
const [ minDaysInFirstWeek ] = useTriliumOptionInt("minDaysInFirstWeek");
|
||||
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
|
||||
|
||||
const weekSettings = {
|
||||
firstDayOfWeek: firstDayOfWeekISO,
|
||||
firstWeekOfYear: firstWeekOfYear ?? 0,
|
||||
minDaysInFirstWeek: minDaysInFirstWeek ?? 4
|
||||
};
|
||||
|
||||
const date = args.date;
|
||||
const firstDay = date.startOf('month');
|
||||
const firstDayISO = firstDay.isoWeekday();
|
||||
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
|
||||
const monthInfo = getMonthInformation(date, firstDayISO, weekSettings);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
|
||||
<div className="calendar-body" data-calendar-area="month">
|
||||
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} {...args} />}
|
||||
<CurrentMonthDays firstDayOfWeekISO={firstDayOfWeekISO} {...args} />
|
||||
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} weekSettings={weekSettings} {...args} />}
|
||||
<CurrentMonthDays weekSettings={weekSettings} {...args} />
|
||||
<NextMonthDays dates={monthInfo.nextMonth.dates} {...args} />
|
||||
</div>
|
||||
</>
|
||||
@@ -67,7 +76,7 @@ function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }
|
||||
)
|
||||
}
|
||||
|
||||
function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { date: Dayjs, info: DateRangeInfo } & CalendarArgs) {
|
||||
function PreviousMonthDays({ date, info: { dates, weekNumbers, weekYears }, weekSettings, ...args }: { date: Dayjs, info: DateRangeInfo, weekSettings: WeekSettings } & CalendarArgs) {
|
||||
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
|
||||
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
|
||||
|
||||
@@ -77,27 +86,28 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { da
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarWeek date={date} weekNumber={weekNumbers[0]} {...args} />
|
||||
<CalendarWeek date={date} weekNumber={weekNumbers[0]} weekYear={weekYears[0]} {...args} />
|
||||
{dates.map(date => <CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" {...args} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) {
|
||||
function CurrentMonthDays({ date, weekSettings, ...args }: { date: Dayjs, weekSettings: WeekSettings } & CalendarArgs) {
|
||||
let dateCursor = date;
|
||||
const currentMonth = date.month();
|
||||
const items: VNode[] = [];
|
||||
const curMonthString = date.format('YYYY-MM');
|
||||
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState<DateNotesForMonth>();
|
||||
const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = weekSettings;
|
||||
|
||||
useEffect(() => {
|
||||
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
|
||||
}, [ date ]);
|
||||
|
||||
while (dateCursor.month() === currentMonth) {
|
||||
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
|
||||
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
|
||||
items.push(<CalendarWeek key={`${dateCursor.year()}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} {...args}/>)
|
||||
const { weekYear, weekNumber } = getWeekInfo(dateCursor, weekSettings);
|
||||
if (dateCursor.isoWeekday() === firstDayOfWeek) {
|
||||
items.push(<CalendarWeek key={`${weekYear}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} weekYear={weekYear} {...args}/>)
|
||||
}
|
||||
|
||||
items.push(<CalendarDay key={dateCursor.toISOString()} date={dateCursor} dateNotesForMonth={dateNotesForCurMonth} {...args} />)
|
||||
@@ -141,14 +151,8 @@ function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDat
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
|
||||
const localDate = date.local();
|
||||
|
||||
// Handle case where week is in between years.
|
||||
let year = localDate.year();
|
||||
if (localDate.month() === 11 && weekNumber === 1) year++;
|
||||
|
||||
const weekString = `${year}-W${String(weekNumber).padStart(2, '0')}`;
|
||||
function CalendarWeek({ date, weekNumber, weekYear, weekNotes, onWeekClicked }: { weekNumber: number, weekYear: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
|
||||
const weekString = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
|
||||
|
||||
if (onWeekClicked) {
|
||||
return (
|
||||
@@ -169,33 +173,33 @@ function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumb
|
||||
>{weekNumber}</span>);
|
||||
}
|
||||
|
||||
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
|
||||
export function getMonthInformation(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings) {
|
||||
return {
|
||||
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
|
||||
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
|
||||
prevMonth: getPrevMonthDays(date, firstDayISO, weekSettings),
|
||||
nextMonth: getNextMonthDays(date, weekSettings.firstDayOfWeek)
|
||||
}
|
||||
}
|
||||
|
||||
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
|
||||
function getPrevMonthDays(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings): DateRangeInfo {
|
||||
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
|
||||
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
|
||||
const daysToAdd = (firstDayISO - weekSettings.firstDayOfWeek + 7) % 7;
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
const firstDay = date.startOf('month');
|
||||
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
|
||||
const { weekYear, weekNumber } = getWeekInfo(firstDay, weekSettings);
|
||||
|
||||
// Get dates from previous month
|
||||
for (let i = daysToAdd - 1; i >= 0; i--) {
|
||||
dates.push(prevMonthLastDay.subtract(i, 'day'));
|
||||
}
|
||||
|
||||
return { weekNumbers: [ weekNumber ], dates };
|
||||
return { weekNumbers: [ weekNumber ], weekYears: [ weekYear ], dates };
|
||||
}
|
||||
|
||||
function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
|
||||
function getNextMonthDays(date: Dayjs, firstDayOfWeek: number): DateRangeInfo {
|
||||
const lastDayOfMonth = date.endOf('month');
|
||||
const lastDayISO = lastDayOfMonth.isoWeekday();
|
||||
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
|
||||
const lastDayOfUserWeek = ((firstDayOfWeek + 6 - 1) % 7) + 1;
|
||||
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
@@ -206,16 +210,5 @@ function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo
|
||||
dates.push(nextMonthFirstDay.add(i, 'day'));
|
||||
}
|
||||
}
|
||||
return { weekNumbers: [], dates };
|
||||
}
|
||||
|
||||
export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
|
||||
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
|
||||
return weekStart.isoWeek();
|
||||
}
|
||||
|
||||
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
|
||||
const currentISO = date.isoWeekday();
|
||||
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
|
||||
return date.clone().subtract(diff, "day").startOf("day");
|
||||
return { weekNumbers: [], weekYears: [], dates };
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ function PromotedAttributes({ note, componentId, noteContext }: {
|
||||
componentId: string,
|
||||
noteContext: NoteContext | undefined
|
||||
}) {
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
|
||||
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
|
||||
const [ expanded, setExpanded ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,57 +1,84 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandMappings } from "../../components/app_context";
|
||||
import contextMenu, { MenuItem } from "../../menus/context_menu";
|
||||
import branches from "../../services/branches";
|
||||
import { t } from "../../services/i18n";
|
||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
||||
import note_create from "../../services/note_create";
|
||||
import tree from "../../services/tree";
|
||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useNoteContext } from "../react/hooks";
|
||||
import { NoteContextMenu } from "../ribbon/NoteActions";
|
||||
import BasicWidget from "../basic_widget";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
|
||||
export default function MobileDetailMenu() {
|
||||
const { note, noteContext, parentComponent, ntxId } = useNoteContext();
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
const subContexts = noteContext?.getMainContext().getSubContexts() ?? [];
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<div style={{ contain: "none" }}>
|
||||
{note && (
|
||||
<NoteContextMenu
|
||||
note={note} noteContext={noteContext}
|
||||
extraItems={<>
|
||||
<FormListItem
|
||||
onClick={() => noteContext?.notePath && note_create.createNote(noteContext.notePath)}
|
||||
icon="bx bx-plus"
|
||||
>{t("mobile_detail_menu.insert_child_note")}</FormListItem>
|
||||
{helpUrl && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
icon="bx bx-help-circle"
|
||||
onClick={() => openInAppHelpFromUrl(helpUrl)}
|
||||
>{t("help-button.title")}</FormListItem>
|
||||
</>}
|
||||
{subContexts.length < 2 && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
onClick={() => parentComponent.triggerCommand("openNewNoteSplit", { ntxId })}
|
||||
icon="bx bx-dock-right"
|
||||
>{t("create_pane_button.create_new_split")}</FormListItem>
|
||||
</>}
|
||||
{!isMainContext && <>
|
||||
<FormDropdownDivider />
|
||||
<FormListItem
|
||||
icon="bx bx-x"
|
||||
onClick={() => {
|
||||
// Wait first for the context menu to be dismissed, otherwise the backdrop stays on.
|
||||
requestAnimationFrame(() => {
|
||||
parentComponent.triggerCommand("closeThisNoteSplit", { ntxId });
|
||||
});
|
||||
}}
|
||||
>{t("close_pane_button.close_this_pane")}</FormListItem>
|
||||
</>}
|
||||
<FormDropdownDivider />
|
||||
</>}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
text=""
|
||||
onClick={(e) => {
|
||||
const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
|
||||
if (!ntxId) return;
|
||||
|
||||
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
|
||||
const subContexts = noteContext.getMainContext().getSubContexts();
|
||||
const isMainContext = noteContext?.isMainContext();
|
||||
const note = noteContext.note;
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
|
||||
const items: (MenuItem<keyof CommandMappings>)[] = [
|
||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||
{ kind: "separator" },
|
||||
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
|
||||
{ kind: "separator" },
|
||||
helpUrl && {
|
||||
title: t("help-button.title"),
|
||||
uiIcon: "bx bx-help-circle",
|
||||
handler: () => openInAppHelpFromUrl(helpUrl)
|
||||
},
|
||||
{ kind: "separator" },
|
||||
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
|
||||
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
|
||||
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
|
||||
|
||||
const lastItem = items.at(-1);
|
||||
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
|
||||
items.pop();
|
||||
}
|
||||
|
||||
contextMenu.show<keyof CommandMappings>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
throw new Error("Cannot get note path to delete.");
|
||||
}
|
||||
|
||||
const branchId = await tree.getBranchIdFromUrl(notePath);
|
||||
|
||||
if (!branchId) {
|
||||
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
||||
}
|
||||
|
||||
if (await branches.deleteNotes([branchId]) && parentComponent) {
|
||||
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||
}
|
||||
} else if (command && parentComponent) {
|
||||
parentComponent.triggerCommand(command, { ntxId });
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ComponentChildren, HTMLAttributes } from "preact";
|
||||
import { CSSProperties, HTMLProps } from "preact/compat";
|
||||
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { isMobile } from "../../services/utils";
|
||||
import { useTooltip, useUniqueName } from "./hooks";
|
||||
|
||||
type DataAttributes = {
|
||||
@@ -33,10 +32,9 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
|
||||
dropdownRef?: MutableRef<BootstrapDropdown | null>;
|
||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||
titleOptions?: Partial<Tooltip.Options>;
|
||||
mobileBackdrop?: boolean;
|
||||
}
|
||||
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) {
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
const dropdownContainerRef = useRef<HTMLUListElement | null>(null);
|
||||
@@ -76,18 +74,12 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
setShown(true);
|
||||
externalOnShown?.();
|
||||
hideTooltip();
|
||||
if (mobileBackdrop && isMobile()) {
|
||||
document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover");
|
||||
}
|
||||
}, [ hideTooltip, mobileBackdrop ]);
|
||||
}, [ hideTooltip ]);
|
||||
|
||||
const onHidden = useCallback(() => {
|
||||
setShown(false);
|
||||
externalOnHidden?.();
|
||||
if (mobileBackdrop && isMobile()) {
|
||||
document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover");
|
||||
}
|
||||
}, [ mobileBackdrop ]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { ComponentChildren, RefObject } from "preact";
|
||||
import { RefObject } from "preact";
|
||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
@@ -63,7 +63,7 @@ function RevisionsButton({ note }: { note: FNote }) {
|
||||
|
||||
type ItemToFocus = "basic-properties";
|
||||
|
||||
export function NoteContextMenu({ note, noteContext, extraItems }: { note: FNote, noteContext?: NoteContext, extraItems?: ComponentChildren; }) {
|
||||
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||
const dropdownRef = useRef<BootstrapDropdown>(null);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const noteType = useNoteProperty(note, "type") ?? "";
|
||||
@@ -99,15 +99,12 @@ export function NoteContextMenu({ note, noteContext, extraItems }: { note: FNote
|
||||
dropdownRef={dropdownRef}
|
||||
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
|
||||
className="note-actions"
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
iconAction
|
||||
onHidden={() => itemToFocusRef.current = null }
|
||||
mobileBackdrop
|
||||
>
|
||||
{extraItems}
|
||||
|
||||
{isReadOnly && <>
|
||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||
@@ -280,7 +277,7 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?:
|
||||
);
|
||||
}
|
||||
|
||||
export function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
||||
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
|
||||
@@ -362,31 +362,6 @@ paths:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/notes/{noteId}/attachments:
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/components/schemas/EntityId"
|
||||
get:
|
||||
description: Returns all attachments for a note identified by its ID
|
||||
operationId: getNoteAttachments
|
||||
responses:
|
||||
"200":
|
||||
description: list of attachments
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Attachment"
|
||||
default:
|
||||
description: unexpected error
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
/notes/{noteId}/undelete:
|
||||
parameters:
|
||||
- name: noteId
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Application } from "express";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { createNote, login } from "./utils.js";
|
||||
import config from "../../src/services/config.js";
|
||||
|
||||
let app: Application;
|
||||
let token: string;
|
||||
|
||||
const USER = "etapi";
|
||||
let createdNoteId: string;
|
||||
let createdAttachmentId: string;
|
||||
|
||||
describe("etapi/get-note-attachments", () => {
|
||||
beforeAll(async () => {
|
||||
config.General.noAuthentication = false;
|
||||
const buildApp = (await (import("../../src/app.js"))).default;
|
||||
app = await buildApp();
|
||||
token = await login(app);
|
||||
|
||||
createdNoteId = await createNote(app, token);
|
||||
|
||||
// Create an attachment for the note
|
||||
const response = await supertest(app)
|
||||
.post(`/etapi/attachments`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.send({
|
||||
"ownerId": createdNoteId,
|
||||
"role": "file",
|
||||
"mime": "text/plain",
|
||||
"title": "test-attachment.txt",
|
||||
"content": "test content",
|
||||
"position": 10
|
||||
});
|
||||
createdAttachmentId = response.body.attachmentId;
|
||||
expect(createdAttachmentId).toBeTruthy();
|
||||
});
|
||||
|
||||
it("gets attachments for a note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes/${createdNoteId}/attachments`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBeGreaterThan(0);
|
||||
|
||||
const attachment = response.body[0];
|
||||
expect(attachment).toHaveProperty("attachmentId", createdAttachmentId);
|
||||
expect(attachment).toHaveProperty("ownerId", createdNoteId);
|
||||
expect(attachment).toHaveProperty("role", "file");
|
||||
expect(attachment).toHaveProperty("mime", "text/plain");
|
||||
expect(attachment).toHaveProperty("title", "test-attachment.txt");
|
||||
expect(attachment).toHaveProperty("position", 10);
|
||||
expect(attachment).toHaveProperty("blobId");
|
||||
expect(attachment).toHaveProperty("dateModified");
|
||||
expect(attachment).toHaveProperty("utcDateModified");
|
||||
expect(attachment).toHaveProperty("contentLength");
|
||||
});
|
||||
|
||||
it("returns empty array for note with no attachments", async () => {
|
||||
// Create a new note without any attachments
|
||||
const newNoteId = await createNote(app, token, "Note without attachments");
|
||||
|
||||
const response = await supertest(app)
|
||||
.get(`/etapi/notes/${newNoteId}/attachments`)
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(200);
|
||||
|
||||
expect(Array.isArray(response.body)).toBe(true);
|
||||
expect(response.body.length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns 404 for non-existent note", async () => {
|
||||
const response = await supertest(app)
|
||||
.get("/etapi/notes/nonexistentnote/attachments")
|
||||
.auth(USER, token, { "type": "basic" })
|
||||
.expect(404);
|
||||
|
||||
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
|
||||
});
|
||||
});
|
||||
@@ -259,8 +259,7 @@
|
||||
"ai-llm-title": "AI/LLM",
|
||||
"inbox-title": "收件箱",
|
||||
"command-palette": "打开命令面板",
|
||||
"zen-mode": "禅模式",
|
||||
"tab-switcher-title": "标签切换器"
|
||||
"zen-mode": "禅模式"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "新建笔记",
|
||||
|
||||
@@ -257,8 +257,7 @@
|
||||
"localization": "Sprache & Region",
|
||||
"inbox-title": "Posteingang",
|
||||
"zen-mode": "Zen-Modus",
|
||||
"command-palette": "Befehlspalette öffnen",
|
||||
"tab-switcher-title": "Tabauswahl"
|
||||
"command-palette": "Befehlspalette öffnen"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Neue Notiz",
|
||||
|
||||
@@ -259,8 +259,7 @@
|
||||
"inbox-title": "Bandeja",
|
||||
"jump-to-note-title": "Saltar a...",
|
||||
"command-palette": "Abrir paleta de comandos",
|
||||
"zen-mode": "Modo Zen",
|
||||
"tab-switcher-title": "Conmutador de pestañas"
|
||||
"zen-mode": "Modo Zen"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Nueva nota",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -344,8 +344,7 @@
|
||||
"inbox-title": "Inbox",
|
||||
"base-abstract-launcher-title": "ベース アブストラクトランチャー",
|
||||
"command-palette": "コマンドパレットを開く",
|
||||
"zen-mode": "禅モード",
|
||||
"tab-switcher-title": "タブ切り替え"
|
||||
"zen-mode": "禅モード"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "新しいノート",
|
||||
|
||||
@@ -8,12 +8,6 @@ import type { AttachmentRow } from "@triliumnext/commons";
|
||||
import type { ValidatorMap } from "./etapi-interface.js";
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
const attachments = note.getAttachments();
|
||||
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = {
|
||||
ownerId: [v.notNull, v.isNoteId],
|
||||
role: [v.notNull, v.isString],
|
||||
|
||||
@@ -2,7 +2,7 @@ import type BNote from "../becca/entities/bnote.js";
|
||||
|
||||
import attributeService from "./attributes.js";
|
||||
import cloningService from "./cloning.js";
|
||||
import { dayjs, Dayjs } from "@triliumnext/commons";
|
||||
import { dayjs, Dayjs, getFirstDayOfWeek1, getWeekInfo, WeekSettings } from "@triliumnext/commons";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import noteService from "./notes.js";
|
||||
import optionService from "./options.js";
|
||||
@@ -63,7 +63,8 @@ function getJournalNoteTitle(
|
||||
rootNote: BNote,
|
||||
timeUnit: TimeUnit,
|
||||
dateObj: Dayjs,
|
||||
number: number
|
||||
number: number,
|
||||
weekYear?: number // Optional: the week year for cross-year weeks
|
||||
) {
|
||||
const patterns = {
|
||||
year: rootNote.getOwnedLabelValue("yearPattern") || "{year}",
|
||||
@@ -79,9 +80,14 @@ function getJournalNoteTitle(
|
||||
const numberStr = number.toString();
|
||||
const ordinalStr = ordinal(dateObj);
|
||||
|
||||
// For week notes, use the weekYear if provided (handles cross-year weeks)
|
||||
const yearForDisplay = (timeUnit === "week" && weekYear !== undefined)
|
||||
? weekYear.toString()
|
||||
: dateObj.format("YYYY");
|
||||
|
||||
const allReplacements: Record<string, string> = {
|
||||
// Common date formats
|
||||
"{year}": dateObj.format("YYYY"),
|
||||
"{year}": yearForDisplay,
|
||||
|
||||
// Month related
|
||||
"{isoMonth}": dateObj.format("YYYY-MM"),
|
||||
@@ -286,6 +292,14 @@ function getMonthNote(dateStr: string, _rootNote: BNote | null = null): BNote {
|
||||
return monthNote as unknown as BNote;
|
||||
}
|
||||
|
||||
function getWeekSettings(): WeekSettings {
|
||||
return {
|
||||
firstDayOfWeek: parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10),
|
||||
firstWeekOfYear: parseInt(optionService.getOptionOrNull("firstWeekOfYear") ?? "0", 10),
|
||||
minDaysInFirstWeek: parseInt(optionService.getOptionOrNull("minDaysInFirstWeek") ?? "4", 10)
|
||||
};
|
||||
}
|
||||
|
||||
function getWeekStartDate(date: Dayjs): Dayjs {
|
||||
const firstDayISO = parseInt(optionService.getOptionOrNull("firstDayOfWeek") ?? "1", 10);
|
||||
const day = date.isoWeekday();
|
||||
@@ -294,9 +308,8 @@ function getWeekStartDate(date: Dayjs): Dayjs {
|
||||
}
|
||||
|
||||
function getWeekNumberStr(date: Dayjs): string {
|
||||
const isoYear = date.isoWeekYear();
|
||||
const isoWeekNum = date.isoWeek();
|
||||
return `${isoYear}-W${isoWeekNum.toString().padStart(2, "0")}`;
|
||||
const { weekYear, weekNumber } = getWeekInfo(date, getWeekSettings());
|
||||
return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function getWeekFirstDayNote(dateStr: string, rootNote: BNote | null = null) {
|
||||
@@ -329,17 +342,19 @@ function getWeekNote(weekStr: string, _rootNote: BNote | null = null): BNote | n
|
||||
|
||||
const [ yearStr, weekNumStr ] = weekStr.trim().split("-W");
|
||||
const weekNumber = parseInt(weekNumStr);
|
||||
const weekYear = parseInt(yearStr);
|
||||
|
||||
const firstDayOfYear = dayjs().year(parseInt(yearStr)).month(0).date(1);
|
||||
const weekStartDate = firstDayOfYear.add(weekNumber - 1, "week");
|
||||
const startDate = getWeekStartDate(weekStartDate);
|
||||
const endDate = dayjs(startDate).add(6, "day");
|
||||
// Calculate week start date based on user's first week of year settings.
|
||||
// This correctly handles cross-year weeks based on user preferences.
|
||||
const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, getWeekSettings());
|
||||
const startDate = firstDayOfWeek1.add(weekNumber - 1, "week");
|
||||
const endDate = startDate.add(6, "day");
|
||||
|
||||
const startMonth = startDate.month();
|
||||
const endMonth = endDate.month();
|
||||
|
||||
const monthNote = getMonthNote(startDate.format("YYYY-MM-DD"), rootNote);
|
||||
const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber);
|
||||
const noteTitle = getJournalNoteTitle(rootNote, "week", startDate, weekNumber, weekYear);
|
||||
|
||||
sql.transactional(() => {
|
||||
weekNote = createNote(monthNote, noteTitle);
|
||||
|
||||
@@ -41,9 +41,7 @@
|
||||
"search_title": "البحث القوي",
|
||||
"web_clipper_title": "اداة قص الويب",
|
||||
"title": "الانتاجية والسلامة",
|
||||
"jump_to_title": "الاوامر والبحث السريع",
|
||||
"revisions_content": "تُحفظ الملاحظات دوريًا في الخلفية، ويمكن استخدام التعديلات للمراجعة أو للتراجع عن التغييرات غير المقصودة. كما يمكن إنشاء التعديلات عند الطلب.",
|
||||
"sync_content": "استخدم نسخة مستضافة ذاتيًا أو نسخة سحابية لمزامنة ملاحظاتك بسهولة عبر أجهزة متعددة، وللوصول إليها من هاتفك المحمول باستخدام تطبيق ويب تقدمي (PWA)."
|
||||
"jump_to_title": "الاوامر والبحث السريع"
|
||||
},
|
||||
"note_types": {
|
||||
"canvas_title": "مساحة العمل",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
337
docs/README-ga.md
vendored
337
docs/README-ga.md
vendored
@@ -1,337 +0,0 @@
|
||||
<div align="center">
|
||||
<sup>Special thanks to:</sup><br />
|
||||
<a href="https://go.warp.dev/Trilium" target="_blank">
|
||||
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png"><br />
|
||||
Warp, built for coding with multiple AI agents<br />
|
||||
</a>
|
||||
<sup>Available for macOS, Linux and Windows</sup>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
# Trilium Notes
|
||||
|
||||

|
||||
\
|
||||

|
||||
\
|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
|
||||
[](https://hosted.weblate.org/engage/trilium/)
|
||||
|
||||
<!-- translate:off -->
|
||||
<!-- LANGUAGE SWITCHER -->
|
||||
[Chinese (Simplified Han script)](./README-ZH_CN.md) | [Chinese (Traditional Han
|
||||
script)](./README-ZH_TW.md) | [English](../README.md) | [French](./README-fr.md)
|
||||
| [German](./README-de.md) | [Greek](./README-el.md) | [Italian](./README-it.md)
|
||||
| [Japanese](./README-ja.md) | [Romanian](./README-ro.md) |
|
||||
[Spanish](./README-es.md)
|
||||
<!-- translate:on -->
|
||||
|
||||
Trilium Notes is a free and open-source, cross-platform hierarchical note taking
|
||||
application with focus on building large personal knowledge bases.
|
||||
|
||||
<img src="./app.png" alt="Trilium Screenshot" width="1000">
|
||||
|
||||
## ⏬ Download
|
||||
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) –
|
||||
stable version, recommended for most users.
|
||||
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) –
|
||||
unstable development version, updated daily with the latest features and
|
||||
fixes.
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
**Visit our comprehensive documentation at
|
||||
[docs.triliumnotes.org](https://docs.triliumnotes.org/)**
|
||||
|
||||
Our documentation is available in multiple formats:
|
||||
- **Online Documentation**: Browse the full documentation at
|
||||
[docs.triliumnotes.org](https://docs.triliumnotes.org/)
|
||||
- **In-App Help**: Press `F1` within Trilium to access the same documentation
|
||||
directly in the application
|
||||
- **GitHub**: Navigate through the [User Guide](./User%20Guide/User%20Guide/) in
|
||||
this repository
|
||||
|
||||
### Quick Links
|
||||
- [Getting Started Guide](https://docs.triliumnotes.org/)
|
||||
- [Installation Instructions](https://docs.triliumnotes.org/user-guide/setup)
|
||||
- [Docker
|
||||
Setup](https://docs.triliumnotes.org/user-guide/setup/server/installation/docker)
|
||||
- [Upgrading
|
||||
TriliumNext](https://docs.triliumnotes.org/user-guide/setup/upgrading)
|
||||
- [Basic Concepts and
|
||||
Features](https://docs.triliumnotes.org/user-guide/concepts/notes)
|
||||
- [Patterns of Personal Knowledge
|
||||
Base](https://docs.triliumnotes.org/user-guide/misc/patterns-of-personal-knowledge)
|
||||
|
||||
## 🎁 Features
|
||||
|
||||
* Notes can be arranged into arbitrarily deep tree. Single note can be placed
|
||||
into multiple places in the tree (see
|
||||
[cloning](https://docs.triliumnotes.org/user-guide/concepts/notes/cloning))
|
||||
* Rich WYSIWYG note editor including e.g. tables, images and
|
||||
[math](https://docs.triliumnotes.org/user-guide/note-types/text) with markdown
|
||||
[autoformat](https://docs.triliumnotes.org/user-guide/note-types/text/markdown-formatting)
|
||||
* Support for editing [notes with source
|
||||
code](https://docs.triliumnotes.org/user-guide/note-types/code), including
|
||||
syntax highlighting
|
||||
* Fast and easy [navigation between
|
||||
notes](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation),
|
||||
full text search and [note
|
||||
hoisting](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
|
||||
* Seamless [note
|
||||
versioning](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
|
||||
* Note
|
||||
[attributes](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes)
|
||||
can be used for note organization, querying and advanced
|
||||
[scripting](https://docs.triliumnotes.org/user-guide/scripts)
|
||||
* UI available in English, German, Spanish, French, Romanian, and Chinese
|
||||
(simplified and traditional)
|
||||
* Direct [OpenID and TOTP
|
||||
integration](https://docs.triliumnotes.org/user-guide/setup/server/mfa) for
|
||||
more secure login
|
||||
* [Synchronization](https://docs.triliumnotes.org/user-guide/setup/synchronization)
|
||||
with self-hosted sync server
|
||||
* there are [3rd party services for hosting synchronisation
|
||||
server](https://docs.triliumnotes.org/user-guide/setup/server/cloud-hosting)
|
||||
* [Sharing](https://docs.triliumnotes.org/user-guide/advanced-usage/sharing)
|
||||
(publishing) notes to public internet
|
||||
* Strong [note
|
||||
encryption](https://docs.triliumnotes.org/user-guide/concepts/notes/protected-notes)
|
||||
with per-note granularity
|
||||
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type
|
||||
"canvas")
|
||||
* [Relation
|
||||
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
|
||||
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
|
||||
for visualizing notes and their relations
|
||||
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
||||
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with
|
||||
location pins and GPX tracks
|
||||
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced
|
||||
showcases](https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases)
|
||||
* [REST API](https://docs.triliumnotes.org/user-guide/advanced-usage/etapi) for
|
||||
automation
|
||||
* Scales well in both usability and performance upwards of 100 000 notes
|
||||
* Touch optimized [mobile
|
||||
frontend](https://docs.triliumnotes.org/user-guide/setup/mobile-frontend) for
|
||||
smartphones and tablets
|
||||
* Built-in [dark
|
||||
theme](https://docs.triliumnotes.org/user-guide/concepts/themes), support for
|
||||
user themes
|
||||
* [Evernote](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote)
|
||||
and [Markdown import &
|
||||
export](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
|
||||
* [Web Clipper](https://docs.triliumnotes.org/user-guide/setup/web-clipper) for
|
||||
easy saving of web content
|
||||
* Customizable UI (sidebar buttons, user-defined widgets, ...)
|
||||
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics),
|
||||
along with a Grafana Dashboard.
|
||||
|
||||
✨ Check out the following third-party resources/communities for more TriliumNext
|
||||
related goodies:
|
||||
|
||||
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party
|
||||
themes, scripts, plugins and more.
|
||||
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
|
||||
|
||||
## ❓Why TriliumNext?
|
||||
|
||||
The original Trilium developer ([Zadam](https://github.com/zadam)) has
|
||||
graciously given the Trilium repository to the community project which resides
|
||||
at https://github.com/TriliumNext
|
||||
|
||||
### ⬆️Migrating from Zadam/Trilium?
|
||||
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to
|
||||
a TriliumNext/Trilium instance. Simply [install
|
||||
TriliumNext/Trilium](#-installation) as usual and it will use your existing
|
||||
database.
|
||||
|
||||
Versions up to and including
|
||||
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
|
||||
compatible with the latest zadam/trilium version of
|
||||
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later
|
||||
versions of TriliumNext/Trilium have their sync versions incremented which
|
||||
prevents direct migration.
|
||||
|
||||
## 💬 Discuss with us
|
||||
|
||||
Feel free to join our official conversations. We would love to hear what
|
||||
features, suggestions, or issues you may have!
|
||||
|
||||
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous
|
||||
discussions.)
|
||||
- The `General` Matrix room is also bridged to
|
||||
[XMPP](xmpp:discuss@trilium.thisgreat.party?join)
|
||||
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For
|
||||
asynchronous discussions.)
|
||||
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug
|
||||
reports and feature requests.)
|
||||
|
||||
## 🏗 Installation
|
||||
|
||||
### Windows / MacOS
|
||||
|
||||
Download the binary release for your platform from the [latest release
|
||||
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
|
||||
and run the `trilium` executable.
|
||||
|
||||
### Linux
|
||||
|
||||
If your distribution is listed in the table below, use your distribution's
|
||||
package.
|
||||
|
||||
[](https://repology.org/project/triliumnext/versions)
|
||||
|
||||
You may also download the binary release for your platform from the [latest
|
||||
release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the
|
||||
package and run the `trilium` executable.
|
||||
|
||||
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
|
||||
|
||||
### Browser (any OS)
|
||||
|
||||
If you use a server installation (see below), you can directly access the web
|
||||
interface (which is almost identical to the desktop app).
|
||||
|
||||
Currently only the latest versions of Chrome & Firefox are supported (and
|
||||
tested).
|
||||
|
||||
### Mobile
|
||||
|
||||
To use TriliumNext on a mobile device, you can use a mobile web browser to
|
||||
access the mobile interface of a server installation (see below).
|
||||
|
||||
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more
|
||||
information on mobile app support.
|
||||
|
||||
If you prefer a native Android app, you can use
|
||||
[TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
|
||||
Report bugs and missing features at [their
|
||||
repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
|
||||
disable automatic updates on your server installation (see below) when using
|
||||
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
|
||||
|
||||
### Server
|
||||
|
||||
To install TriliumNext on your own server (including via Docker from
|
||||
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
|
||||
installation docs](https://docs.triliumnotes.org/user-guide/setup/server).
|
||||
|
||||
|
||||
## 💻 Contribute
|
||||
|
||||
### Translations
|
||||
|
||||
If you are a native speaker, help us translate Trilium by heading over to our
|
||||
[Weblate page](https://hosted.weblate.org/engage/trilium/).
|
||||
|
||||
Here's the language coverage we have so far:
|
||||
|
||||
[](https://hosted.weblate.org/engage/trilium/)
|
||||
|
||||
### Code
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the
|
||||
server (available at http://localhost:8080):
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
Download the repository, install dependencies using `pnpm` and then run the
|
||||
environment required to edit the documentation:
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm edit-docs:edit-docs
|
||||
```
|
||||
|
||||
### Building the Executable
|
||||
Download the repository, install dependencies using `pnpm` and then build the
|
||||
desktop app for Windows:
|
||||
```shell
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
For more details, see the [development
|
||||
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
Please view the [documentation
|
||||
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
|
||||
for details. If you have more questions, feel free to reach out via the links
|
||||
described in the "Discuss with us" section above.
|
||||
|
||||
## 👏 Shoutouts
|
||||
|
||||
* [zadam](https://github.com/zadam) for the original concept and implementation
|
||||
of the application.
|
||||
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
|
||||
application icon.
|
||||
* [nriver](https://github.com/nriver) for his work on internationalization.
|
||||
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
|
||||
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
|
||||
widget.
|
||||
* [Dosu](https://dosu.dev/) for providing us with the automated responses to
|
||||
GitHub issues and discussions.
|
||||
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
|
||||
|
||||
Trilium would not be possible without the technologies behind it:
|
||||
|
||||
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind
|
||||
text notes. We are grateful for being offered a set of the premium features.
|
||||
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with
|
||||
support for huge amount of languages.
|
||||
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
|
||||
whiteboard used in Canvas notes.
|
||||
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
|
||||
mind map functionality.
|
||||
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
|
||||
maps.
|
||||
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
|
||||
table used in collections.
|
||||
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
|
||||
without real competition.
|
||||
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
|
||||
Used in [relation
|
||||
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
|
||||
[link
|
||||
maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
|
||||
|
||||
## 🤝 Support
|
||||
|
||||
Trilium is built and maintained with [hundreds of hours of
|
||||
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your
|
||||
support keeps it open-source, improves features, and covers costs such as
|
||||
hosting.
|
||||
|
||||
Consider supporting the main developer
|
||||
([eliandoran](https://github.com/eliandoran)) of the application via:
|
||||
|
||||
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
|
||||
- [PayPal](https://paypal.me/eliandoran)
|
||||
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
|
||||
|
||||
## 🔑 License
|
||||
|
||||
Copyright 2017-2025 zadam, Elian Doran, and other contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option) any
|
||||
later version.
|
||||
@@ -39,7 +39,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.3"
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.3"
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.3"
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.3"
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"vite-plugin-svgo": "2.0.0",
|
||||
"vitest": "4.0.18",
|
||||
"webdriverio": "9.23.3"
|
||||
"webdriverio": "9.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ckeditor5": "47.4.0"
|
||||
|
||||
@@ -13,3 +13,4 @@ export * from "./lib/attribute_names.js";
|
||||
export * from "./lib/utils.js";
|
||||
export * from "./lib/dayjs.js";
|
||||
export * from "./lib/notes.js";
|
||||
export * from "./lib/week_utils.js";
|
||||
|
||||
210
packages/commons/src/lib/week_utils.spec.ts
Normal file
210
packages/commons/src/lib/week_utils.spec.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dayjs } from "./dayjs.js";
|
||||
import { getWeekInfo, getFirstDayOfWeek1, getWeekString, WeekSettings, DEFAULT_WEEK_SETTINGS } from "./week_utils.js";
|
||||
|
||||
describe("week_utils", () => {
|
||||
describe("getWeekInfo", () => {
|
||||
describe("with firstWeekOfYear=0 (first week contains first day of year)", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
it("2025-12-29 should be 2026-W01 (cross-year week)", () => {
|
||||
// 2026-01-01 is Thursday, so the week containing it starts on 2025-12-29 (Monday)
|
||||
// This week should be 2026-W01 because it contains 2026-01-01
|
||||
const result = getWeekInfo(dayjs("2025-12-29"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("2026-01-01 should be 2026-W01", () => {
|
||||
const result = getWeekInfo(dayjs("2026-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("2025-12-28 should be 2025-W52", () => {
|
||||
// 2025-12-28 is Sunday, which is the last day of the week starting 2025-12-22
|
||||
const result = getWeekInfo(dayjs("2025-12-28"), settings);
|
||||
expect(result.weekYear).toBe(2025);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2026-01-05 should be 2026-W02", () => {
|
||||
// 2026-01-05 is Monday, start of second week
|
||||
const result = getWeekInfo(dayjs("2026-01-05"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
it("2026-02-01 (Sunday) should be 2026-W05 (still in week 5)", () => {
|
||||
// Feb 1, 2026 is Sunday - with Monday as first day, this is the last day of week 5
|
||||
// Week 5 starts on 2026-01-26 (Mon) and ends on 2026-02-01 (Sun)
|
||||
const result = getWeekInfo(dayjs("2026-02-01"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(5);
|
||||
});
|
||||
|
||||
it("2026-02-02 (Monday) should be 2026-W06 (start of week 6)", () => {
|
||||
// Feb 2, 2026 is Monday - start of week 6
|
||||
const result = getWeekInfo(dayjs("2026-02-02"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with firstDayOfWeek=7 (Sunday as first day)", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 7, // Sunday
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
it("2026-02-01 (Sunday) should be 2026-W06 (start of new week)", () => {
|
||||
// Feb 1, 2026 is Sunday - should be the START of week 6, not end of week 5
|
||||
const result = getWeekInfo(dayjs("2026-02-01"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(6);
|
||||
});
|
||||
|
||||
it("2026-01-31 (Saturday) should be 2026-W05 (last day of week 5)", () => {
|
||||
// Jan 31, 2026 is Saturday - should be the last day of week 5
|
||||
const result = getWeekInfo(dayjs("2026-01-31"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(5);
|
||||
});
|
||||
|
||||
it("2026-01-25 (Sunday) should be 2026-W05 (start of week 5)", () => {
|
||||
// Jan 25, 2026 is Sunday - week 5 starts here
|
||||
const result = getWeekInfo(dayjs("2026-01-25"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with firstWeekOfYear=1 (ISO standard, first week contains first Thursday)", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 1,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
it("2023-01-01 should be 2022-W52 (Jan 1 is Sunday)", () => {
|
||||
// 2023-01-01 is Sunday, so the week starts on 2022-12-26
|
||||
// Since this week doesn't contain Jan 4, it's 2022-W52
|
||||
const result = getWeekInfo(dayjs("2023-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2022);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2023-01-02 should be 2023-W01 (first Monday)", () => {
|
||||
const result = getWeekInfo(dayjs("2023-01-02"), settings);
|
||||
expect(result.weekYear).toBe(2023);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with firstWeekOfYear=2 (minimum days in first week)", () => {
|
||||
// 2026-01-01 is Thursday
|
||||
// The week containing Jan 1 starts on 2025-12-29 (Monday)
|
||||
// This week has 4 days in 2026 (Thu, Fri, Sat, Sun = Jan 1-4)
|
||||
|
||||
describe("with minDaysInFirstWeek=1", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 2,
|
||||
minDaysInFirstWeek: 1
|
||||
};
|
||||
|
||||
it("2025-12-29 should be 2026-W01 (4 days >= 1 minimum)", () => {
|
||||
// Week has 4 days in 2026, which is >= 1
|
||||
const result = getWeekInfo(dayjs("2025-12-29"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
|
||||
it("2026-01-01 should be 2026-W01", () => {
|
||||
const result = getWeekInfo(dayjs("2026-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with minDaysInFirstWeek=7", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 2,
|
||||
minDaysInFirstWeek: 7
|
||||
};
|
||||
|
||||
it("2025-12-29 should be 2025-W52 (4 days < 7 minimum, so this is last week of 2025)", () => {
|
||||
// Week has only 4 days in 2026, which is < 7
|
||||
// So this week belongs to 2025
|
||||
const result = getWeekInfo(dayjs("2025-12-29"), settings);
|
||||
expect(result.weekYear).toBe(2025);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2026-01-01 should be 2025-W52 (still last week of 2025)", () => {
|
||||
const result = getWeekInfo(dayjs("2026-01-01"), settings);
|
||||
expect(result.weekYear).toBe(2025);
|
||||
expect(result.weekNumber).toBe(52);
|
||||
});
|
||||
|
||||
it("2026-01-05 should be 2026-W01 (first full week of 2026)", () => {
|
||||
// 2026-01-05 is Monday, start of the first full week
|
||||
const result = getWeekInfo(dayjs("2026-01-05"), settings);
|
||||
expect(result.weekYear).toBe(2026);
|
||||
expect(result.weekNumber).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFirstDayOfWeek1", () => {
|
||||
it("with firstWeekOfYear=0, returns the first day of the week containing Jan 1", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
// 2026-01-01 is Thursday, so week starts on 2025-12-29
|
||||
const result = getFirstDayOfWeek1(2026, settings);
|
||||
expect(result.format("YYYY-MM-DD")).toBe("2025-12-29");
|
||||
});
|
||||
|
||||
it("with firstWeekOfYear=1, returns the first day of the week containing Jan 4", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 1,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
// 2023-01-04 is Wednesday, so week starts on 2023-01-02
|
||||
const result = getFirstDayOfWeek1(2023, settings);
|
||||
expect(result.format("YYYY-MM-DD")).toBe("2023-01-02");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWeekString", () => {
|
||||
it("generates correct week string for cross-year week", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
expect(getWeekString(dayjs("2025-12-29"), settings)).toBe("2026-W01");
|
||||
});
|
||||
|
||||
it("generates correct week string with padded week number", () => {
|
||||
const settings: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
expect(getWeekString(dayjs("2026-01-05"), settings)).toBe("2026-W02");
|
||||
});
|
||||
});
|
||||
});
|
||||
143
packages/commons/src/lib/week_utils.ts
Normal file
143
packages/commons/src/lib/week_utils.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { dayjs, Dayjs } from "./dayjs.js";
|
||||
|
||||
/**
|
||||
* Week settings for calculating week numbers.
|
||||
*/
|
||||
export interface WeekSettings {
|
||||
/** First day of the week (1=Monday to 7=Sunday) */
|
||||
firstDayOfWeek: number;
|
||||
/**
|
||||
* How to determine the first week of the year:
|
||||
* - 0: First week contains first day of the year
|
||||
* - 1: First week contains first Thursday (ISO 8601 standard)
|
||||
* - 2: First week has minimum days
|
||||
*/
|
||||
firstWeekOfYear: number;
|
||||
/** Minimum days in first week (used when firstWeekOfYear=2) */
|
||||
minDaysInFirstWeek: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default week settings (first week contains first day of year, week starts on Monday).
|
||||
*/
|
||||
export const DEFAULT_WEEK_SETTINGS: WeekSettings = {
|
||||
firstDayOfWeek: 1,
|
||||
firstWeekOfYear: 0,
|
||||
minDaysInFirstWeek: 4
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the first day of week 1 for a given year, based on user settings.
|
||||
*
|
||||
* @param year The year to calculate for
|
||||
* @param settings Week calculation settings
|
||||
* @returns The first day of week 1
|
||||
*/
|
||||
export function getFirstDayOfWeek1(year: number, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): Dayjs {
|
||||
const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = settings;
|
||||
|
||||
const jan1 = dayjs(`${year}-01-01`);
|
||||
const jan1Weekday = jan1.isoWeekday(); // 1=Monday, 7=Sunday
|
||||
|
||||
// Calculate the first day of the week containing Jan 1
|
||||
const daysToSubtract = (jan1Weekday - firstDayOfWeek + 7) % 7;
|
||||
const weekContainingJan1Start = jan1.subtract(daysToSubtract, "day");
|
||||
|
||||
if (firstWeekOfYear === 0) {
|
||||
// First week contains first day of the year
|
||||
return weekContainingJan1Start;
|
||||
} else if (firstWeekOfYear === 1) {
|
||||
// First week contains first Thursday (ISO 8601 standard)
|
||||
const jan4 = dayjs(`${year}-01-04`);
|
||||
const jan4Weekday = jan4.isoWeekday();
|
||||
const daysToSubtractFromJan4 = (jan4Weekday - firstDayOfWeek + 7) % 7;
|
||||
return jan4.subtract(daysToSubtractFromJan4, "day");
|
||||
} else {
|
||||
// First week has minimum days
|
||||
const daysInFirstWeek = 7 - daysToSubtract;
|
||||
if (daysInFirstWeek >= minDaysInFirstWeek) {
|
||||
return weekContainingJan1Start;
|
||||
} else {
|
||||
return weekContainingJan1Start.add(1, "week");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the week year and week number for a given date based on user settings.
|
||||
*
|
||||
* @param date The date to calculate week info for
|
||||
* @param settings Week calculation settings
|
||||
* @returns Object with weekYear and weekNumber
|
||||
*/
|
||||
export function getWeekInfo(date: Dayjs, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): { weekYear: number; weekNumber: number } {
|
||||
const { firstDayOfWeek } = settings;
|
||||
|
||||
// Get the start of the week containing this date
|
||||
const dateWeekday = date.isoWeekday();
|
||||
const daysToSubtract = (dateWeekday - firstDayOfWeek + 7) % 7;
|
||||
const weekStart = date.subtract(daysToSubtract, "day");
|
||||
|
||||
// Try current year first
|
||||
let year = date.year();
|
||||
let firstDayOfWeek1 = getFirstDayOfWeek1(year, settings);
|
||||
|
||||
// If the week start is before week 1 of current year, it belongs to previous year
|
||||
if (weekStart.isBefore(firstDayOfWeek1)) {
|
||||
year--;
|
||||
firstDayOfWeek1 = getFirstDayOfWeek1(year, settings);
|
||||
} else {
|
||||
// Check if this might belong to next year's week 1
|
||||
const nextYearFirstDayOfWeek1 = getFirstDayOfWeek1(year + 1, settings);
|
||||
if (!weekStart.isBefore(nextYearFirstDayOfWeek1)) {
|
||||
year++;
|
||||
firstDayOfWeek1 = nextYearFirstDayOfWeek1;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate week number
|
||||
const weekNumber = weekStart.diff(firstDayOfWeek1, "week") + 1;
|
||||
|
||||
return { weekYear: year, weekNumber };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a week string in the format "YYYY-Www" (e.g., "2026-W01").
|
||||
*
|
||||
* @param date The date to generate the week string for
|
||||
* @param settings Week calculation settings
|
||||
* @returns Week string in format "YYYY-Www"
|
||||
*/
|
||||
export function getWeekString(date: Dayjs, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): string {
|
||||
const { weekYear, weekNumber } = getWeekInfo(date, settings);
|
||||
return `${weekYear}-W${weekNumber.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the start date of the week containing the given date.
|
||||
*
|
||||
* @param date The date to find the week start for
|
||||
* @param firstDayOfWeek First day of the week (1=Monday to 7=Sunday)
|
||||
* @returns The start of the week
|
||||
*/
|
||||
export function getWeekStartDate(date: Dayjs, firstDayOfWeek: number = 1): Dayjs {
|
||||
const dateWeekday = date.isoWeekday();
|
||||
const diff = (dateWeekday - firstDayOfWeek + 7) % 7;
|
||||
return date.clone().subtract(diff, "day").startOf("day");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a week string and returns the start date of that week.
|
||||
*
|
||||
* @param weekStr Week string in format "YYYY-Www" (e.g., "2026-W01")
|
||||
* @param settings Week calculation settings
|
||||
* @returns The start date of the week
|
||||
*/
|
||||
export function parseWeekString(weekStr: string, settings: WeekSettings = DEFAULT_WEEK_SETTINGS): Dayjs {
|
||||
const [yearStr, weekNumStr] = weekStr.trim().split("-W");
|
||||
const weekNumber = parseInt(weekNumStr);
|
||||
const weekYear = parseInt(yearStr);
|
||||
|
||||
const firstDayOfWeek1 = getFirstDayOfWeek1(weekYear, settings);
|
||||
return firstDayOfWeek1.add(weekNumber - 1, "week");
|
||||
}
|
||||
95
pnpm-lock.yaml
generated
95
pnpm-lock.yaml
generated
@@ -73,7 +73,7 @@ importers:
|
||||
version: 24.10.9
|
||||
'@vitest/browser-webdriverio':
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)(webdriverio@9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))
|
||||
version: 4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)(webdriverio@9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(@vitest/browser@4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18))(vitest@4.0.18)
|
||||
@@ -980,8 +980,8 @@ importers:
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/browser-webdriverio@4.0.18)(@vitest/ui@4.0.18)(happy-dom@20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.31.1)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
|
||||
webdriverio:
|
||||
specifier: 9.23.3
|
||||
version: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
specifier: 9.23.2
|
||||
version: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
|
||||
packages/ckeditor5-footnotes:
|
||||
devDependencies:
|
||||
@@ -1040,8 +1040,8 @@ importers:
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/browser-webdriverio@4.0.18)(@vitest/ui@4.0.18)(happy-dom@20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.31.1)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
|
||||
webdriverio:
|
||||
specifier: 9.23.3
|
||||
version: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
specifier: 9.23.2
|
||||
version: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
|
||||
packages/ckeditor5-keyboard-marker:
|
||||
devDependencies:
|
||||
@@ -1100,8 +1100,8 @@ importers:
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/browser-webdriverio@4.0.18)(@vitest/ui@4.0.18)(happy-dom@20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.31.1)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
|
||||
webdriverio:
|
||||
specifier: 9.23.3
|
||||
version: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
specifier: 9.23.2
|
||||
version: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
|
||||
packages/ckeditor5-math:
|
||||
dependencies:
|
||||
@@ -1167,8 +1167,8 @@ importers:
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/browser-webdriverio@4.0.18)(@vitest/ui@4.0.18)(happy-dom@20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.31.1)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
|
||||
webdriverio:
|
||||
specifier: 9.23.3
|
||||
version: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
specifier: 9.23.2
|
||||
version: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
|
||||
packages/ckeditor5-mermaid:
|
||||
dependencies:
|
||||
@@ -1234,8 +1234,8 @@ importers:
|
||||
specifier: 4.0.18
|
||||
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/browser-webdriverio@4.0.18)(@vitest/ui@4.0.18)(happy-dom@20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.31.1)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
|
||||
webdriverio:
|
||||
specifier: 9.23.3
|
||||
version: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
specifier: 9.23.2
|
||||
version: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
|
||||
packages/codemirror:
|
||||
dependencies:
|
||||
@@ -6151,27 +6151,27 @@ packages:
|
||||
'@vue/shared@3.5.14':
|
||||
resolution: {integrity: sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==}
|
||||
|
||||
'@wdio/config@9.23.3':
|
||||
resolution: {integrity: sha512-tQCT1R6R3hdib7Qb+82Dxgn/sB+CiR8+GS4zyJh5vU0dzLGeYsCo2B5W89VLItvRjveTmsmh8NOQGV2KH0FHTQ==}
|
||||
'@wdio/config@9.23.2':
|
||||
resolution: {integrity: sha512-19Z+AIQ1NUpr6ncTumjSthm6A7c3DbaGTp+VCdcyN+vHYOK4WsWIomSk+uSbFosYFQVGRjCaHaeGSnC8GNPGYQ==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/logger@9.18.0':
|
||||
resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/protocols@9.23.3':
|
||||
resolution: {integrity: sha512-QfA3Gfl9/3QRX1FnH7x2+uZrgpkwYcksgk1bxGLzl/E0Qefp3BkhgHAfSB1+iKsiYIw9iFOLVx+x+zh0F4BSeg==}
|
||||
'@wdio/protocols@9.23.2':
|
||||
resolution: {integrity: sha512-pmCYOYI2N89QCC8IaiHwaWyP0mR8T1iKkEGpoTq2XVihp7VK/lfPvieyeZT5/e28MadYLJsDQ603pbu5J1NRDg==}
|
||||
|
||||
'@wdio/repl@9.16.2':
|
||||
resolution: {integrity: sha512-FLTF0VL6+o5BSTCO7yLSXocm3kUnu31zYwzdsz4n9s5YWt83sCtzGZlZpt7TaTzb3jVUfxuHNQDTb8UMkCu0lQ==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/types@9.23.3':
|
||||
resolution: {integrity: sha512-Ufjh06DAD7cGTMORUkq5MTZLw1nAgBSr2y8OyiNNuAfPGCwHEU3EwEfhG/y0V7S7xT5pBxliqWi7AjRrCgGcIA==}
|
||||
'@wdio/types@9.23.2':
|
||||
resolution: {integrity: sha512-ryfrERGsNp+aCcrTE1rFU6cbmDj8GHZ04R9k52KNt2u1a6bv3Eh5A/cUA0hXuMdEUfsc8ePLYdwQyOLFydZ0ig==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/utils@9.23.3':
|
||||
resolution: {integrity: sha512-LO/cTpOcb3r49psjmWTxjFduHUMHDOhVfSzL1gfBCS5cGv6h3hAWOYw/94OrxLn1SIOgZu/hyLwf3SWeZB529g==}
|
||||
'@wdio/utils@9.23.2':
|
||||
resolution: {integrity: sha512-+QfgXUWeA940AXT5l5UlrBKoHBk9GLSQE3BA+7ra1zWuFvv6SHG6M2mwplcPlOlymJMqXy8e7ZgLEoLkXuvC1Q==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
@@ -14746,12 +14746,12 @@ packages:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webdriver@9.23.3:
|
||||
resolution: {integrity: sha512-8FdXOhzkxqDI6F1dyIsQONhKLDZ9HPSEwNBnH3bD1cHnj/6nVvyYrUtDPo/+J324BuwOa1IVTH3m8mb3B2hTlA==}
|
||||
webdriver@9.23.2:
|
||||
resolution: {integrity: sha512-HZy3eydZbmex0pbyLwHaDsAyZ+S+V4XQTdGK/nAOi4uPa74U6yT9vXqtb+3B+5/LDM7L8kTD6Z3b1y4gB4pmTw==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
webdriverio@9.23.3:
|
||||
resolution: {integrity: sha512-1dhMsBx/GLHJsDLhg/xuEQ48JZPrbldz7qdFT+MXQZADj9CJ4bJywWtVBME648MmVMfgDvLc5g2ThGIOupSLvQ==}
|
||||
webdriverio@9.23.2:
|
||||
resolution: {integrity: sha512-VjfTw1bRJdBrzjoCu7BGThxn1JK2V7mAGvxibaBrCNIayPPQjLhVDNJPOVEiR7txM6zmOUWxhkCDxHjhMYirfQ==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
peerDependencies:
|
||||
puppeteer-core: '>=22.x || <=24.x'
|
||||
@@ -16254,8 +16254,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-table': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-emoji@47.4.0':
|
||||
dependencies:
|
||||
@@ -16438,8 +16436,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.4.0
|
||||
ckeditor5: 47.4.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-icons@47.4.0': {}
|
||||
|
||||
@@ -16938,6 +16934,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-icons': 47.4.0
|
||||
'@ckeditor/ckeditor5-ui': 47.4.0
|
||||
'@ckeditor/ckeditor5-utils': 47.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-upload@47.4.0':
|
||||
dependencies:
|
||||
@@ -21486,11 +21484,11 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@vitest/browser-webdriverio@4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)(webdriverio@9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
|
||||
'@vitest/browser-webdriverio@4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)(webdriverio@9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)
|
||||
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.9)(@vitest/browser-webdriverio@4.0.18)(@vitest/ui@4.0.18)(happy-dom@20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(lightningcss@1.31.1)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)
|
||||
webdriverio: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
webdriverio: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- msw
|
||||
@@ -21644,15 +21642,14 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.14': {}
|
||||
|
||||
'@wdio/config@9.23.3':
|
||||
'@wdio/config@9.23.2':
|
||||
dependencies:
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/types': 9.23.3
|
||||
'@wdio/utils': 9.23.3
|
||||
'@wdio/types': 9.23.2
|
||||
'@wdio/utils': 9.23.2
|
||||
deepmerge-ts: 7.1.5
|
||||
glob: 13.0.0
|
||||
import-meta-resolve: 4.2.0
|
||||
jiti: 2.6.1
|
||||
transitivePeerDependencies:
|
||||
- bare-buffer
|
||||
- supports-color
|
||||
@@ -21665,21 +21662,21 @@ snapshots:
|
||||
safe-regex2: 5.0.0
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
'@wdio/protocols@9.23.3': {}
|
||||
'@wdio/protocols@9.23.2': {}
|
||||
|
||||
'@wdio/repl@9.16.2':
|
||||
dependencies:
|
||||
'@types/node': 20.19.25
|
||||
|
||||
'@wdio/types@9.23.3':
|
||||
'@wdio/types@9.23.2':
|
||||
dependencies:
|
||||
'@types/node': 20.19.25
|
||||
|
||||
'@wdio/utils@9.23.3':
|
||||
'@wdio/utils@9.23.2':
|
||||
dependencies:
|
||||
'@puppeteer/browsers': 2.10.10
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/types': 9.23.3
|
||||
'@wdio/types': 9.23.2
|
||||
decamelize: 6.0.1
|
||||
deepmerge-ts: 7.1.5
|
||||
edgedriver: 6.1.2
|
||||
@@ -32043,7 +32040,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 24.10.9
|
||||
'@vitest/browser-webdriverio': 4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)(webdriverio@9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5))
|
||||
'@vitest/browser-webdriverio': 4.0.18(bufferutil@4.0.9)(msw@2.7.5(@types/node@24.10.9)(typescript@5.9.3))(utf-8-validate@6.0.5)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.31.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))(vitest@4.0.18)(webdriverio@9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))
|
||||
'@vitest/ui': 4.0.18(vitest@4.0.18)
|
||||
happy-dom: 20.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
@@ -32158,15 +32155,15 @@ snapshots:
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webdriver@9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5):
|
||||
webdriver@9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5):
|
||||
dependencies:
|
||||
'@types/node': 20.19.25
|
||||
'@types/ws': 8.18.1
|
||||
'@wdio/config': 9.23.3
|
||||
'@wdio/config': 9.23.2
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/protocols': 9.23.3
|
||||
'@wdio/types': 9.23.3
|
||||
'@wdio/utils': 9.23.3
|
||||
'@wdio/protocols': 9.23.2
|
||||
'@wdio/types': 9.23.2
|
||||
'@wdio/utils': 9.23.2
|
||||
deepmerge-ts: 7.1.5
|
||||
https-proxy-agent: 7.0.6
|
||||
undici: 6.23.0
|
||||
@@ -32177,16 +32174,16 @@ snapshots:
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
webdriverio@9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5):
|
||||
webdriverio@9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5):
|
||||
dependencies:
|
||||
'@types/node': 20.19.25
|
||||
'@types/sinonjs__fake-timers': 8.1.5
|
||||
'@wdio/config': 9.23.3
|
||||
'@wdio/config': 9.23.2
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/protocols': 9.23.3
|
||||
'@wdio/protocols': 9.23.2
|
||||
'@wdio/repl': 9.16.2
|
||||
'@wdio/types': 9.23.3
|
||||
'@wdio/utils': 9.23.3
|
||||
'@wdio/types': 9.23.2
|
||||
'@wdio/utils': 9.23.2
|
||||
archiver: 7.0.1
|
||||
aria-query: 5.3.2
|
||||
cheerio: 1.2.0
|
||||
@@ -32203,7 +32200,7 @@ snapshots:
|
||||
rgb2hex: 0.2.5
|
||||
serialize-error: 12.0.0
|
||||
urlpattern-polyfill: 10.1.0
|
||||
webdriver: 9.23.3(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
webdriver: 9.23.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)
|
||||
transitivePeerDependencies:
|
||||
- bare-buffer
|
||||
- bufferutil
|
||||
|
||||
Reference in New Issue
Block a user