chore: get date, time formats and start week on from user metadata (#43)

* chore: get date, time formats and start week on from user metadata

* test: fix tests

* chore: no need to provide currentUser in context

Signed-off-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>

* fix: font sizing

Signed-off-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>

* chore: fetch user profile on receive user profile change

* fix: only undefined

* chore: memoize loadMentionableUsers

* chore: adjust dropdown menu style

---------

Signed-off-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
This commit is contained in:
Richard Shiue
2025-08-29 10:41:28 +08:00
committed by GitHub
parent bc5065a80c
commit 61242d9836
25 changed files with 566 additions and 255 deletions

View File

@@ -2817,7 +2817,9 @@
},
"visitOurWebsite": "Visit our official website",
"addMessagesToPage": "Add messages to page",
"addMessagesToPageDisabled": "No messages available"
"addMessagesToPageDisabled": "No messages available",
"accountSettings": "Account settings",
"startWeekOn": "Start week on"
},
"globalComment": {
"comments": "Comments",

View File

@@ -1,7 +1,7 @@
import { expect } from '@jest/globals';
import { toZonedTime } from 'date-fns-tz';
import { DateFormat } from '../types';
import {
DateFormatType,
MetadataDefaults,
MetadataKey,
MetadataUtils,
@@ -18,28 +18,11 @@ describe('User Metadata', () => {
jest.clearAllMocks();
});
describe('MetadataKey enum', () => {
it('should have correct values', () => {
expect(MetadataKey.Timezone).toBe('timezone');
expect(MetadataKey.Language).toBe('language');
expect(MetadataKey.DateFormat).toBe('date_format');
expect(MetadataKey.IconUrl).toBe('icon_url');
});
});
describe('DateFormatType enum', () => {
it('should have correct date format patterns', () => {
expect(DateFormatType.US).toBe('MM/DD/YYYY');
expect(DateFormatType.EU).toBe('DD/MM/YYYY');
expect(DateFormatType.ISO).toBe('YYYY-MM-DD');
});
});
describe('MetadataDefaults', () => {
it('should have default values for all metadata keys', () => {
expect(MetadataDefaults[MetadataKey.Timezone]).toBe('UTC');
expect(MetadataDefaults[MetadataKey.Language]).toBe('en');
expect(MetadataDefaults[MetadataKey.DateFormat]).toBe(DateFormatType.US);
expect(MetadataDefaults[MetadataKey.DateFormat]).toBe(DateFormat.US);
expect(MetadataDefaults[MetadataKey.IconUrl]).toBe('');
});
});
@@ -62,8 +45,8 @@ describe('User Metadata', () => {
});
it('should set date format', () => {
const result = builder.setDateFormat(DateFormatType.EU).build();
expect(result[MetadataKey.DateFormat]).toBe('DD/MM/YYYY');
const result = builder.setDateFormat(DateFormat.DayMonthYear).build();
expect(result[MetadataKey.DateFormat]).toBe(4);
});
it('should set icon URL', () => {
@@ -71,24 +54,17 @@ describe('User Metadata', () => {
expect(result[MetadataKey.IconUrl]).toBe('https://example.com/icon.png');
});
it('should set custom metadata', () => {
const result = builder.setCustom('theme', 'dark').build();
expect(result.theme).toBe('dark');
});
it('should chain multiple setters', () => {
const result = builder
.setTimezone('Europe/London')
.setLanguage('en-GB')
.setDateFormat(DateFormatType.EU)
.setCustom('theme', 'light')
.setDateFormat(DateFormat.DayMonthYear)
.build();
expect(result).toEqual({
[MetadataKey.Timezone]: 'Europe/London',
[MetadataKey.Language]: 'en-GB',
[MetadataKey.DateFormat]: DateFormatType.EU,
theme: 'light',
[MetadataKey.DateFormat]: DateFormat.DayMonthYear,
});
});
@@ -105,21 +81,21 @@ describe('User Metadata', () => {
describe('MetadataUtils', () => {
describe('detectDateFormat', () => {
it('should detect US format for US locale', () => {
expect(MetadataUtils.detectDateFormat('en-US')).toBe(DateFormatType.US);
expect(MetadataUtils.detectDateFormat('en-CA')).toBe(DateFormatType.US);
expect(MetadataUtils.detectDateFormat('en-PH')).toBe(DateFormatType.US);
expect(MetadataUtils.detectDateFormat('en-US')).toBe(DateFormat.US);
expect(MetadataUtils.detectDateFormat('en-CA')).toBe(DateFormat.US);
expect(MetadataUtils.detectDateFormat('en-PH')).toBe(DateFormat.US);
});
it('should detect EU format for European locales', () => {
expect(MetadataUtils.detectDateFormat('en-GB')).toBe(DateFormatType.EU);
expect(MetadataUtils.detectDateFormat('fr-FR')).toBe(DateFormatType.EU);
expect(MetadataUtils.detectDateFormat('de-DE')).toBe(DateFormatType.EU);
expect(MetadataUtils.detectDateFormat('en-GB')).toBe(DateFormat.DayMonthYear);
expect(MetadataUtils.detectDateFormat('fr-FR')).toBe(DateFormat.DayMonthYear);
expect(MetadataUtils.detectDateFormat('de-DE')).toBe(DateFormat.DayMonthYear);
});
it('should detect ISO format for specific regions', () => {
expect(MetadataUtils.detectDateFormat('sv-SE')).toBe(DateFormatType.ISO);
expect(MetadataUtils.detectDateFormat('fi-FI')).toBe(DateFormatType.ISO);
expect(MetadataUtils.detectDateFormat('ko-KR')).toBe(DateFormatType.ISO);
expect(MetadataUtils.detectDateFormat('sv-SE')).toBe(DateFormat.ISO);
expect(MetadataUtils.detectDateFormat('fi-FI')).toBe(DateFormat.ISO);
expect(MetadataUtils.detectDateFormat('ko-KR')).toBe(DateFormat.ISO);
});
it('should use browser locale as default', () => {
@@ -129,7 +105,7 @@ describe('User Metadata', () => {
configurable: true,
});
expect(MetadataUtils.detectDateFormat()).toBe(DateFormatType.US);
expect(MetadataUtils.detectDateFormat()).toBe(DateFormat.US);
Object.defineProperty(navigator, 'language', {
value: originalLanguage,
@@ -138,8 +114,8 @@ describe('User Metadata', () => {
});
it('should handle locale without region', () => {
expect(MetadataUtils.detectDateFormat('en')).toBe(DateFormatType.EU);
expect(MetadataUtils.detectDateFormat('fr')).toBe(DateFormatType.EU);
expect(MetadataUtils.detectDateFormat('en')).toBe(DateFormat.DayMonthYear);
expect(MetadataUtils.detectDateFormat('fr')).toBe(DateFormat.DayMonthYear);
});
});
@@ -222,14 +198,12 @@ describe('User Metadata', () => {
it('should merge multiple metadata objects', () => {
const obj1 = { [MetadataKey.Timezone]: 'UTC' };
const obj2 = { [MetadataKey.Language]: 'en' };
const obj3 = { custom: 'value' };
const result = MetadataUtils.merge(obj1, obj2, obj3);
const result = MetadataUtils.merge(obj1, obj2);
expect(result).toEqual({
[MetadataKey.Timezone]: 'UTC',
[MetadataKey.Language]: 'en',
custom: 'value',
});
});

View File

@@ -2,7 +2,7 @@ import * as Y from 'yjs';
import { FieldType } from '@/application/database-yjs/database.type';
import { getDateCellStr, parseChecklistData, parseSelectOptionTypeOptions } from '@/application/database-yjs/fields';
import { YDatabaseCell, YDatabaseField, YjsDatabaseKey } from '@/application/types';
import { User, YDatabaseCell, YDatabaseField, YjsDatabaseKey } from '@/application/types';
import { Cell, DateTimeCell, FileMediaCell, FileMediaCellData } from './cell.type';
@@ -94,7 +94,7 @@ export function parseYDatabaseRelationCellToCell(cell: YDatabaseCell): Cell {
};
}
export function getCellDataText(cell: YDatabaseCell, field: YDatabaseField): string {
export function getCellDataText(cell: YDatabaseCell, field: YDatabaseField, currentUser?: User): string {
const type = parseInt(field.get(YjsDatabaseKey.type));
switch (type) {
@@ -143,7 +143,7 @@ export function getCellDataText(cell: YDatabaseCell, field: YDatabaseField): str
case FieldType.DateTime: {
const dateCell = parseYDatabaseDateTimeCellToCell(cell);
return getDateCellStr({ cell: dateCell, field });
return getDateCellStr({ cell: dateCell, field, currentUser });
}
case FieldType.CreatedTime:

View File

@@ -2,8 +2,7 @@ import React from 'react';
import * as Y from 'yjs';
import { FieldType } from '@/application/database-yjs/database.type';
import { DateFormat, TimeFormat } from '@/application/database-yjs/index';
import { FieldId, RowId } from '@/application/types';
import { DateFormat, FieldId, RowId, TimeFormat } from '@/application/types';
export interface Cell {
createdAt: number;

View File

@@ -28,7 +28,6 @@ import {
SortCondition,
} from '@/application/database-yjs/database.type';
import {
DateFormat,
getDateCellStr,
getFieldName,
isDate,
@@ -38,7 +37,6 @@ import {
safeParseTimestamp,
SelectOption,
SelectTypeOption,
TimeFormat,
} from '@/application/database-yjs/fields';
import { createCheckboxCell, getChecked } from '@/application/database-yjs/fields/checkbox/utils';
import { EnhancedBigStats } from '@/application/database-yjs/fields/number/EnhancedBigStats';
@@ -51,8 +49,10 @@ import { useBoardLayoutSettings, useFieldSelector, useFieldType } from '@/applic
import { executeOperations } from '@/application/slate-yjs/utils/yjs';
import {
DatabaseViewLayout,
DateFormat,
FieldId,
RowId,
TimeFormat,
UpdatePagePayload,
ViewLayout,
YDatabase,
@@ -81,6 +81,7 @@ import {
YMapFieldTypeOption,
YSharedRoot,
} from '@/application/types';
import { useCurrentUser } from '@/components/main/app.hooks';
export function useResizeColumnWidthDispatch() {
const database = useDatabase();
@@ -2232,6 +2233,7 @@ export function useSwitchPropertyType() {
const database = useDatabase();
const sharedRoot = useSharedRoot();
const rowDocMap = useRowDocMap();
const currentUser = useCurrentUser();
return useCallback(
(fieldId: string, fieldType: FieldType) => {
@@ -2284,8 +2286,6 @@ export function useSwitchPropertyType() {
// Set default values for the type option
if ([FieldType.CreatedTime, FieldType.LastEditedTime, FieldType.DateTime].includes(fieldType)) {
// to DateTime
newTypeOption.set(YjsDatabaseKey.time_format, TimeFormat.TwentyFourHour);
newTypeOption.set(YjsDatabaseKey.date_format, DateFormat.Friendly);
if (oldFieldType !== FieldType.DateTime) {
newTypeOption.set(YjsDatabaseKey.include_time, true);
}
@@ -2455,6 +2455,7 @@ export function useSwitchPropertyType() {
newData = getDateCellStr({
cell: dateCell,
field,
currentUser,
});
break;
@@ -2545,7 +2546,7 @@ export function useSwitchPropertyType() {
'switchPropertyType'
);
},
[database, sharedRoot, rowDocMap]
[database, sharedRoot, rowDocMap, currentUser]
);
}

View File

@@ -1,18 +1,5 @@
import { Filter } from '@/application/database-yjs';
export enum TimeFormat {
TwelveHour = 0,
TwentyFourHour = 1,
}
export enum DateFormat {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
DayMonthYear = 4,
}
export enum DateFilterCondition {
DateStartsOn = 0,
DateStartsBefore = 1,

View File

@@ -1,6 +1,6 @@
import { getTimeFormat, getDateFormat } from './utils';
import { DateFormat, TimeFormat } from '@/application/types';
import { getDateFormat, getTimeFormat } from '@/utils/time';
import { expect } from '@jest/globals';
import { DateFormat, TimeFormat } from '@/application/database-yjs';
describe('DateFormat', () => {
it('should return time format', () => {

View File

@@ -1,37 +1,10 @@
import dayjs from 'dayjs';
import { DateFormat, getTypeOptions, TimeFormat } from '@/application/database-yjs';
import { getTypeOptions } from '@/application/database-yjs';
import { DateTimeCell } from '@/application/database-yjs/cell.type';
import { YDatabaseField, YjsDatabaseKey } from '@/application/types';
import { renderDate } from '@/utils/time';
export function getTimeFormat(timeFormat?: TimeFormat) {
switch (timeFormat) {
case TimeFormat.TwelveHour:
return 'h:mm A';
case TimeFormat.TwentyFourHour:
return 'HH:mm';
default:
return 'HH:mm';
}
}
export function getDateFormat(dateFormat?: DateFormat) {
switch (dateFormat) {
case DateFormat.Friendly:
return 'MMM DD, YYYY';
case DateFormat.ISO:
return 'YYYY-MM-DD';
case DateFormat.US:
return 'YYYY/MM/DD';
case DateFormat.Local:
return 'MM/DD/YYYY';
case DateFormat.DayMonthYear:
return 'DD/MM/YYYY';
default:
return 'YYYY-MM-DD';
}
}
import { DateFormat, TimeFormat, User, YDatabaseField, YjsDatabaseKey, YMapFieldTypeOption } from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import { getDateFormat, getTimeFormat, renderDate } from '@/utils/time';
function getDateTimeStr({
timeStamp,
@@ -59,39 +32,45 @@ function getDateTimeStr({
export const RIGHTWARDS_ARROW = '→';
export function getRowTimeString(field: YDatabaseField, timeStamp: string) {
export function getRowTimeString(field: YDatabaseField, timeStamp: string, currentUser?: User) {
const typeOption = getTypeOptions(field);
const typeOptionValue = getFieldDateTimeFormats(typeOption, currentUser);
const timeFormat = parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat;
const dateFormat = parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat;
const includeTime = typeOption.get(YjsDatabaseKey.include_time);
return getDateTimeStr({
timeStamp,
includeTime,
typeOptionValue: {
timeFormat,
dateFormat,
},
typeOptionValue,
});
}
export function getDateCellStr({ cell, field }: { cell: DateTimeCell; field: YDatabaseField }) {
export function getFieldDateTimeFormats(typeOption: YMapFieldTypeOption, currentUser?: User) {
const typeOptionTimeFormat = typeOption.get(YjsDatabaseKey.time_format);
const typeOptionDateFormat = typeOption.get(YjsDatabaseKey.date_format);
const dateFormat = typeOptionDateFormat
? parseInt(typeOptionDateFormat) as DateFormat
: currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat ?? DateFormat.Local;
const timeFormat = typeOptionTimeFormat
? parseInt(typeOptionTimeFormat) as TimeFormat
: currentUser?.metadata?.[MetadataKey.TimeFormat] as TimeFormat ?? TimeFormat.TwelveHour;
return {
dateFormat,
timeFormat,
}
}
export function getDateCellStr({ cell, field, currentUser }: { cell: DateTimeCell; field: YDatabaseField, currentUser?: User }) {
const typeOptionMap = field.get(YjsDatabaseKey.type_option);
const typeOption = typeOptionMap.get(String(cell.fieldType));
const timeFormat = parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat;
const dateFormat = parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat;
const typeOptionValue = getFieldDateTimeFormats(typeOption, currentUser);
const startData = cell.data || '';
const includeTime = cell.includeTime;
const typeOptionValue = {
timeFormat,
dateFormat,
};
const startDateTime = getDateTimeStr({
timeStamp: startData,
includeTime,

View File

@@ -13,15 +13,12 @@ import {
useRowDocMap,
} from '@/application/database-yjs/context';
import {
DateFormat,
getDateCellStr,
getDateFormat,
getTimeFormat,
getFieldDateTimeFormats,
getTypeOptions,
parseRelationTypeOption,
parseSelectOptionTypeOptions,
SelectOption,
TimeFormat,
} from '@/application/database-yjs/fields';
import { filterBy, parseFilter } from '@/application/database-yjs/filter';
import { groupByField } from '@/application/database-yjs/group';
@@ -38,7 +35,8 @@ import {
YjsDatabaseKey,
YjsEditorKey,
} from '@/application/types';
import { renderDate } from '@/utils/time';
import { useCurrentUser } from '@/components/main/app.hooks';
import { getDateFormat, getTimeFormat, renderDate } from '@/utils/time';
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMeta, SortCondition } from './database.type';
@@ -1145,28 +1143,32 @@ export const usePropertiesSelector = (isFilterHidden?: boolean) => {
};
export const useDateTimeCellString = (cell: DateTimeCell | undefined, fieldId: string) => {
const currentUser = useCurrentUser();
const { field, clock } = useFieldSelector(fieldId);
return useMemo(() => {
if (!cell) return null;
return getDateCellStr({ cell, field });
return getDateCellStr({ cell, field, currentUser });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cell, field, clock]);
}, [cell, field, clock, currentUser]);
};
export const useRowTimeString = (rowId: string, fieldId: string, attrName: string) => {
const currentUser = useCurrentUser();
const { field, clock } = useFieldSelector(fieldId);
const typeOptionValue = useMemo(() => {
const typeOption = getTypeOptions(field);
const { dateFormat, timeFormat } = getFieldDateTimeFormats(typeOption, currentUser);
return {
timeFormat: parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat,
dateFormat: parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat,
dateFormat,
timeFormat,
includeTime: typeOption.get(YjsDatabaseKey.include_time),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [field, clock]);
}, [field, clock, currentUser?.metadata]);
const getDateTimeStr = useCallback(
(timeStamp: string, includeTime?: boolean) => {

View File

@@ -344,8 +344,8 @@ export enum YjsDatabaseKey {
include_time = 'include_time',
is_range = 'is_range',
reminder_id = 'reminder_id',
time_format = 'time_format',
date_format = 'date_format',
time_format = 'time_format_v2',
date_format = 'date_format_v2',
calculations = 'calculations',
field_id = 'field_id',
calculation_value = 'calculation_value',
@@ -686,11 +686,11 @@ export interface YMapFieldTypeOption extends Y.Map<unknown> {
// CreatedTime, LastEditedTime, DateTime
// eslint-disable-next-line @typescript-eslint/unified-signatures
get(key: YjsDatabaseKey.time_format): string;
get(key: YjsDatabaseKey.time_format): string | undefined;
// CreatedTime, LastEditedTime, DateTime
// eslint-disable-next-line @typescript-eslint/unified-signatures
get(key: YjsDatabaseKey.date_format): string;
get(key: YjsDatabaseKey.date_format): string | undefined;
// Relation
get(key: YjsDatabaseKey.database_id): DatabaseId;
@@ -699,7 +699,7 @@ export interface YMapFieldTypeOption extends Y.Map<unknown> {
// eslint-disable-next-line @typescript-eslint/unified-signatures
get(key: YjsDatabaseKey.format): string;
// LastModified and CreatedTime
// LastEditedTime and CreatedTime
get(key: YjsDatabaseKey.include_time): boolean;
// AI Translate
@@ -1086,6 +1086,7 @@ export interface ViewComponentProps {
}>;
updatePageIcon?: (viewId: string, icon: { ty: ViewIconType; value: string }) => Promise<void>;
updatePageName?: (viewId: string, name: string) => Promise<void>;
currentUser?: User;
}
export interface CreatePagePayload {
@@ -1206,3 +1207,16 @@ export interface MentionablePerson {
invited: boolean;
last_mentioned_at: string | null;
}
export enum DateFormat {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
DayMonthYear = 4,
}
export enum TimeFormat {
TwelveHour = 0,
TwentyFourHour = 1,
}

View File

@@ -1,4 +1,6 @@
import { toZonedTime } from 'date-fns-tz';
import { DateFormat, TimeFormat } from './types';
import { UserTimezone } from './user-timezone.types';
/**
@@ -9,6 +11,8 @@ export enum MetadataKey {
Timezone = 'timezone',
Language = 'language',
DateFormat = 'date_format',
TimeFormat = 'time_format',
StartWeekOn = 'start_week_on',
IconUrl = 'icon_url',
}
@@ -18,18 +22,10 @@ export enum MetadataKey {
export interface MetadataValues {
[MetadataKey.Timezone]: string | UserTimezone;
[MetadataKey.Language]: string;
[MetadataKey.DateFormat]: DateFormatType;
[MetadataKey.DateFormat]: DateFormat;
[MetadataKey.TimeFormat]: TimeFormat;
[MetadataKey.StartWeekOn]: number;
[MetadataKey.IconUrl]: string;
[key: string]: string | DateFormatType | UserTimezone | Record<string, unknown> | number | boolean | null | undefined; // Allow custom keys
}
/**
* Supported date format types
*/
export enum DateFormatType {
US = 'MM/DD/YYYY',
EU = 'DD/MM/YYYY',
ISO = 'YYYY-MM-DD',
}
/**
@@ -38,7 +34,9 @@ export enum DateFormatType {
export const MetadataDefaults: Partial<MetadataValues> = {
[MetadataKey.Timezone]: 'UTC',
[MetadataKey.Language]: 'en',
[MetadataKey.DateFormat]: DateFormatType.US,
[MetadataKey.DateFormat]: DateFormat.US,
[MetadataKey.TimeFormat]: TimeFormat.TwelveHour,
[MetadataKey.StartWeekOn]: 0,
[MetadataKey.IconUrl]: '',
};
@@ -67,7 +65,7 @@ export class UserMetadataBuilder {
/**
* Set date format metadata
*/
setDateFormat(format: DateFormatType): this {
setDateFormat(format: DateFormat): this {
this.metadata[MetadataKey.DateFormat] = format;
return this;
}
@@ -80,14 +78,6 @@ export class UserMetadataBuilder {
return this;
}
/**
* Set custom metadata
*/
setCustom(key: string, value: string | DateFormatType | UserTimezone | Record<string, unknown> | number | boolean | null | undefined): this {
this.metadata[key] = value;
return this;
}
/**
* Build the final metadata object
*/
@@ -103,21 +93,21 @@ export const MetadataUtils = {
/**
* Detect user's preferred date format based on locale
*/
detectDateFormat(locale: string = navigator.language): DateFormatType {
detectDateFormat(locale: string = navigator.language): DateFormat {
const region = locale.split('-')[1]?.toUpperCase() || locale.toUpperCase();
// US format countries
if (['US', 'CA', 'PH'].includes(region)) {
return DateFormatType.US;
return DateFormat.US;
}
// ISO format preference
if (['SE', 'FI', 'JP', 'KR', 'CN', 'TW', 'HK'].includes(region)) {
return DateFormatType.ISO;
return DateFormat.ISO;
}
// Default to EU format for most other countries
return DateFormatType.EU;
return DateFormat.DayMonthYear;
},
/**

View File

@@ -1,9 +1,9 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { sortBy, uniqBy } from 'lodash-es';
import { validate as uuidValidate } from 'uuid';
import { View, DatabaseRelations, UIVariant , ViewLayout, MentionablePerson } from '@/application/types';
import { View, DatabaseRelations, UIVariant, ViewLayout, MentionablePerson } from '@/application/types';
import { findView, findViewByLayout } from '@/components/_shared/outline/utils';
import { createDeduplicatedNoArgsRequest } from '@/utils/deduplicateRequest';
import { useAuthInternal } from '../contexts/AuthInternalContext';
@@ -214,7 +214,9 @@ export function useWorkspaceData() {
}
}, [currentWorkspaceId, service]);
const loadMentionableUsers = createDeduplicatedNoArgsRequest(_loadMentionableUsers);
const loadMentionableUsers = useMemo(() => {
return createDeduplicatedNoArgsRequest(_loadMentionableUsers);
}, [_loadMentionableUsers]);
// Get mention user
const getMentionUser = useCallback(

View File

@@ -104,19 +104,7 @@ export const AppSyncLayer: React.FC<AppSyncLayerProps> = ({ children }) => {
return;
}
// Merge notification changes with existing user data
// Only update fields that are present in the notification (selective update)
const updatedUser = {
...existingUser,
// Update name if provided in notification
...(profileChange.name !== undefined && { name: profileChange.name }),
// Update email if provided in notification
...(profileChange.email !== undefined && { email: profileChange.email }),
};
// Update database cache - this triggers useLiveQuery to re-render all UI components
// displaying user profile information. No manual component updates needed.
await db.users.put(updatedUser, userId);
const updatedUser = service?.getCurrentUser();
console.log('User profile updated in database:', updatedUser);
} catch (error) {
@@ -131,21 +119,20 @@ export const AppSyncLayer: React.FC<AppSyncLayerProps> = ({ children }) => {
return () => {
currentEventEmitter.off(APP_EVENTS.USER_PROFILE_CHANGED, handleUserProfileChange);
};
}, [isAuthenticated, currentWorkspaceId]);
}, [isAuthenticated, currentWorkspaceId, service]);
// Context value for synchronization layer
const syncContextValue: SyncInternalContextType = useMemo(() => ({
const syncContextValue: SyncInternalContextType = useMemo(
() => ({
webSocket,
broadcastChannel,
registerSyncContext,
eventEmitter: eventEmitterRef.current,
awarenessMap,
lastUpdatedCollab,
}), [webSocket, broadcastChannel, registerSyncContext, awarenessMap, lastUpdatedCollab]);
return (
<SyncInternalContext.Provider value={syncContextValue}>
{children}
</SyncInternalContext.Provider>
}),
[webSocket, broadcastChannel, registerSyncContext, awarenessMap, lastUpdatedCollab]
);
return <SyncInternalContext.Provider value={syncContextValue}>{children}</SyncInternalContext.Provider>;
};

View File

@@ -0,0 +1,270 @@
import dayjs from 'dayjs';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DateFormat, TimeFormat } from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import { ReactComponent as ChevronDownIcon } from '@/assets/icons/alt_arrow_down.svg';
import { useCurrentUser, useService } from '@/components/main/app.hooks';
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';
export function AccountSettings({ children }: { children?: React.ReactNode }) {
const { t } = useTranslation();
const currentUser = useCurrentUser();
const service = useService();
const handleSelectDateFormat = useCallback(
async (dateFormat: number) => {
if (!service || !currentUser?.metadata) return;
await service?.updateUserProfile({ ...currentUser.metadata, [MetadataKey.DateFormat]: dateFormat });
},
[currentUser, service]
);
const handleSelectTimeFormat = useCallback(
async (timeFormat: number) => {
if (!service || !currentUser?.metadata) return;
await service?.updateUserProfile({ ...currentUser.metadata, [MetadataKey.TimeFormat]: timeFormat });
},
[currentUser, service]
);
const handleSelectStartWeekOn = useCallback(
async (startWeekOn: number) => {
if (!service || !currentUser?.metadata) return;
await service?.updateUserProfile({ ...currentUser.metadata, [MetadataKey.StartWeekOn]: startWeekOn });
},
[currentUser, service]
);
if (!currentUser || !service) {
return <></>;
}
const dateFormat = Number(currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat) || DateFormat.Local;
const timeFormat = Number(currentUser?.metadata?.[MetadataKey.TimeFormat] as TimeFormat) || TimeFormat.TwelveHour;
const startWeekOn = Number(currentUser?.metadata?.[MetadataKey.StartWeekOn] as number) || 0;
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className='flex h-[300px] min-h-0 w-[400px] flex-col gap-3 sm:max-w-[calc(100%-2rem)]'>
<DialogTitle className='text-md font-bold text-text-primary'>{t('web.accountSettings')}</DialogTitle>
<div className='flex min-h-0 w-full flex-1 flex-col items-start gap-3 py-4'>
<DateFormatDropdown dateFormat={dateFormat} onSelect={handleSelectDateFormat} />
<TimeFormatDropdown timeFormat={timeFormat} onSelect={handleSelectTimeFormat} />
<StartWeekOnDropdown startWeekOn={startWeekOn} onSelect={handleSelectStartWeekOn} />
</div>
</DialogContent>
</Dialog>
);
}
function DateFormatDropdown({ dateFormat, onSelect }: { dateFormat: number; onSelect: (dateFormat: number) => void }) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const dateFormats = useMemo(
() => [
{
value: DateFormat.Local,
label: t('grid.field.dateFormatLocal'),
},
{
label: t('grid.field.dateFormatUS'),
value: DateFormat.US,
},
{
label: t('grid.field.dateFormatISO'),
value: DateFormat.ISO,
},
{
label: t('grid.field.dateFormatFriendly'),
value: DateFormat.Friendly,
},
{
label: t('grid.field.dateFormatDayMonthYear'),
value: DateFormat.DayMonthYear,
},
],
[t]
);
const value = dateFormats.find((format) => format.value === dateFormat);
return (
<div className='flex flex-col items-start gap-1'>
<span className='text-xs font-medium text-text-secondary'>{t('grid.field.dateFormat')}</span>
<div className='relative'>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen} modal={false}>
<DropdownMenuTrigger
asChild
onPointerDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => setIsOpen((prev) => !prev)}
>
<div
className={cn(
'flex h-8 flex-1 cursor-default items-center gap-1 rounded-300 border px-2 text-sm font-normal',
isOpen ? 'border-border-theme-thick' : 'border-border-primary hover:border-border-primary-hover'
)}
>
<span className='flex-1 truncate' onMouseDown={(e) => e.preventDefault()}>
{value?.label || t('settings.workspacePage.dateTime.dateFormat.label')}
</span>
<ChevronDownIcon className='text-icon-primary' />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-[--radix-dropdown-menu-trigger-width]' align='start'>
<DropdownMenuRadioGroup value={dateFormat.toString()} onValueChange={(value) => onSelect(Number(value))}>
{dateFormats.map((item) => (
<DropdownMenuRadioItem key={item.value} value={item.value.toString()}>
{item.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
function TimeFormatDropdown({ timeFormat, onSelect }: { timeFormat: number; onSelect: (timeFormat: number) => void }) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const timeFormats = useMemo(
() => [
{
value: TimeFormat.TwelveHour,
label: t('grid.field.timeFormatTwelveHour'),
},
{
label: t('grid.field.timeFormatTwentyFourHour'),
value: TimeFormat.TwentyFourHour,
},
],
[t]
);
const value = timeFormats.find((format) => format.value === timeFormat);
return (
<div className='flex flex-col items-start gap-1'>
<span className='text-xs font-medium text-text-secondary'>{t('grid.field.timeFormat')}</span>
<div className='relative'>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen} modal={false}>
<DropdownMenuTrigger
asChild
onPointerDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => setIsOpen((prev) => !prev)}
>
<div
className={cn(
'flex h-8 flex-1 cursor-default items-center gap-1 rounded-300 border px-2 text-sm font-normal',
isOpen ? 'border-border-theme-thick' : 'border-border-primary hover:border-border-primary-hover'
)}
>
<span className='flex-1 truncate' onMouseDown={(e) => e.preventDefault()}>
{value?.label || t('grid.field.timeFormatTwelveHour')}
</span>
<ChevronDownIcon className='text-icon-primary' />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-[--radix-dropdown-menu-trigger-width]' align='start'>
<DropdownMenuRadioGroup value={timeFormat.toString()} onValueChange={(value) => onSelect(Number(value))}>
{timeFormats.map((item) => (
<DropdownMenuRadioItem key={item.value} value={item.value.toString()}>
{item.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
function StartWeekOnDropdown({
startWeekOn,
onSelect,
}: {
startWeekOn: number;
onSelect: (startWeekOn: number) => void;
}) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const daysOfWeek = [
{
value: 0,
label: dayjs().day(0).format('dddd'),
},
{
value: 1,
label: dayjs().day(1).format('dddd'),
},
] as const;
const value = daysOfWeek.find((format) => format.value === startWeekOn);
return (
<div className='flex flex-col items-start gap-1'>
<span className='text-xs font-medium text-text-secondary'>{t('web.startWeekOn')}</span>
<div className='relative'>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen} modal={false}>
<DropdownMenuTrigger
asChild
onPointerDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => setIsOpen((prev) => !prev)}
>
<div
className={cn(
'flex h-8 flex-1 cursor-default items-center gap-1 rounded-300 border px-2 text-sm font-normal',
isOpen ? 'border-border-theme-thick' : 'border-border-primary hover:border-border-primary-hover'
)}
>
<span className='flex-1 truncate' onMouseDown={(e) => e.preventDefault()}>
{value?.label || t('grid.field.timeFormatTwelveHour')}
</span>
<ChevronDownIcon className='text-icon-primary' />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-[--radix-dropdown-menu-trigger-width]' align='start'>
<DropdownMenuRadioGroup value={startWeekOn.toString()} onValueChange={(value) => onSelect(Number(value))}>
{daysOfWeek.map((item) => (
<DropdownMenuRadioItem key={item.value} value={item.value.toString()}>
{item.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}

View File

@@ -6,11 +6,13 @@ import { invalidToken } from '@/application/session/token';
import { Workspace } from '@/application/types';
import { ReactComponent as UpgradeAIMaxIcon } from '@/assets/icons/ai.svg';
import { ReactComponent as ChevronDownIcon } from '@/assets/icons/alt_arrow_down.svg';
import { ReactComponent as ChevronRightIcon } from '@/assets/icons/alt_arrow_right.svg';
import { ReactComponent as TipIcon } from '@/assets/icons/help.svg';
import { ReactComponent as AddUserIcon } from '@/assets/icons/invite_user.svg';
import { ReactComponent as LogoutIcon } from '@/assets/icons/logout.svg';
import { ReactComponent as AddIcon } from '@/assets/icons/plus.svg';
import { ReactComponent as ImportIcon } from '@/assets/icons/save_as.svg';
import { ReactComponent as SettingsIcon } from '@/assets/icons/settings.svg';
import { ReactComponent as UpgradeIcon } from '@/assets/icons/upgrade.svg';
import { useAppHandlers, useCurrentWorkspaceId, useUserWorkspaceInfo } from '@/components/app/app.hooks';
import CurrentWorkspace from '@/components/app/workspaces/CurrentWorkspace';
@@ -37,6 +39,8 @@ import Import from '@/components/_shared/more-actions/importer/Import';
import { notify } from '@/components/_shared/notify';
import { openUrl } from '@/utils/url';
import { AccountSettings } from './AccountSettings';
export function Workspaces() {
const { t } = useTranslation();
const userWorkspaceInfo = useUserWorkspaceInfo();
@@ -211,6 +215,13 @@ export function Workspaces() {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<AccountSettings>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<SettingsIcon />
<div className={'flex-1 text-left'}>{t('web.accountSettings')}</div>
<ChevronRightIcon className='text-icon-tertiary' />
</DropdownMenuItem>
</AccountSettings>
<DropdownMenuItem onSelect={handleSignOut}>
<LogoutIcon />
{t('button.logout')}

View File

@@ -17,6 +17,7 @@ import { languageTexts, parseAITranslateTypeOption } from '@/application/databas
import { GenerateAITranslateRowPayload, YDatabaseCell, YDatabaseField, YjsDatabaseKey } from '@/application/types';
import { ReactComponent as AIIcon } from '@/assets/icons/ai_improve_writing.svg';
import { ReactComponent as CopyIcon } from '@/assets/icons/copy.svg';
import { useCurrentUser } from '@/components/main/app.hooks';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -56,18 +57,20 @@ function AITextCellActions({
const row = useRowData(rowId);
const updateCell = useUpdateCellDispatch(rowId, fieldId);
const { generateAITranslateForRow, generateAISummaryForRow } = useDatabaseContext();
const currentUser = useCurrentUser();
const getCellData = useCallback(
(cell: YDatabaseCell, field: YDatabaseField) => {
if (!currentUser) return '';
const type = Number(field?.get(YjsDatabaseKey.type));
if (type === FieldType.CreatedTime) {
return getRowTimeString(field, row.get(YjsDatabaseKey.created_at)) || '';
return getRowTimeString(field, row.get(YjsDatabaseKey.created_at), currentUser) || '';
} else if (type === FieldType.LastEditedTime) {
return getRowTimeString(field, row.get(YjsDatabaseKey.last_modified)) || '';
return getRowTimeString(field, row.get(YjsDatabaseKey.last_modified), currentUser) || '';
} else if (cell && ![FieldType.AISummaries, FieldType.AITranslations].includes(type)) {
try {
return getCellDataText(cell, field);
return getCellDataText(cell, field, currentUser);
} catch (e) {
console.error(e);
return '';
@@ -76,7 +79,7 @@ function AITextCellActions({
return '';
},
[row]
[currentUser, row]
);
const handleGenerateSummary = useCallback(async () => {

View File

@@ -4,20 +4,19 @@ import { useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
DateFormat,
getDateFormat,
getTimeFormat,
getTypeOptions, TimeFormat,
getFieldDateTimeFormats,
getTypeOptions,
useFieldSelector,
} from '@/application/database-yjs';
import { DateTimeCell } from '@/application/database-yjs/cell.type';
import { useUpdateCellDispatch } from '@/application/database-yjs/dispatch';
import { YjsDatabaseKey } from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import { ReactComponent as ChevronRight } from '@/assets/icons/alt_arrow_right.svg';
import { ReactComponent as DateSvg } from '@/assets/icons/date.svg';
import { ReactComponent as TimeIcon } from '@/assets/icons/time.svg';
import DateTimeFormatMenu from '@/components/database/components/cell/date/DateTimeFormatMenu';
import DateTimeInput from '@/components/database/components/cell/date/DateTimeInput';
import { useCurrentUser } from '@/components/main/app.hooks';
import { Calendar } from '@/components/ui/calendar';
import { dropdownMenuItemVariants } from '@/components/ui/dropdown-menu';
import {
@@ -28,6 +27,7 @@ import {
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { cn } from '@/lib/utils';
import { getDateFormat, getTimeFormat } from '@/utils/time';
function DateTimeCellPicker ({ open, onOpenChange, cell, fieldId, rowId }: {
open: boolean;
@@ -36,6 +36,7 @@ function DateTimeCellPicker ({ open, onOpenChange, cell, fieldId, rowId }: {
fieldId: string;
rowId: string;
}) {
const currentUser = useCurrentUser();
const { t } = useTranslation();
const [isRange, setIsRange] = useState<boolean>(() => {
@@ -56,13 +57,20 @@ function DateTimeCellPicker ({ open, onOpenChange, cell, fieldId, rowId }: {
const typeOptionValue = useMemo(() => {
const typeOption = getTypeOptions(field);
const { dateFormat, timeFormat } = getFieldDateTimeFormats(typeOption, currentUser);
return {
timeFormat: getTimeFormat(Number(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat),
dateFormat: getDateFormat(Number(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat),
dateFormat: getDateFormat(dateFormat),
timeFormat: getTimeFormat(timeFormat),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [field, clock]);
}, [field, clock, currentUser?.metadata]);
const weekStartsOn = useMemo(() => {
const value = Number(currentUser?.metadata?.[MetadataKey.StartWeekOn]) || 0;
return value >= 0 && value <= 6 ? (value as 0 | 1 | 2 | 3 | 4 | 5 | 6) : 0;
}, [currentUser?.metadata]);
const updateCell = useUpdateCellDispatch(rowId, fieldId);
@@ -193,6 +201,7 @@ function DateTimeCellPicker ({ open, onOpenChange, cell, fieldId, rowId }: {
showOutsideDays
month={month}
onMonthChange={onMonthChange}
weekStartsOn={weekStartsOn}
{...(isRange ? {
mode: 'range',
selected: dateRange,

View File

@@ -23,8 +23,9 @@ import {
YjsDatabaseKey,
YjsEditorKey,
} from '@/application/types';
import { Editor } from '@/components/editor';
import { EditorSkeleton } from '@/components/_shared/skeleton/EditorSkeleton';
import { Editor } from '@/components/editor';
import { useCurrentUser } from '@/components/main/app.hooks';
export const DatabaseRowSubDocument = memo(({ rowId }: { rowId: string }) => {
const meta = useRowMetaSelector(rowId);
@@ -34,6 +35,8 @@ export const DatabaseRowSubDocument = memo(({ rowId }: { rowId: string }) => {
const database = useDatabase();
const row = useRowData(rowId) as YDatabaseRow | undefined;
const checkIfRowDocumentExists = context.checkIfRowDocumentExists;
const { createOrphanedView, loadView } = context;
const currentUser = useCurrentUser();
const getCellData = useCallback(
(cell: YDatabaseCell, field: YDatabaseField) => {
@@ -41,12 +44,12 @@ export const DatabaseRowSubDocument = memo(({ rowId }: { rowId: string }) => {
const type = Number(field?.get(YjsDatabaseKey.type));
if (type === FieldType.CreatedTime) {
return getRowTimeString(field, row.get(YjsDatabaseKey.created_at)) || '';
return getRowTimeString(field, row.get(YjsDatabaseKey.created_at), currentUser) || '';
} else if (type === FieldType.LastEditedTime) {
return getRowTimeString(field, row.get(YjsDatabaseKey.last_modified)) || '';
return getRowTimeString(field, row.get(YjsDatabaseKey.last_modified), currentUser) || '';
} else if (cell) {
try {
return getCellDataText(cell, field);
return getCellDataText(cell, field, currentUser);
} catch (e) {
console.error(e);
return '';
@@ -55,7 +58,7 @@ export const DatabaseRowSubDocument = memo(({ rowId }: { rowId: string }) => {
return '';
},
[row]
[row, currentUser]
);
const properties = useMemo(() => {
@@ -82,7 +85,6 @@ export const DatabaseRowSubDocument = memo(({ rowId }: { rowId: string }) => {
return obj;
}, [database, getCellData, row]);
const { createOrphanedView, loadView } = context;
const updateRowMeta = useUpdateRowMetaDispatch(rowId);
const [loading, setLoading] = useState(true);

View File

@@ -1,22 +1,26 @@
import dayjs from 'dayjs';
import { useCallback, useMemo, useState } from 'react';
import {
DateFilter,
DateFilterCondition,
DateFormat,
getDateFormat,
getTimeFormat,
TimeFormat,
} from '@/application/database-yjs';
import { DateFilter, DateFilterCondition } from '@/application/database-yjs';
import { useUpdateFilter } from '@/application/database-yjs/dispatch';
import { DateFormat, TimeFormat } from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import DateTimeInput from '@/components/database/components/cell/date/DateTimeInput';
import { useCurrentUser } from '@/components/main/app.hooks';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { renderDate } from '@/utils/time';
import { getDateFormat, getTimeFormat, renderDate } from '@/utils/time';
function DateTimeFilterDatePicker({ filter }: { filter: DateFilter }) {
const currentUser = useCurrentUser();
const weekStartsOn = useMemo(() => {
const value = Number(currentUser?.metadata?.[MetadataKey.StartWeekOn]) || 0;
return value >= 0 && value <= 6 ? (value as 0 | 1 | 2 | 3 | 4 | 5 | 6) : 0;
}, [currentUser?.metadata]);
const isRange = useMemo(() => {
return [DateFilterCondition.DateStartsBetween, DateFilterCondition.DateEndsBetween].includes(filter.condition);
}, [filter.condition]);
@@ -74,9 +78,11 @@ function DateTimeFilterDatePicker({ filter }: { filter: DateFilter }) {
const { timestamp, end, start } = filter;
if (isRange && start && end) {
return `${renderDate(start.toString(), getDateFormat(DateFormat.Local), true)} - ${renderDate(
const dateFormat = currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat | DateFormat.Local;
return `${renderDate(start.toString(), getDateFormat(dateFormat), true)} - ${renderDate(
end.toString(),
getDateFormat(DateFormat.Local),
getDateFormat(dateFormat),
true
)}`;
}
@@ -84,7 +90,7 @@ function DateTimeFilterDatePicker({ filter }: { filter: DateFilter }) {
if (!timestamp) return '';
return renderDate(timestamp.toString(), getDateFormat(DateFormat.Local), true);
}, [filter, isRange]);
}, [filter, isRange, currentUser?.metadata]);
const [month, setMonth] = useState<Date | undefined>(undefined);
@@ -135,6 +141,7 @@ function DateTimeFilterDatePicker({ filter }: { filter: DateFilter }) {
showOutsideDays
month={month}
onMonthChange={onMonthChange}
weekStartsOn={weekStartsOn}
{...(isRange
? {
mode: 'range',

View File

@@ -1,10 +1,16 @@
import { DateFilter, DateFilterCondition, DateFormat, getDateFormat } from '@/application/database-yjs';
import dayjs from 'dayjs';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
function DateFilterContentOverview ({ filter }: { filter: DateFilter }) {
import { DateFilter, DateFilterCondition, } from '@/application/database-yjs';
import { DateFormat } from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import { getDateFormat } from '@/utils/time';
import { useCurrentUser } from '@/components/main/app.hooks';
function DateFilterContentOverview({ filter }: { filter: DateFilter }) {
const { t } = useTranslation();
const currentUser = useCurrentUser();
const value = useMemo(() => {
let startStr = '';
@@ -18,7 +24,8 @@ function DateFilterContentOverview ({ filter }: { filter: DateFilter }) {
endStr = dayjs.unix(end).format(format);
}
const timestamp = filter.timestamp ? dayjs.unix(filter.timestamp).format(getDateFormat(DateFormat.Local)) : '';
const dateFormat = currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat | DateFormat.Local;
const timestamp = filter.timestamp ? dayjs.unix(filter.timestamp).format(getDateFormat(dateFormat)) : '';
switch (filter.condition) {
case DateFilterCondition.DateStartsOn:
@@ -48,7 +55,7 @@ function DateFilterContentOverview ({ filter }: { filter: DateFilter }) {
default:
return '';
}
}, [filter, t]);
}, [filter, t, currentUser?.metadata]);
return <>{value}</>;
}

View File

@@ -1,6 +1,5 @@
import { DateFormat, TimeFormat } from '@/application/database-yjs';
import { useUpdateDateTimeFieldFormat } from '@/application/database-yjs/dispatch';
import { YjsDatabaseKey } from '@/application/types';
import { DateFormat, TimeFormat, YjsDatabaseKey } from '@/application/types';
import { useFieldTypeOption } from '@/components/database/components/cell/Cell.hooks';
import {
DropdownMenuGroup, DropdownMenuItem, DropdownMenuItemTick,
@@ -18,8 +17,11 @@ function DateTimeFormatGroup ({
}) {
const { t } = useTranslation();
const typeOption = useFieldTypeOption(fieldId);
const selectedDateFormat = Number(typeOption.get(YjsDatabaseKey.date_format));
const selectedTimeFormat = Number(typeOption.get(YjsDatabaseKey.time_format));
const typeOptionDateFormat = typeOption.get(YjsDatabaseKey.date_format);
const typeOptionTimeFormat = typeOption.get(YjsDatabaseKey.time_format);
const selectedDateFormat = typeOptionDateFormat !== undefined ? Number(typeOptionDateFormat) : undefined;
const selectedTimeFormat = typeOptionTimeFormat !== undefined ? Number(typeOptionTimeFormat) : undefined;
const updateFormat = useUpdateDateTimeFieldFormat(fieldId);
const dateFormats = useMemo(() => [{

View File

@@ -1,12 +1,20 @@
import { ReactComponent as DateSvg } from '@/assets/icons/date.svg';
import { ReactComponent as ReminderSvg } from '@/assets/icons/reminder_clock.svg';
import { renderDate } from '@/utils/time';
import { useMemo } from 'react';
import { DateFormat } from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import { ReactComponent as DateSvg } from '@/assets/icons/date.svg';
import { ReactComponent as ReminderSvg } from '@/assets/icons/reminder_clock.svg';
import { useCurrentUser } from '@/components/main/app.hooks';
import { getDateFormat, renderDate } from '@/utils/time';
function MentionDate({ date, reminder }: { date: string; reminder?: { id: string; option: string } }) {
const dateFormat = useMemo(() => {
return renderDate(date, 'MMM D, YYYY');
}, [date]);
const currentUser = useCurrentUser();
const formattedDate = useMemo(() => {
const dateFormat = (currentUser?.metadata?.[MetadataKey.DateFormat] as DateFormat) ?? DateFormat.Local;
return renderDate(date, getDateFormat(dateFormat));
}, [currentUser?.metadata, date]);
return (
<span
@@ -17,7 +25,7 @@ function MentionDate({ date, reminder }: { date: string; reminder?: { id: string
>
<span className={'mention-content ml-0 px-0'}>
<span>@</span>
{dateFormat}
{formattedDate}
</span>
{reminder ? <ReminderSvg /> : <DateSvg />}
</span>

View File

@@ -7,6 +7,8 @@ import { ReactComponent as ChevronRightIcon } from '@/assets/icons/alt_arrow_rig
import { ReactComponent as CheckIcon } from '@/assets/icons/tick.svg';
import { cn } from '@/lib/utils';
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot='dropdown-menu' {...props} />;
}
@@ -112,6 +114,28 @@ const DropdownMenuItem = forwardRef<
);
});
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex items-center rounded-[8px] px-2 py-1.5',
'cursor-default select-none text-sm',
'outline-none transition-colors focus:bg-fill-content-hover',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'data-[state=checked]:bg-fill-theme-select',
className
)}
{...props}
>
{children}
</DropdownMenuPrimitive.RadioItem>
));
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
function DropdownMenuLabel({
className,
inset,
@@ -245,6 +269,7 @@ export {
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuItemTick,
DropdownMenuRadioItem,
dropdownMenuItemVariants,
DropdownMenuLabel,
DropdownMenuPortal,
@@ -254,4 +279,5 @@ export {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuRadioGroup,
};

View File

@@ -1,3 +1,4 @@
import { DateFormat, TimeFormat } from '@/application/types';
import dayjs from 'dayjs';
export function renderDate(date: string | number, format: string, isUnix?: boolean): string {
@@ -61,3 +62,31 @@ export function isTimestampBetweenRange(timestamp: string, startTimestamp: strin
return dateUnix >= startUnix && dateUnix <= endUnix;
}
export function getTimeFormat(timeFormat?: TimeFormat) {
switch (timeFormat) {
case TimeFormat.TwelveHour:
return 'h:mm A';
case TimeFormat.TwentyFourHour:
return 'HH:mm';
default:
return 'HH:mm';
}
}
export function getDateFormat(dateFormat?: DateFormat) {
switch (dateFormat) {
case DateFormat.Friendly:
return 'MMM DD, YYYY';
case DateFormat.ISO:
return 'YYYY-MM-DD';
case DateFormat.US:
return 'YYYY/MM/DD';
case DateFormat.Local:
return 'MM/DD/YYYY';
case DateFormat.DayMonthYear:
return 'DD/MM/YYYY';
default:
return 'YYYY-MM-DD';
}
}