Commit 97ba1545 authored by Johnny's avatar Johnny

chore: prevent unnecessary API calls when timestamp unchanged in MemoDetailSidebar

- Add same value check before updating createTime/updateTime
- Skip request if new timestamp equals current timestamp
- Simplify callback handlers and improve code readability
- Use .some() instead of .filter().length for cleaner code
parent f7a81296
import { memo } from "react"; import { memo, useMemo } from "react";
import { useInstance } from "@/contexts/InstanceContext"; import { useInstance } from "@/contexts/InstanceContext";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { CalendarCell } from "./CalendarCell"; import { CalendarCell } from "./CalendarCell";
import { useTodayDate, useWeekdayLabels } from "./hooks"; import { useTodayDate, useWeekdayLabels } from "./hooks";
import type { MonthCalendarProps } from "./types"; import type { CalendarSize, MonthCalendarProps } from "./types";
import { useCalendarMatrix } from "./useCalendar"; import { useCalendarMatrix } from "./useCalendar";
import { getTooltipText } from "./utils"; import { getTooltipText } from "./utils";
const GRID_STYLES: Record<CalendarSize, { gap: string; headerText: string }> = {
small: { gap: "gap-1.5", headerText: "text-[10px]" },
default: { gap: "gap-2", headerText: "text-xs" },
};
interface WeekdayHeaderProps {
weekDays: string[];
size: CalendarSize;
}
const WeekdayHeader = memo(({ weekDays, size }: WeekdayHeaderProps) => (
<div className={cn("grid grid-cols-7 mb-1", GRID_STYLES[size].gap, GRID_STYLES[size].headerText)} role="row">
{weekDays.map((label, index) => (
<div
key={index}
className="flex h-4 items-center justify-center font-medium uppercase tracking-wide text-muted-foreground/60"
role="columnheader"
aria-label={label}
>
{label}
</div>
))}
</div>
));
WeekdayHeader.displayName = "WeekdayHeader";
export const MonthCalendar = memo((props: MonthCalendarProps) => { export const MonthCalendar = memo((props: MonthCalendarProps) => {
const { month, data, maxCount, size = "default", onClick, className } = props; const { month, data, maxCount, size = "default", onClick, className } = props;
const t = useTranslate(); const t = useTranslate();
const { generalSetting } = useInstance(); const { generalSetting } = useInstance();
const weekStartDayOffset = generalSetting.weekStartDayOffset;
const today = useTodayDate(); const today = useTodayDate();
const weekDays = useWeekdayLabels(); const weekDays = useWeekdayLabels();
...@@ -22,40 +45,28 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => { ...@@ -22,40 +45,28 @@ export const MonthCalendar = memo((props: MonthCalendarProps) => {
month, month,
data, data,
weekDays, weekDays,
weekStartDayOffset, weekStartDayOffset: generalSetting.weekStartDayOffset,
today, today,
selectedDate: "", selectedDate: "",
}); });
const gridGap = size === "small" ? "gap-x-3 gap-y-3" : "gap-x-3.5 gap-y-3.5"; const flatDays = useMemo(() => weeks.flatMap((week) => week.days), [weeks]);
return ( return (
<div className={cn("flex flex-col gap-2", className)}> <div className={cn("flex flex-col", className)} role="grid" aria-label={`Calendar for ${month}`}>
<div className={cn("grid grid-cols-7", gridGap, "text-muted-foreground mb-1", size === "small" ? "text-[10px]" : "text-xs")}> <WeekdayHeader weekDays={rotatedWeekDays} size={size} />
{rotatedWeekDays.map((label, index) => (
<div key={index} className="flex h-4 items-center justify-center text-muted-foreground/70 font-medium uppercase tracking-wide">
{label}
</div>
))}
</div>
<div className={cn("grid grid-cols-7 px-2", gridGap)}> <div className={cn("grid grid-cols-7", GRID_STYLES[size].gap)} role="rowgroup">
{weeks.map((week, weekIndex) => {flatDays.map((day) => (
week.days.map((day, dayIndex) => { <CalendarCell
const tooltipText = getTooltipText(day.count, day.date, t); key={day.date}
day={day}
return ( maxCount={maxCount}
<CalendarCell tooltipText={getTooltipText(day.count, day.date, t)}
key={`${weekIndex}-${dayIndex}-${day.date}`} onClick={onClick}
day={day} size={size}
maxCount={maxCount} />
tooltipText={tooltipText} ))}
onClick={onClick}
size={size}
/>
);
}),
)}
</div> </div>
</div> </div>
); );
......
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useMemo } from "react"; import { memo, useMemo } from "react";
import {
calculateYearMaxCount,
filterDataByYear,
generateMonthsForYear,
getMonthLabel,
MonthCalendar,
} from "@/components/ActivityCalendar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { getMaxYear, MIN_YEAR } from "./constants"; import { getMaxYear, MIN_YEAR } from "./constants";
import { MonthCalendar } from "./MonthCalendar";
import type { YearCalendarProps } from "./types"; import type { YearCalendarProps } from "./types";
import { calculateYearMaxCount, filterDataByYear, generateMonthsForYear, getMonthLabel } from "./utils";
export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => { interface YearNavigationProps {
selectedYear: number;
currentYear: number;
onPrev: () => void;
onNext: () => void;
onToday: () => void;
canGoPrev: boolean;
canGoNext: boolean;
}
const YearNavigation = memo(({ selectedYear, currentYear, onPrev, onNext, onToday, canGoPrev, canGoNext }: YearNavigationProps) => {
const t = useTranslate(); const t = useTranslate();
const isCurrentYear = selectedYear === currentYear;
return (
<div className="flex items-center justify-between px-1">
<h2 className="text-2xl font-semibold text-foreground tracking-tight">{selectedYear}</h2>
<nav className="inline-flex items-center gap-0.5 rounded-lg border border-border/30 bg-muted/10 p-0.5" aria-label="Year navigation">
<Button
variant="ghost"
size="sm"
onClick={onPrev}
disabled={!canGoPrev}
aria-label="Previous year"
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
>
<ChevronLeftIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={onToday}
disabled={isCurrentYear}
aria-label={t("common.today")}
className={cn(
"h-7 px-2.5 rounded-md text-[10px] font-medium uppercase tracking-wider",
isCurrentYear ? "text-muted-foreground/50 cursor-default" : "text-muted-foreground hover:text-foreground hover:bg-muted/40",
)}
>
{t("common.today")}
</Button>
<Button
variant="ghost"
size="sm"
onClick={onNext}
disabled={!canGoNext}
aria-label="Next year"
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
>
<ChevronRightIcon className="w-4 h-4" />
</Button>
</nav>
</div>
);
});
YearNavigation.displayName = "YearNavigation";
interface MonthCardProps {
month: string;
data: Record<string, number>;
maxCount: number;
onDateClick: (date: string) => void;
}
const MonthCard = memo(({ month, data, maxCount, onDateClick }: MonthCardProps) => (
<article className="flex flex-col gap-2 rounded-xl border border-border/20 bg-muted/5 p-3 transition-colors hover:bg-muted/10">
<header className="text-[10px] font-medium text-muted-foreground/80 uppercase tracking-widest">{getMonthLabel(month)}</header>
<MonthCalendar month={month} data={data} maxCount={maxCount} size="small" onClick={onDateClick} />
</article>
));
MonthCard.displayName = "MonthCard";
export const YearCalendar = memo(({ selectedYear, data, onYearChange, onDateClick, className }: YearCalendarProps) => {
const currentYear = useMemo(() => new Date().getFullYear(), []); const currentYear = useMemo(() => new Date().getFullYear(), []);
const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]); const yearData = useMemo(() => filterDataByYear(data, selectedYear), [data, selectedYear]);
const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]); const months = useMemo(() => generateMonthsForYear(selectedYear), [selectedYear]);
...@@ -23,75 +92,28 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl ...@@ -23,75 +92,28 @@ export const YearCalendar = ({ selectedYear, data, onYearChange, onDateClick, cl
const canGoPrev = selectedYear > MIN_YEAR; const canGoPrev = selectedYear > MIN_YEAR;
const canGoNext = selectedYear < getMaxYear(); const canGoNext = selectedYear < getMaxYear();
const isCurrentYear = selectedYear === currentYear;
const handlePrevYear = () => canGoPrev && onYearChange(selectedYear - 1);
const handleNextYear = () => canGoNext && onYearChange(selectedYear + 1);
const handleToday = () => onYearChange(currentYear);
return ( return (
<div className={cn("w-full flex flex-col gap-6 px-4 sm:px-0 py-4 select-none", className)}> <section className={cn("w-full flex flex-col gap-5 px-4 py-4 select-none", className)} aria-label={`Year ${selectedYear} calendar`}>
<div className="flex items-center justify-between px-1"> <YearNavigation
<div className="flex items-baseline gap-3"> selectedYear={selectedYear}
<h2 className="text-2xl md:text-3xl font-semibold text-foreground tracking-tight leading-none">{selectedYear}</h2> currentYear={currentYear}
</div> onPrev={() => canGoPrev && onYearChange(selectedYear - 1)}
onNext={() => canGoNext && onYearChange(selectedYear + 1)}
<div className="inline-flex items-center gap-1 shrink-0 rounded-lg border border-border/20 bg-muted/20 p-1"> onToday={() => onYearChange(currentYear)}
<Button canGoPrev={canGoPrev}
variant="ghost" canGoNext={canGoNext}
size="sm" />
onClick={handlePrevYear}
disabled={!canGoPrev}
aria-label="Previous year"
className="h-8 w-8 p-0 rounded-md hover:bg-muted/30 text-muted-foreground hover:text-foreground"
>
<ChevronLeftIcon className="w-5 h-5" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleToday}
disabled={isCurrentYear}
aria-label={t("common.today")}
className={cn(
"h-8 px-3 rounded-md text-[11px] font-semibold uppercase tracking-[0.18em] transition-colors",
isCurrentYear ? "bg-muted/30 text-muted-foreground cursor-default" : "hover:bg-muted/30 text-foreground",
)}
>
{t("common.today")}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleNextYear}
disabled={!canGoNext}
aria-label="Next year"
className="h-8 w-8 p-0 rounded-md hover:bg-muted/30 text-muted-foreground hover:text-foreground"
>
<ChevronRightIcon className="w-5 h-5" />
</Button>
</div>
</div>
<TooltipProvider> <TooltipProvider>
<div className="w-full animate-fade-in"> <div className="grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 animate-fade-in">
<div className="grid gap-6 md:gap-7 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> {months.map((month) => (
{months.map((month) => ( <MonthCard key={month} month={month} data={yearData} maxCount={yearMaxCount} onDateClick={onDateClick} />
<div ))}
key={month}
className="flex flex-col gap-3 rounded-2xl border border-border/20 bg-muted/10 p-4 shadow-sm hover:shadow-md transition-shadow cursor-default"
>
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-[0.22em] pl-1">
{getMonthLabel(month)}
</div>
<MonthCalendar month={month} data={yearData} maxCount={yearMaxCount} size="small" onClick={onDateClick} />
</div>
))}
</div>
</div> </div>
</TooltipProvider> </TooltipProvider>
</div> </section>
); );
}; });
YearCalendar.displayName = "YearCalendar";
...@@ -18,28 +18,25 @@ interface Props { ...@@ -18,28 +18,25 @@ interface Props {
const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
const t = useTranslate(); const t = useTranslate();
const property = create(Memo_PropertySchema, memo.property || {});
const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode || property.hasIncompleteTasks;
const shouldShowRelationGraph = memo.relations.filter((r) => r.type === MemoRelation_Type.REFERENCE).length > 0;
const { mutate: updateMemo } = useUpdateMemo(); const { mutate: updateMemo } = useUpdateMemo();
const property = create(Memo_PropertySchema, memo.property || {});
const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode;
const hasReferenceRelations = memo.relations.some((r) => r.type === MemoRelation_Type.REFERENCE);
const handleUpdateTimestamp = (field: "createTime" | "updateTime", date: Date) => { const handleUpdateTimestamp = (field: "createTime" | "updateTime", date: Date) => {
const timestamp = timestampFromDate(date); const currentTimestamp = memo[field];
const newTimestamp = timestampFromDate(date);
if (isEqual(currentTimestamp, newTimestamp)) {
return;
}
updateMemo( updateMemo(
{ {
update: { update: { name: memo.name, [field]: newTimestamp },
name: memo.name,
[field]: timestamp,
},
updateMask: [field === "createTime" ? "create_time" : "update_time"], updateMask: [field === "createTime" ? "create_time" : "update_time"],
}, },
{ {
onSuccess: () => { onSuccess: () => toast.success("Updated successfully"),
toast.success("Updated successfully"); onError: (error) => toast.error(error.message),
},
onError: (error) => {
toast.error(error.message);
},
}, },
); );
}; };
...@@ -49,7 +46,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { ...@@ -49,7 +46,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
className={cn("relative w-full h-auto max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start", className)} className={cn("relative w-full h-auto max-h-screen overflow-auto hide-scrollbar flex flex-col justify-start items-start", className)}
> >
<div className="flex flex-col justify-start items-start w-full gap-4 h-auto shrink-0 flex-nowrap hide-scrollbar"> <div className="flex flex-col justify-start items-start w-full gap-4 h-auto shrink-0 flex-nowrap hide-scrollbar">
{shouldShowRelationGraph && ( {hasReferenceRelations && (
<div className="relative w-full h-36 border border-border rounded-lg bg-muted overflow-hidden"> <div className="relative w-full h-36 border border-border rounded-lg bg-muted overflow-hidden">
<MemoRelationForceGraph className="w-full h-full" memo={memo} parentPage={parentPage} /> <MemoRelationForceGraph className="w-full h-full" memo={memo} parentPage={parentPage} />
<div className="absolute top-2 left-2 text-xs text-muted-foreground/60 font-medium gap-1 flex flex-row items-center"> <div className="absolute top-2 left-2 text-xs text-muted-foreground/60 font-medium gap-1 flex flex-row items-center">
...@@ -58,16 +55,19 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { ...@@ -58,16 +55,19 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
</div> </div>
</div> </div>
)} )}
<div className="w-full space-y-1"> <div className="w-full space-y-1">
<p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.created-at")}</p> <p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.created-at")}</p>
<EditableTimestamp timestamp={memo.createTime} onChange={(date) => handleUpdateTimestamp("createTime", date)} /> <EditableTimestamp timestamp={memo.createTime} onChange={(date) => handleUpdateTimestamp("createTime", date)} />
</div> </div>
{!isEqual(memo.createTime, memo.updateTime) && ( {!isEqual(memo.createTime, memo.updateTime) && (
<div className="w-full space-y-1"> <div className="w-full space-y-1">
<p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.last-updated-at")}</p> <p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.last-updated-at")}</p>
<EditableTimestamp timestamp={memo.updateTime} onChange={(date) => handleUpdateTimestamp("updateTime", date)} /> <EditableTimestamp timestamp={memo.updateTime} onChange={(date) => handleUpdateTimestamp("updateTime", date)} />
</div> </div>
)} )}
{hasSpecialProperty && ( {hasSpecialProperty && (
<div className="w-full space-y-2"> <div className="w-full space-y-2">
<p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.properties")}</p> <p className="text-xs font-medium text-muted-foreground/60 uppercase tracking-wide px-1">{t("common.properties")}</p>
...@@ -93,6 +93,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { ...@@ -93,6 +93,7 @@ const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => {
</div> </div>
</div> </div>
)} )}
{memo.tags.length > 0 && ( {memo.tags.length > 0 && (
<div className="w-full space-y-2"> <div className="w-full space-y-2">
<div className="flex flex-row justify-start items-center gap-1.5 px-1"> <div className="flex flex-row justify-start items-center gap-1.5 px-1">
......
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useState } from "react"; import { memo, useCallback, useMemo, useState } from "react";
import { YearCalendar } from "@/components/ActivityCalendar"; import { YearCalendar } from "@/components/ActivityCalendar";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import i18n from "@/i18n"; import i18n from "@/i18n";
import { addMonths, formatMonth, getMonthFromDate, getYearFromDate, setYearAndMonth } from "@/lib/calendar-utils"; import { addMonths, formatMonth, getMonthFromDate, getYearFromDate, setYearAndMonth } from "@/lib/calendar-utils";
import type { MonthNavigatorProps } from "@/types/statistics"; import type { MonthNavigatorProps } from "@/types/statistics";
export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => { export const MonthNavigator = memo(({ visibleMonth, onMonthChange, activityStats }: MonthNavigatorProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const currentMonth = dayjs(visibleMonth).toDate();
const currentYear = getYearFromDate(visibleMonth);
const currentMonthNum = getMonthFromDate(visibleMonth);
const handlePrevMonth = () => { const { currentMonth, currentYear, currentMonthNum } = useMemo(
onMonthChange(addMonths(visibleMonth, -1)); () => ({
}; currentMonth: dayjs(visibleMonth).toDate(),
currentYear: getYearFromDate(visibleMonth),
currentMonthNum: getMonthFromDate(visibleMonth),
}),
[visibleMonth],
);
const monthLabel = useMemo(() => currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" }), [currentMonth]);
const handleNextMonth = () => { const handlePrevMonth = useCallback(() => onMonthChange(addMonths(visibleMonth, -1)), [visibleMonth, onMonthChange]);
onMonthChange(addMonths(visibleMonth, 1)); const handleNextMonth = useCallback(() => onMonthChange(addMonths(visibleMonth, 1)), [visibleMonth, onMonthChange]);
};
const handleDateClick = (date: string) => { const handleDateClick = useCallback(
onMonthChange(formatMonth(date)); (date: string) => {
setIsOpen(false); onMonthChange(formatMonth(date));
}; setIsOpen(false);
},
[onMonthChange],
);
const handleYearChange = (year: number) => { const handleYearChange = useCallback(
onMonthChange(setYearAndMonth(year, currentMonthNum)); (year: number) => onMonthChange(setYearAndMonth(year, currentMonthNum)),
}; [currentMonthNum, onMonthChange],
);
return ( return (
<div className="w-full mb-2 flex flex-row justify-between items-center gap-2"> <header className="w-full mb-2 flex items-center justify-between gap-2">
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<button className="py-1 text-sm text-foreground font-medium transition-colors flex items-center select-none"> <button
{currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" })} type="button"
className="py-0.5 text-sm text-foreground font-medium transition-colors hover:text-foreground/80 select-none"
>
{monthLabel}
</button> </button>
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className="p-0 border border-border/20 bg-background md:max-w-6xl w-[min(100vw-24px,1200px)] max-h-[85vh] overflow-auto rounded-2xl shadow-2xl" className="p-0 border border-border/20 bg-background md:max-w-6xl w-[min(100vw-24px,1200px)] max-h-[85vh] overflow-auto rounded-xl shadow-xl"
size="2xl" size="2xl"
showCloseButton={false} showCloseButton={false}
> >
...@@ -47,22 +58,29 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: M ...@@ -47,22 +58,29 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange, activityStats }: M
<YearCalendar selectedYear={currentYear} data={activityStats} onYearChange={handleYearChange} onDateClick={handleDateClick} /> <YearCalendar selectedYear={currentYear} data={activityStats} onYearChange={handleYearChange} onDateClick={handleDateClick} />
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<div className="flex justify-end items-center shrink-0">
<button <nav className="flex items-center shrink-0" aria-label="Month navigation">
className="h-8 w-8 rounded-lg hover:border-border/40 hover:bg-muted/30 text-muted-foreground hover:text-foreground transition-all" <Button
variant="ghost"
size="sm"
onClick={handlePrevMonth} onClick={handlePrevMonth}
aria-label="Previous month" aria-label="Previous month"
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
> >
<ChevronLeftIcon className="w-4 h-4 mx-auto" /> <ChevronLeftIcon className="w-4 h-4" />
</button> </Button>
<button <Button
className="h-8 w-8 rounded-lg hover:border-border/40 hover:bg-muted/30 text-muted-foreground hover:text-foreground transition-all" variant="ghost"
size="sm"
onClick={handleNextMonth} onClick={handleNextMonth}
aria-label="Next month" aria-label="Next month"
className="h-7 w-7 p-0 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted/40"
> >
<ChevronRightIcon className="w-4 h-4 mx-auto" /> <ChevronRightIcon className="w-4 h-4" />
</button> </Button>
</div> </nav>
</div> </header>
); );
}; });
MonthNavigator.displayName = "MonthNavigator";
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment