feat: daily notes (dev only) (#1894)
This commit is contained in:
@@ -28,6 +28,10 @@ export class SNTag extends DecryptedItem<TagContent> implements TagContentSpecia
|
|||||||
this.preferences = this.payload.content.preferences
|
this.preferences = this.payload.content.preferences
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isDailyEntry(): boolean {
|
||||||
|
return this.preferences?.entryMode === 'daily'
|
||||||
|
}
|
||||||
|
|
||||||
get noteReferences(): ContentReference[] {
|
get noteReferences(): ContentReference[] {
|
||||||
const references = this.payload.references
|
const references = this.payload.references
|
||||||
return references.filter((ref) => ref.content_type === ContentType.Note)
|
return references.filter((ref) => ref.content_type === ContentType.Note)
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ export interface TagPreferences {
|
|||||||
newNoteTitleFormat?: NewNoteTitleFormat
|
newNoteTitleFormat?: NewNoteTitleFormat
|
||||||
customNoteTitleFormat?: string
|
customNoteTitleFormat?: string
|
||||||
editorIdentifier?: FeatureIdentifier | string
|
editorIdentifier?: FeatureIdentifier | string
|
||||||
|
entryMode?: 'normal' | 'daily'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
DeletedItemInterface,
|
DeletedItemInterface,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
PredicateInterface,
|
PredicateInterface,
|
||||||
|
DecryptedPayload,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ export interface ItemManagerInterface extends AbstractService {
|
|||||||
>(
|
>(
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
content?: C,
|
content?: C,
|
||||||
|
override?: Partial<DecryptedPayload<C>>,
|
||||||
): I
|
): I
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
BackupFile,
|
BackupFile,
|
||||||
DecryptedItemInterface,
|
DecryptedItemInterface,
|
||||||
DecryptedItemMutator,
|
DecryptedItemMutator,
|
||||||
|
DecryptedPayload,
|
||||||
EncryptedItemInterface,
|
EncryptedItemInterface,
|
||||||
FileItem,
|
FileItem,
|
||||||
ItemContent,
|
ItemContent,
|
||||||
@@ -117,6 +118,7 @@ export interface MutatorClientInterface {
|
|||||||
>(
|
>(
|
||||||
contentType: ContentType,
|
contentType: ContentType,
|
||||||
content?: C,
|
content?: C,
|
||||||
|
override?: Partial<DecryptedPayload<C>>,
|
||||||
): I
|
): I
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('note view controller', () => {
|
|||||||
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
|
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
|
||||||
ContentType.Note,
|
ContentType.Note,
|
||||||
expect.objectContaining({ noteType: NoteType.Plain }),
|
expect.objectContaining({ noteType: NoteType.Plain }),
|
||||||
|
expect.anything(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
private removeStreamObserver?: () => void
|
private removeStreamObserver?: () => void
|
||||||
public isTemplateNote = false
|
public isTemplateNote = false
|
||||||
private saveTimeout?: ReturnType<typeof setTimeout>
|
private saveTimeout?: ReturnType<typeof setTimeout>
|
||||||
private defaultTitle: string | undefined
|
|
||||||
private defaultTagUuid: UuidString | undefined
|
private defaultTagUuid: UuidString | undefined
|
||||||
private defaultTag?: SNTag
|
private defaultTag?: SNTag
|
||||||
public runtimeId = `${Math.random()}`
|
public runtimeId = `${Math.random()}`
|
||||||
@@ -42,14 +41,13 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
constructor(
|
constructor(
|
||||||
private application: SNApplication,
|
private application: SNApplication,
|
||||||
item?: SNNote,
|
item?: SNNote,
|
||||||
templateNoteOptions?: TemplateNoteViewControllerOptions,
|
public templateNoteOptions?: TemplateNoteViewControllerOptions,
|
||||||
) {
|
) {
|
||||||
if (item) {
|
if (item) {
|
||||||
this.item = item
|
this.item = item
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templateNoteOptions) {
|
if (templateNoteOptions) {
|
||||||
this.defaultTitle = templateNoteOptions.title
|
|
||||||
this.defaultTagUuid = templateNoteOptions.tag
|
this.defaultTagUuid = templateNoteOptions.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +78,19 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
? this.application.componentManager.componentWithIdentifier(editorIdentifier)
|
? this.application.componentManager.componentWithIdentifier(editorIdentifier)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(ContentType.Note, {
|
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(
|
||||||
text: '',
|
ContentType.Note,
|
||||||
title: this.defaultTitle || '',
|
{
|
||||||
noteType: defaultEditor?.noteType || NoteType.Plain,
|
text: '',
|
||||||
editorIdentifier: editorIdentifier,
|
title: this.templateNoteOptions?.title || '',
|
||||||
references: [],
|
noteType: defaultEditor?.noteType || NoteType.Plain,
|
||||||
})
|
editorIdentifier: editorIdentifier,
|
||||||
|
references: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
created_at: this.templateNoteOptions?.createdAt || new Date(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
this.isTemplateNote = true
|
this.isTemplateNote = true
|
||||||
this.item = note
|
this.item = note
|
||||||
@@ -109,6 +113,10 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private streamItems() {
|
private streamItems() {
|
||||||
|
if (this.dealloced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.removeStreamObserver = this.application.streamItems<SNNote>(
|
this.removeStreamObserver = this.application.streamItems<SNNote>(
|
||||||
ContentType.Note,
|
ContentType.Note,
|
||||||
({ changed, inserted, source }) => {
|
({ changed, inserted, source }) => {
|
||||||
|
|||||||
@@ -3,4 +3,8 @@ import { UuidString } from '@Lib/Types/UuidString'
|
|||||||
export type TemplateNoteViewControllerOptions = {
|
export type TemplateNoteViewControllerOptions = {
|
||||||
title?: string
|
title?: string
|
||||||
tag?: UuidString
|
tag?: UuidString
|
||||||
|
createdAt?: Date
|
||||||
|
autofocusBehavior?: TemplateNoteViewAutofocusBehavior
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TemplateNoteViewAutofocusBehavior = 'title' | 'editor'
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ export const STRING_SAVING_WHILE_DOCUMENT_HIDDEN =
|
|||||||
export const STRING_INVALID_NOTE =
|
export const STRING_INVALID_NOTE =
|
||||||
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."
|
"The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note."
|
||||||
export const STRING_ELLIPSES = '...'
|
export const STRING_ELLIPSES = '...'
|
||||||
export const NOTE_PREVIEW_CHAR_LIMIT = 80
|
export const NOTE_PREVIEW_CHAR_LIMIT = 160
|
||||||
export const SAVE_TIMEOUT_DEBOUNCE = 350
|
export const SAVE_TIMEOUT_DEBOUNCE = 350
|
||||||
export const SAVE_TIMEOUT_NO_DEBOUNCE = 100
|
export const SAVE_TIMEOUT_NO_DEBOUNCE = 100
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './NoteViewController'
|
|||||||
export * from './FileViewController'
|
export * from './FileViewController'
|
||||||
export * from './ItemGroupController'
|
export * from './ItemGroupController'
|
||||||
export * from './ReactNativeToWebEvent'
|
export * from './ReactNativeToWebEvent'
|
||||||
|
export * from './TemplateNoteViewControllerOptions'
|
||||||
|
|||||||
@@ -805,12 +805,13 @@ export class ItemManager
|
|||||||
public createTemplateItem<
|
public createTemplateItem<
|
||||||
C extends Models.ItemContent = Models.ItemContent,
|
C extends Models.ItemContent = Models.ItemContent,
|
||||||
I extends Models.DecryptedItemInterface<C> = Models.DecryptedItemInterface<C>,
|
I extends Models.DecryptedItemInterface<C> = Models.DecryptedItemInterface<C>,
|
||||||
>(contentType: ContentType, content?: C): I {
|
>(contentType: ContentType, content?: C, override?: Partial<Models.DecryptedPayload<C>>): I {
|
||||||
const payload = new Models.DecryptedPayload<C>({
|
const payload = new Models.DecryptedPayload<C>({
|
||||||
uuid: UuidGenerator.GenerateUuid(),
|
uuid: UuidGenerator.GenerateUuid(),
|
||||||
content_type: contentType,
|
content_type: contentType,
|
||||||
content: Models.FillItemContent<C>(content || {}),
|
content: Models.FillItemContent<C>(content || {}),
|
||||||
...Models.PayloadTimestampDefaults(),
|
...Models.PayloadTimestampDefaults(),
|
||||||
|
...override,
|
||||||
})
|
})
|
||||||
const item = Models.CreateDecryptedItemFromPayload<C, I>(payload)
|
const item = Models.CreateDecryptedItemFromPayload<C, I>(payload)
|
||||||
return item
|
return item
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
CreateEncryptedBackupFileContextPayload,
|
CreateEncryptedBackupFileContextPayload,
|
||||||
DecryptedItemInterface,
|
DecryptedItemInterface,
|
||||||
DecryptedItemMutator,
|
DecryptedItemMutator,
|
||||||
|
DecryptedPayload,
|
||||||
DecryptedPayloadInterface,
|
DecryptedPayloadInterface,
|
||||||
EncryptedItemInterface,
|
EncryptedItemInterface,
|
||||||
FileItem,
|
FileItem,
|
||||||
@@ -221,8 +222,8 @@ export class MutatorService extends AbstractService implements MutatorClientInte
|
|||||||
public createTemplateItem<
|
public createTemplateItem<
|
||||||
C extends ItemContent = ItemContent,
|
C extends ItemContent = ItemContent,
|
||||||
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
|
||||||
>(contentType: ContentType, content?: C): I {
|
>(contentType: ContentType, content?: C, override?: Partial<DecryptedPayload<C>>): I {
|
||||||
return this.itemManager.createTemplateItem(contentType, content)
|
return this.itemManager.createTemplateItem(contentType, content, override)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setItemNeedsSync(
|
public async setItemNeedsSync(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
--sn-stylekit-danger-color: #cc2128;
|
--sn-stylekit-danger-color: #cc2128;
|
||||||
--sn-stylekit-danger-contrast-color: #ffffff;
|
--sn-stylekit-danger-contrast-color: #ffffff;
|
||||||
|
--sn-stylekit-danger-light-color: #f9e4e5;
|
||||||
|
|
||||||
--sn-stylekit-shadow-color: #c8c8c8;
|
--sn-stylekit-shadow-color: #c8c8c8;
|
||||||
--sn-stylekit-background-color: #ffffff;
|
--sn-stylekit-background-color: #ffffff;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useState, useEffect, FunctionComponent, useMemo } from 'react'
|
||||||
|
import { CalendarActivity } from './CalendarActivity'
|
||||||
|
import CalendarDay from './CalendarDay'
|
||||||
|
import { CalendarDays, CalendarDaysLeap, CalendarDaysOfTheWeek } from './Constants'
|
||||||
|
import { createActivityRecord, dateToDateOnlyString, isLeapYear, getStartDayOfMonth } from './CalendarUtilts'
|
||||||
|
import { isDateInSameDay } from '@/Utils/DateUtils'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activities: CalendarActivity[]
|
||||||
|
onDateSelect: (date: Date) => void
|
||||||
|
startDate: Date
|
||||||
|
className?: string
|
||||||
|
selectedDay?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
const Calendar: FunctionComponent<Props> = ({ activities, startDate, onDateSelect, selectedDay, className }) => {
|
||||||
|
const activityMap = useMemo(() => createActivityRecord(activities), [activities])
|
||||||
|
|
||||||
|
const [date, setDate] = useState(startDate || new Date())
|
||||||
|
const [month, setMonth] = useState(date.getMonth())
|
||||||
|
const [year, setYear] = useState(date.getFullYear())
|
||||||
|
const [startDay, setStartDay] = useState(getStartDayOfMonth(date))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDate(startDate)
|
||||||
|
setMonth(startDate.getMonth())
|
||||||
|
setYear(startDate.getFullYear())
|
||||||
|
setStartDay(getStartDayOfMonth(startDate))
|
||||||
|
}, [startDate])
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const days = isLeapYear(year) ? CalendarDaysLeap : CalendarDays
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-300 ${className} border-left border-right border border-neutral`}>
|
||||||
|
<div className="mr-auto ml-auto w-70">
|
||||||
|
<div className="flex w-full flex-wrap">
|
||||||
|
{CalendarDaysOfTheWeek.map((d) => (
|
||||||
|
<div className={'flex h-8 w-[14.2%] items-center justify-center'} key={d}>
|
||||||
|
{d}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-wrap">
|
||||||
|
{Array(days[month] + (startDay - 1))
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => {
|
||||||
|
const d = index - (startDay - 2)
|
||||||
|
const date = new Date(year, month, d)
|
||||||
|
const activities = activityMap[dateToDateOnlyString(date)] || []
|
||||||
|
return (
|
||||||
|
<CalendarDay
|
||||||
|
key={index}
|
||||||
|
day={d}
|
||||||
|
isToday={isDateInSameDay(date, today)}
|
||||||
|
activities={activities}
|
||||||
|
onClick={() => onDateSelect(date)}
|
||||||
|
hasPendingEntry={selectedDay && isDateInSameDay(selectedDay, date)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
|
||||||
|
export type CalendarActivityType = 'created' | 'edited'
|
||||||
|
|
||||||
|
export type CalendarActivity = {
|
||||||
|
date: Date
|
||||||
|
item: ListableContentItem
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { FunctionComponent } from 'react'
|
||||||
|
import { CalendarActivity } from './CalendarActivity'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
day: number
|
||||||
|
activities: CalendarActivity[]
|
||||||
|
isToday: boolean
|
||||||
|
onClick: () => void
|
||||||
|
hasPendingEntry?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const CalendarDay: FunctionComponent<Props> = ({ day, activities = [], hasPendingEntry, isToday, onClick }) => {
|
||||||
|
const hasActivity = day > 0 && activities.length > 0
|
||||||
|
const todayClassNames = 'bg-danger text-danger-contrast font-bold'
|
||||||
|
const hasActivityClassNames = 'bg-danger-light text-danger font-bold'
|
||||||
|
const defaultClassNames = 'bg-transparent hover:bg-contrast'
|
||||||
|
const hasPendingEntryNames = 'bg-contrast'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-7 w-[14.2%] p-0.5">
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
!hasActivity && !isToday ? defaultClassNames : ''
|
||||||
|
} flex h-full w-full cursor-pointer items-center justify-center rounded ${
|
||||||
|
isToday ? todayClassNames : hasActivity ? hasActivityClassNames : ''
|
||||||
|
} ${hasPendingEntry ? hasPendingEntryNames : ''}`}
|
||||||
|
key={day}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{day > 0 ? day : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarDay
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export type CalendarMonth = {
|
||||||
|
/** Any date in the month */
|
||||||
|
date: Date
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { addMonths, areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||||
|
import { CalendarMonth } from './CalendarMonth'
|
||||||
|
import { insertMonthsWithTarget } from './CalendarUtilts'
|
||||||
|
|
||||||
|
describe('calendar utils', () => {
|
||||||
|
it('insertMonthsWithTarget in past', () => {
|
||||||
|
const today = new Date()
|
||||||
|
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
|
||||||
|
const targetMonth = addMonths(today, -12)
|
||||||
|
|
||||||
|
const result = insertMonthsWithTarget(months, targetMonth)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(14)
|
||||||
|
expect(areDatesInSameMonth(result[0].date, targetMonth))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('insertMonthsWithTarget in future', () => {
|
||||||
|
const today = new Date()
|
||||||
|
const months: CalendarMonth[] = [{ date: addMonths(today, -1) }, { date: today }, { date: addMonths(today, 1) }]
|
||||||
|
const targetMonth = addMonths(today, 12)
|
||||||
|
|
||||||
|
const result = insertMonthsWithTarget(months, targetMonth)
|
||||||
|
|
||||||
|
expect(result).toHaveLength(14)
|
||||||
|
expect(areDatesInSameMonth(result[result.length - 1].date, targetMonth))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { addMonths, numberOfMonthsBetweenDates } from '@/Utils/DateUtils'
|
||||||
|
import { CalendarActivity } from './CalendarActivity'
|
||||||
|
import { CalendarMonth } from './CalendarMonth'
|
||||||
|
|
||||||
|
type DateOnlyString = string
|
||||||
|
|
||||||
|
export function dateToDateOnlyString(date: Date): DateOnlyString {
|
||||||
|
return date.toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityRecord = Record<DateOnlyString, CalendarActivity[]>
|
||||||
|
|
||||||
|
export function createActivityRecord(activities: CalendarActivity[]): ActivityRecord {
|
||||||
|
const map: Record<string, CalendarActivity[]> = {}
|
||||||
|
for (const activity of activities) {
|
||||||
|
const string = dateToDateOnlyString(activity.date)
|
||||||
|
if (!map[string]) {
|
||||||
|
map[string] = []
|
||||||
|
}
|
||||||
|
map[string].push(activity)
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLeapYear(year: number) {
|
||||||
|
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStartDayOfMonth(date: Date) {
|
||||||
|
const startDate = new Date(date.getFullYear(), date.getMonth(), 1).getDay()
|
||||||
|
return startDate === 0 ? 7 : startDate
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies months array in-place.
|
||||||
|
*/
|
||||||
|
export function insertMonths(months: CalendarMonth[], location: 'front' | 'end', number: number): CalendarMonth[] {
|
||||||
|
const earlierMonth = months[0].date
|
||||||
|
const laterMonth = months[months.length - 1].date
|
||||||
|
|
||||||
|
for (let i = 1; i <= number; i++) {
|
||||||
|
if (location === 'front') {
|
||||||
|
const minusNFromFirstMonth = addMonths(earlierMonth, -i)
|
||||||
|
months.unshift({
|
||||||
|
date: minusNFromFirstMonth,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const plusNFromLastMonth = addMonths(laterMonth, i)
|
||||||
|
months.push({
|
||||||
|
date: plusNFromLastMonth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return months
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies months array in-place.
|
||||||
|
*/
|
||||||
|
export function insertMonthsWithTarget(months: CalendarMonth[], targetMonth: Date): CalendarMonth[] {
|
||||||
|
const firstMonth = months[0].date
|
||||||
|
const lastMonth = months[months.length - 1].date
|
||||||
|
|
||||||
|
const isBeforeFirstMonth = targetMonth.getTime() < firstMonth.getTime()
|
||||||
|
|
||||||
|
const numMonthsToAdd = Math.abs(
|
||||||
|
isBeforeFirstMonth
|
||||||
|
? numberOfMonthsBetweenDates(firstMonth, targetMonth)
|
||||||
|
: numberOfMonthsBetweenDates(lastMonth, targetMonth),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isBeforeFirstMonth) {
|
||||||
|
return insertMonths(months, 'front', numMonthsToAdd)
|
||||||
|
} else {
|
||||||
|
return insertMonths(months, 'end', numMonthsToAdd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies months array in-place.
|
||||||
|
*/
|
||||||
|
export function removeMonths(months: CalendarMonth[], location: 'front' | 'end', number: number): void {
|
||||||
|
if (location === 'front') {
|
||||||
|
months.splice(0, number)
|
||||||
|
} else {
|
||||||
|
months.splice(months.length - number - 1, number)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const CalendarDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
export const CalendarDaysLeap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
||||||
|
export const CalendarDaysOfTheWeek = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
|
||||||
|
export const CalendarMonths = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
]
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import { areDatesInSameMonth } from '@/Utils/DateUtils'
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import Calendar from './Calendar'
|
||||||
|
import { CalendarActivity, CalendarActivityType } from './CalendarActivity'
|
||||||
|
import { CalendarMonth } from './CalendarMonth'
|
||||||
|
import { CalendarMonths } from './Constants'
|
||||||
|
import { insertMonths, insertMonthsWithTarget } from './CalendarUtilts'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activityType: CalendarActivityType
|
||||||
|
activities: CalendarActivity[]
|
||||||
|
onDateSelect: (date: Date) => void
|
||||||
|
selectedTemplateDay?: Date
|
||||||
|
selectedItemDay?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InfiniteCalendarInterface = {
|
||||||
|
changeMonth: (month: Date) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageSize = 10
|
||||||
|
|
||||||
|
const InfiniteCalendar = forwardRef<InfiniteCalendarInterface, Props>(
|
||||||
|
({ activities, onDateSelect, selectedTemplateDay, selectedItemDay }: Props, ref) => {
|
||||||
|
const [date, setDate] = useState(new Date())
|
||||||
|
const [month, setMonth] = useState(date.getMonth())
|
||||||
|
const [year, setYear] = useState(date.getFullYear())
|
||||||
|
|
||||||
|
const [expanded, setExpanded] = useState(true)
|
||||||
|
const [scrollWidth, setScrollWidth] = useState(0)
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const [months, setMonths] = useState<CalendarMonth[]>(() => {
|
||||||
|
const base = [{ date: today }]
|
||||||
|
insertMonths(base, 'front', 2)
|
||||||
|
insertMonths(base, 'end', 2)
|
||||||
|
return base
|
||||||
|
})
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
changeMonth(date: Date) {
|
||||||
|
setDate(date)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [didPaginateLeft, setDidPaginateLeft] = useState(false)
|
||||||
|
const [restoreScrollAfterExpand, setRestoreScrollAfterExpand] = useState(false)
|
||||||
|
const scrollArea = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const hasMonthInList = useCallback(
|
||||||
|
(date: Date): boolean => {
|
||||||
|
for (const month of months) {
|
||||||
|
if (areDatesInSameMonth(month.date, date)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
[months],
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertMonthInList = useCallback(
|
||||||
|
(date: Date): void => {
|
||||||
|
setMonths(insertMonthsWithTarget(months, date))
|
||||||
|
},
|
||||||
|
[months, setMonths],
|
||||||
|
)
|
||||||
|
|
||||||
|
const resetNumberOfCalendarsToBase = useCallback(
|
||||||
|
(centerOnDate: Date) => {
|
||||||
|
const base = [{ date: centerOnDate }]
|
||||||
|
insertMonths(base, 'front', 1)
|
||||||
|
insertMonths(base, 'end', 1)
|
||||||
|
setMonths(base)
|
||||||
|
},
|
||||||
|
[setMonths],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTemplateDay) {
|
||||||
|
setDate(selectedTemplateDay)
|
||||||
|
}
|
||||||
|
}, [selectedTemplateDay])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedItemDay) {
|
||||||
|
setDate(selectedItemDay)
|
||||||
|
}
|
||||||
|
}, [selectedItemDay])
|
||||||
|
|
||||||
|
const scrollToMonth = useCallback((date: Date) => {
|
||||||
|
const elementId = elementIdForMonth(date)
|
||||||
|
const element = document.getElementById(elementId)
|
||||||
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollArea.current!.scrollLeft = element.offsetLeft + -60
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setMonth(date.getMonth())
|
||||||
|
setYear(date.getFullYear())
|
||||||
|
|
||||||
|
if (!hasMonthInList(date)) {
|
||||||
|
insertMonthInList(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToMonth(date)
|
||||||
|
}, [date, hasMonthInList, insertMonthInList, scrollToMonth])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!restoreScrollAfterExpand) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollArea.current && expanded) {
|
||||||
|
scrollToMonth(date)
|
||||||
|
setRestoreScrollAfterExpand(false)
|
||||||
|
}
|
||||||
|
}, [expanded, scrollToMonth, date, restoreScrollAfterExpand, setRestoreScrollAfterExpand])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!scrollArea.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didPaginateLeft) {
|
||||||
|
scrollArea.current.scrollLeft += scrollArea.current.scrollWidth - scrollWidth
|
||||||
|
setDidPaginateLeft(false)
|
||||||
|
}
|
||||||
|
}, [months, didPaginateLeft, scrollWidth])
|
||||||
|
|
||||||
|
const paginateLeft = useCallback(() => {
|
||||||
|
if (scrollArea.current) {
|
||||||
|
setScrollWidth(scrollArea.current.scrollWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copy = months.slice()
|
||||||
|
insertMonths(copy, 'front', PageSize)
|
||||||
|
setDidPaginateLeft(true)
|
||||||
|
setMonths(copy)
|
||||||
|
}, [months, setMonths])
|
||||||
|
|
||||||
|
const paginateRight = useCallback(() => {
|
||||||
|
const copy = months.slice()
|
||||||
|
insertMonths(copy, 'end', PageSize)
|
||||||
|
setDidPaginateLeft(false)
|
||||||
|
setMonths(copy)
|
||||||
|
}, [months, setMonths])
|
||||||
|
|
||||||
|
const updateCurrentMonth = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const newMonth = months[index]
|
||||||
|
setMonth(newMonth.date.getMonth())
|
||||||
|
setYear(newMonth.date.getFullYear())
|
||||||
|
},
|
||||||
|
[months, setMonth, setYear],
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibilityObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||||
|
if (visibleEntry) {
|
||||||
|
const id = visibleEntry.target.id
|
||||||
|
const index = months.findIndex((candidate) => elementIdForMonth(candidate.date) === id)
|
||||||
|
updateCurrentMonth(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.9 },
|
||||||
|
),
|
||||||
|
[updateCurrentMonth, months],
|
||||||
|
)
|
||||||
|
|
||||||
|
const rightObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
paginateRight()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
),
|
||||||
|
[paginateRight],
|
||||||
|
)
|
||||||
|
|
||||||
|
const leftObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
paginateLeft()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1.0 },
|
||||||
|
),
|
||||||
|
[paginateLeft],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastElement) {
|
||||||
|
rightObserver.observe(lastElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (lastElement) {
|
||||||
|
rightObserver.unobserve(lastElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lastElement, rightObserver])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstElement) {
|
||||||
|
leftObserver.observe(firstElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (firstElement) {
|
||||||
|
leftObserver.unobserve(firstElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [firstElement, leftObserver])
|
||||||
|
|
||||||
|
const toggleVisibility = useCallback(() => {
|
||||||
|
setRestoreScrollAfterExpand(true)
|
||||||
|
|
||||||
|
setExpanded(!expanded)
|
||||||
|
}, [expanded, setExpanded, setRestoreScrollAfterExpand])
|
||||||
|
|
||||||
|
const elementIdForMonth = (date: Date): string => {
|
||||||
|
return `month-${date.getFullYear()}-${date.getMonth()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDaySelection = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
resetNumberOfCalendarsToBase(date)
|
||||||
|
onDateSelect(date)
|
||||||
|
},
|
||||||
|
[onDateSelect, resetNumberOfCalendarsToBase],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex-shrink-0 border-b border-solid border-border">
|
||||||
|
<div
|
||||||
|
className="text-md flex cursor-pointer items-center justify-center py-2 px-4 text-center font-bold hover:bg-contrast"
|
||||||
|
onClick={toggleVisibility}
|
||||||
|
>
|
||||||
|
{CalendarMonths[month]} {year}
|
||||||
|
</div>
|
||||||
|
{expanded && (
|
||||||
|
<div
|
||||||
|
style={{ scrollBehavior: 'smooth' }}
|
||||||
|
ref={scrollArea}
|
||||||
|
id="calendar-scroller"
|
||||||
|
className="flex w-full overflow-x-scroll pb-2 md:max-w-full"
|
||||||
|
>
|
||||||
|
{months.map((month, index) => {
|
||||||
|
const isFirst = index === 0
|
||||||
|
const isLast = index === months.length - 1
|
||||||
|
const id = elementIdForMonth(month.date)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
key={id}
|
||||||
|
ref={(ref) => {
|
||||||
|
isFirst ? setFirstElement(ref) : isLast ? setLastElement(ref) : null
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
visibilityObserver.observe(ref)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
key={id}
|
||||||
|
className="mx-2"
|
||||||
|
activities={activities}
|
||||||
|
onDateSelect={handleDaySelection}
|
||||||
|
startDate={month.date}
|
||||||
|
selectedDay={selectedTemplateDay}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export default InfiniteCalendar
|
||||||
@@ -12,6 +12,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
|
|||||||
import { NotesController } from '@/Controllers/NotesController'
|
import { NotesController } from '@/Controllers/NotesController'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -64,6 +65,33 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
[selectNextItem, selectPreviousItem],
|
[selectNextItem, selectPreviousItem],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const selectItem = useCallback(
|
||||||
|
(item: ListableContentItem, userTriggered?: boolean) => {
|
||||||
|
return selectionController.selectItem(item.uuid, userTriggered)
|
||||||
|
},
|
||||||
|
[selectionController],
|
||||||
|
)
|
||||||
|
|
||||||
|
const getTagsForItem = (item: ListableContentItem) => {
|
||||||
|
if (hideTags) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedTag = navigationController.selected
|
||||||
|
if (!selectedTag) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = application.getItemTags(item)
|
||||||
|
|
||||||
|
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||||
|
if (isNavigatingOnlyTag) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -88,8 +116,8 @@ const ContentList: FunctionComponent<Props> = ({
|
|||||||
hideIcon={hideEditorIcon}
|
hideIcon={hideEditorIcon}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
selectionController={selectionController}
|
onSelect={selectItem}
|
||||||
navigationController={navigationController}
|
tags={getTagsForItem(item)}
|
||||||
notesController={notesController}
|
notesController={notesController}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,35 +1,15 @@
|
|||||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
import { ContentType } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import FileListItem from './FileListItem'
|
import FileListItem from './FileListItem'
|
||||||
import NoteListItem from './NoteListItem'
|
import NoteListItem from './NoteListItem'
|
||||||
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
||||||
|
|
||||||
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||||
const getTags = () => {
|
|
||||||
if (props.hideTags) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedTag = props.navigationController.selected
|
|
||||||
if (!selectedTag) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = props.application.getItemTags(props.item)
|
|
||||||
|
|
||||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
|
||||||
if (isNavigatingOnlyTag) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return tags
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (props.item.content_type) {
|
switch (props.item.content_type) {
|
||||||
case ContentType.Note:
|
case ContentType.Note:
|
||||||
return <NoteListItem tags={getTags()} {...props} />
|
return <NoteListItem {...props} />
|
||||||
case ContentType.File:
|
case ContentType.File:
|
||||||
return <FileListItem tags={getTags()} {...props} />
|
return <FileListItem {...props} />
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
|
|||||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
|
import DailyContentList from './Daily/DailyContentList'
|
||||||
|
import { ListableContentItem } from './Types/ListableContentItem'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
accountMenuController: AccountMenuController
|
accountMenuController: AccountMenuController
|
||||||
@@ -106,12 +108,13 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
panelTitle,
|
panelTitle,
|
||||||
panelWidth,
|
panelWidth,
|
||||||
renderedItems,
|
renderedItems,
|
||||||
|
items,
|
||||||
searchBarElement,
|
searchBarElement,
|
||||||
} = itemListController
|
} = itemListController
|
||||||
|
|
||||||
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||||
|
|
||||||
const { selected: selectedTag } = navigationController
|
const { selected: selectedTag, selectedAsTag } = navigationController
|
||||||
|
|
||||||
const icon = selectedTag?.iconString
|
const icon = selectedTag?.iconString
|
||||||
|
|
||||||
@@ -227,6 +230,19 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl)
|
||||||
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint
|
||||||
|
|
||||||
|
const dailyMode = selectedAsTag?.isDailyEntry
|
||||||
|
|
||||||
|
const handleDailyListSelection = useCallback(
|
||||||
|
async (item: ListableContentItem, userTriggered: boolean) => {
|
||||||
|
await selectionController.selectItemWithScrollHandling(item, {
|
||||||
|
userTriggered: true,
|
||||||
|
scrollIntoView: userTriggered === false,
|
||||||
|
animated: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[selectionController],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="items-column"
|
id="items-column"
|
||||||
@@ -279,20 +295,35 @@ const ContentListView: FunctionComponent<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{completedFullSync && !renderedItems.length ? <p className="empty-items-list opacity-50">No items.</p> : null}
|
{selectedAsTag && dailyMode && (
|
||||||
{!completedFullSync && !renderedItems.length ? <p className="empty-items-list opacity-50">Loading...</p> : null}
|
<DailyContentList
|
||||||
{renderedItems.length ? (
|
items={items}
|
||||||
<ContentList
|
selectedTag={selectedAsTag}
|
||||||
items={renderedItems}
|
|
||||||
selectedUuids={selectedUuids}
|
selectedUuids={selectedUuids}
|
||||||
application={application}
|
|
||||||
paginate={paginate}
|
|
||||||
filesController={filesController}
|
|
||||||
itemListController={itemListController}
|
itemListController={itemListController}
|
||||||
navigationController={navigationController}
|
onSelect={handleDailyListSelection}
|
||||||
notesController={notesController}
|
|
||||||
selectionController={selectionController}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
{!dailyMode && renderedItems.length ? (
|
||||||
|
<>
|
||||||
|
{completedFullSync && !renderedItems.length ? (
|
||||||
|
<p className="empty-items-list opacity-50">No items.</p>
|
||||||
|
) : null}
|
||||||
|
{!completedFullSync && !renderedItems.length ? (
|
||||||
|
<p className="empty-items-list opacity-50">Loading...</p>
|
||||||
|
) : null}
|
||||||
|
<ContentList
|
||||||
|
items={renderedItems}
|
||||||
|
selectedUuids={selectedUuids}
|
||||||
|
application={application}
|
||||||
|
paginate={paginate}
|
||||||
|
filesController={filesController}
|
||||||
|
itemListController={itemListController}
|
||||||
|
navigationController={navigationController}
|
||||||
|
notesController={notesController}
|
||||||
|
selectionController={selectionController}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="absolute bottom-0 h-safe-bottom w-full" />
|
<div className="absolute bottom-0 h-safe-bottom w-full" />
|
||||||
</ResponsivePaneContent>
|
</ResponsivePaneContent>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||||
|
import { addDays, getWeekdayName } from '@/Utils/DateUtils'
|
||||||
|
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||||
|
import { dateToDailyDayIdentifier } from './Utils'
|
||||||
|
|
||||||
|
export const createItemsByDateMapping = (items: ListableContentItem[]) => {
|
||||||
|
const mapping: Record<string, ListableContentItem[]> = {}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const key = dateToDailyDayIdentifier(item.created_at)
|
||||||
|
if (!mapping[key]) {
|
||||||
|
mapping[key] = []
|
||||||
|
}
|
||||||
|
mapping[key].push(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
export const templateEntryForDate = (date: Date): DailyItemsDay => {
|
||||||
|
const entryDateString = dateToDailyDayIdentifier(date)
|
||||||
|
|
||||||
|
return {
|
||||||
|
dateKey: entryDateString,
|
||||||
|
date: date,
|
||||||
|
day: date.getDate(),
|
||||||
|
isToday: entryDateString === dateToDailyDayIdentifier(new Date()),
|
||||||
|
id: entryDateString,
|
||||||
|
weekday: getWeekdayName(date, 'short'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDailyItemsWithToday(count: number): DailyItemsDay[] {
|
||||||
|
const today = templateEntryForDate(new Date())
|
||||||
|
return insertBlanks([today], 'end', count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies entries array in-place.
|
||||||
|
*/
|
||||||
|
export function insertBlanks(entries: DailyItemsDay[], location: 'front' | 'end', number: number): DailyItemsDay[] {
|
||||||
|
let laterDay, earlierDay
|
||||||
|
|
||||||
|
if (entries.length > 0) {
|
||||||
|
laterDay = entries[0].date
|
||||||
|
earlierDay = entries[entries.length - 1].date
|
||||||
|
} else {
|
||||||
|
const today = new Date()
|
||||||
|
laterDay = today
|
||||||
|
earlierDay = today
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= number; i++) {
|
||||||
|
if (location === 'front') {
|
||||||
|
const plusNFromFirstDay = addDays(laterDay, i)
|
||||||
|
const futureEntry = templateEntryForDate(plusNFromFirstDay)
|
||||||
|
entries.unshift(futureEntry)
|
||||||
|
} else {
|
||||||
|
const minusNFromLastDay = addDays(earlierDay, -i)
|
||||||
|
const pastEntry = templateEntryForDate(minusNFromLastDay)
|
||||||
|
entries.push(pastEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import { FunctionComponent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||||
|
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||||
|
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||||
|
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||||
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { useResponsiveAppPane } from '../../ResponsivePane/ResponsivePaneProvider'
|
||||||
|
import { AppPaneId } from '../../ResponsivePane/AppPaneMetadata'
|
||||||
|
import { createDailyItemsWithToday, createItemsByDateMapping, insertBlanks } from './CreateDailySections'
|
||||||
|
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||||
|
import { DailyItemCell } from './DailyItemCell'
|
||||||
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
|
import { CalendarActivity } from '../Calendar/CalendarActivity'
|
||||||
|
import { dateToDailyDayIdentifier } from './Utils'
|
||||||
|
import InfiniteCalendar, { InfiniteCalendarInterface } from '../Calendar/InfiniteCalendar'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
itemListController: ItemListController
|
||||||
|
items: ListableContentItem[]
|
||||||
|
onSelect: (item: ListableContentItem, userTriggered: boolean) => Promise<void>
|
||||||
|
selectedTag: SNTag
|
||||||
|
selectedUuids: SelectedItemsController['selectedUuids']
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageSize = 10
|
||||||
|
|
||||||
|
const DailyContentList: FunctionComponent<Props> = ({
|
||||||
|
items,
|
||||||
|
itemListController,
|
||||||
|
onSelect,
|
||||||
|
selectedUuids,
|
||||||
|
selectedTag,
|
||||||
|
}) => {
|
||||||
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
const [needsSelectionReload, setNeedsSelectionReload] = useState(false)
|
||||||
|
const [todayItem, setTodayItem] = useState<DailyItemsDay>()
|
||||||
|
const [selectedDay, setSelectedDay] = useState<Date>()
|
||||||
|
const [lastElement, setLastElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [firstElement, setFirstElement] = useState<HTMLDivElement | null>(null)
|
||||||
|
const [lastScrollHeight, setLastScrollHeight] = useState(0)
|
||||||
|
const [didPaginateTop, setDidPaginateTop] = useState(false)
|
||||||
|
const scrollArea = useRef<HTMLDivElement>(null)
|
||||||
|
const calendarRef = useRef<InfiniteCalendarInterface | null>(null)
|
||||||
|
|
||||||
|
const [dailyItems, setDailyItems] = useState<DailyItemsDay[]>(() => {
|
||||||
|
return createDailyItemsWithToday(PageSize)
|
||||||
|
})
|
||||||
|
|
||||||
|
const { hideTags, hideDate, hideNotePreview } = itemListController.webDisplayOptions
|
||||||
|
|
||||||
|
const itemsByDateMapping = useMemo(() => {
|
||||||
|
return createItemsByDateMapping(items)
|
||||||
|
}, [items])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTodayItem(dailyItems.find((item) => item.isToday) as DailyItemsDay)
|
||||||
|
}, [dailyItems])
|
||||||
|
|
||||||
|
const calendarActivities: CalendarActivity[] = useMemo(() => {
|
||||||
|
return items.map((item) => {
|
||||||
|
return {
|
||||||
|
date: item.created_at,
|
||||||
|
item: item,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [items])
|
||||||
|
|
||||||
|
const paginateBottom = useCallback(() => {
|
||||||
|
const copy = dailyItems.slice()
|
||||||
|
insertBlanks(copy, 'end', PageSize)
|
||||||
|
setDailyItems(copy)
|
||||||
|
}, [dailyItems, setDailyItems])
|
||||||
|
|
||||||
|
const paginateTop = useCallback(() => {
|
||||||
|
if (scrollArea.current) {
|
||||||
|
setLastScrollHeight(scrollArea.current.scrollHeight)
|
||||||
|
}
|
||||||
|
const copy = dailyItems.slice()
|
||||||
|
insertBlanks(copy, 'front', PageSize)
|
||||||
|
setDidPaginateTop(true)
|
||||||
|
setDailyItems(copy)
|
||||||
|
}, [dailyItems, setDailyItems, setDidPaginateTop])
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!scrollArea.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didPaginateTop) {
|
||||||
|
scrollArea.current.scrollTop += scrollArea.current.scrollHeight - lastScrollHeight
|
||||||
|
setDidPaginateTop(false)
|
||||||
|
}
|
||||||
|
}, [didPaginateTop, lastScrollHeight])
|
||||||
|
|
||||||
|
const onListItemDidBecomeVisible = useCallback(
|
||||||
|
(elementId: string) => {
|
||||||
|
const dailyItem = dailyItems.find((candidate) => candidate.id === elementId)
|
||||||
|
if (dailyItem) {
|
||||||
|
calendarRef?.current?.changeMonth(dailyItem.date)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dailyItems],
|
||||||
|
)
|
||||||
|
|
||||||
|
const visibilityObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const visibleEntry = entries.find((entry) => entry.isIntersecting)
|
||||||
|
if (visibleEntry) {
|
||||||
|
onListItemDidBecomeVisible(visibleEntry.target.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.9 },
|
||||||
|
),
|
||||||
|
[onListItemDidBecomeVisible],
|
||||||
|
)
|
||||||
|
|
||||||
|
const bottomObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const first = entries[0]
|
||||||
|
if (first.isIntersecting) {
|
||||||
|
paginateBottom()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
),
|
||||||
|
[paginateBottom],
|
||||||
|
)
|
||||||
|
|
||||||
|
const topObserver = useMemo(
|
||||||
|
() =>
|
||||||
|
new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const first = entries[0]
|
||||||
|
if (first.isIntersecting) {
|
||||||
|
paginateTop()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.5 },
|
||||||
|
),
|
||||||
|
[paginateTop],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastElement) {
|
||||||
|
bottomObserver.observe(lastElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (lastElement) {
|
||||||
|
bottomObserver.unobserve(lastElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [lastElement, bottomObserver])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstElement) {
|
||||||
|
topObserver.observe(firstElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (firstElement) {
|
||||||
|
topObserver.unobserve(firstElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [firstElement, topObserver])
|
||||||
|
|
||||||
|
const onClickItem = useCallback(
|
||||||
|
async (day: DailyItemsDay, item: ListableContentItem, userTriggered: boolean) => {
|
||||||
|
await onSelect(item, userTriggered)
|
||||||
|
toggleAppPane(AppPaneId.Editor)
|
||||||
|
setSelectedDay(day.date)
|
||||||
|
},
|
||||||
|
[onSelect, toggleAppPane],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onClickTemplate = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
setSelectedDay(date)
|
||||||
|
itemListController.createNewNote(undefined, date, 'editor')
|
||||||
|
toggleAppPane(AppPaneId.Editor)
|
||||||
|
},
|
||||||
|
[setSelectedDay, itemListController, toggleAppPane],
|
||||||
|
)
|
||||||
|
|
||||||
|
const dailyItemForDate = useCallback(
|
||||||
|
(date: Date): DailyItemsDay | undefined => {
|
||||||
|
return dailyItems.find((candidate) => dateToDailyDayIdentifier(date) === candidate.dateKey)
|
||||||
|
},
|
||||||
|
[dailyItems],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (needsSelectionReload) {
|
||||||
|
setNeedsSelectionReload(false)
|
||||||
|
|
||||||
|
if (!todayItem) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = itemsByDateMapping[todayItem.id]
|
||||||
|
if (items?.length > 0) {
|
||||||
|
const item = items[0]
|
||||||
|
const dailyItem = dailyItemForDate(item.created_at)
|
||||||
|
if (dailyItem) {
|
||||||
|
void onClickItem(dailyItem, items[0], false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onClickTemplate(todayItem.date)
|
||||||
|
const itemElement = document.getElementById(todayItem.id)
|
||||||
|
itemElement?.scrollIntoView({ behavior: 'auto' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [needsSelectionReload, onClickItem, onClickTemplate, todayItem, dailyItemForDate, itemsByDateMapping])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNeedsSelectionReload(true)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedTag.uuid])
|
||||||
|
|
||||||
|
const onCalendarSelect = useCallback(
|
||||||
|
(date: Date) => {
|
||||||
|
const dailyItem = dailyItemForDate(date)
|
||||||
|
if (dailyItem) {
|
||||||
|
const items = itemsByDateMapping[dailyItem.id]
|
||||||
|
if (items?.length > 0) {
|
||||||
|
void onClickItem(dailyItem, items[0], false)
|
||||||
|
} else if (dailyItem) {
|
||||||
|
void onClickTemplate(dailyItem.date)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
void onClickTemplate(date)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClickItem, onClickTemplate, dailyItemForDate, itemsByDateMapping],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InfiniteCalendar
|
||||||
|
activities={calendarActivities}
|
||||||
|
activityType={'created'}
|
||||||
|
onDateSelect={onCalendarSelect}
|
||||||
|
selectedTemplateDay={selectedDay}
|
||||||
|
selectedItemDay={selectedDay}
|
||||||
|
ref={calendarRef}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
|
||||||
|
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
|
||||||
|
'md:hover:[overflow-y:_overlay]',
|
||||||
|
)}
|
||||||
|
ref={scrollArea}
|
||||||
|
id={ElementIds.ContentList}
|
||||||
|
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
|
>
|
||||||
|
{dailyItems.map((dailyItem, index) => {
|
||||||
|
const isFirst = index === 0
|
||||||
|
const isLast = index === dailyItems.length - 1
|
||||||
|
const items = itemsByDateMapping[dailyItem.id]
|
||||||
|
if (items) {
|
||||||
|
return items.map((item) => (
|
||||||
|
<DailyItemCell
|
||||||
|
selected={selectedUuids.has(item.uuid)}
|
||||||
|
section={dailyItem}
|
||||||
|
key={item.uuid}
|
||||||
|
id={dailyItem.id}
|
||||||
|
item={item}
|
||||||
|
hideDate={hideDate}
|
||||||
|
hidePreview={hideNotePreview}
|
||||||
|
hideTags={hideTags}
|
||||||
|
onClick={() => onClickItem(dailyItem, item, true)}
|
||||||
|
ref={(ref) => {
|
||||||
|
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
|
||||||
|
if (ref) {
|
||||||
|
visibilityObserver.observe(ref)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DailyItemCell
|
||||||
|
selected={selectedDay && dailyItem.id === dateToDailyDayIdentifier(selectedDay)}
|
||||||
|
section={dailyItem}
|
||||||
|
id={dailyItem.id}
|
||||||
|
key={dailyItem.dateKey}
|
||||||
|
onClick={() => onClickTemplate(dailyItem.date)}
|
||||||
|
ref={(ref) => {
|
||||||
|
isLast ? setLastElement(ref) : isFirst ? setFirstElement(ref) : null
|
||||||
|
if (ref) {
|
||||||
|
visibilityObserver.observe(ref)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DailyContentList
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||||
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
|
import { ComponentPropsWithoutRef, forwardRef, FunctionComponent, Ref } from 'react'
|
||||||
|
import ListItemFlagIcons from '../ListItemFlagIcons'
|
||||||
|
import ListItemMetadata from '../ListItemMetadata'
|
||||||
|
import ListItemTags from '../ListItemTags'
|
||||||
|
import ListItemNotePreviewText from '../ListItemNotePreviewText'
|
||||||
|
import { ListableContentItem } from '../Types/ListableContentItem'
|
||||||
|
import { DailyItemsDay } from './DailyItemsDaySection'
|
||||||
|
import { ListItemTitle } from '../ListItemTitle'
|
||||||
|
import { EmptyPlaceholderBars } from './EmptyPlaceholderBars'
|
||||||
|
|
||||||
|
type DaySquareProps = {
|
||||||
|
day: number
|
||||||
|
hasActivity: boolean
|
||||||
|
weekday: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DaySquare: FunctionComponent<DaySquareProps> = ({ day, hasActivity, weekday }) => {
|
||||||
|
return (
|
||||||
|
<div className="mr-5">
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
hasActivity ? 'bg-danger text-danger-contrast' : 'bg-neutral text-neutral-contrast'
|
||||||
|
} h-15 w-18 rounded p-2 text-center`}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-bold">{weekday}</div>
|
||||||
|
<div className="text-4xl font-bold">{day}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends ComponentPropsWithoutRef<'div'> {
|
||||||
|
item?: ListableContentItem
|
||||||
|
onClick: () => void
|
||||||
|
section: DailyItemsDay
|
||||||
|
selected?: boolean
|
||||||
|
tags?: SNTag[]
|
||||||
|
hideDate?: boolean
|
||||||
|
hideTags?: boolean
|
||||||
|
hidePreview?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DailyItemCell = forwardRef(
|
||||||
|
(
|
||||||
|
{ item, tags = [], section, onClick, selected, hideDate = false, hidePreview = false, hideTags = false }: Props,
|
||||||
|
ref: Ref<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||||
|
selected && 'selected border-l-2 border-solid border-danger'
|
||||||
|
}`}
|
||||||
|
id={section.id}
|
||||||
|
>
|
||||||
|
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-4">
|
||||||
|
<div className="flex items-start overflow-hidden text-base">
|
||||||
|
<DaySquare weekday={section.weekday} hasActivity={item != undefined} day={section.day} />
|
||||||
|
|
||||||
|
<div className="w-full leading-[1.3]">
|
||||||
|
{item && (
|
||||||
|
<>
|
||||||
|
<ListItemTitle item={item} />
|
||||||
|
<ListItemNotePreviewText hidePreview={hidePreview} item={item} lineLimit={5} />
|
||||||
|
<ListItemMetadata item={item} hideDate={hideDate} sortBy={'created_at'} />
|
||||||
|
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!item && (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="break-word mr-2 font-semibold">{formatDateAndTimeForNote(section.date, false)}</div>
|
||||||
|
<EmptyPlaceholderBars rows={4} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{item && <ListItemFlagIcons item={item} hasFiles={false} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type DailyItemsDay = {
|
||||||
|
dateKey: string
|
||||||
|
day: number
|
||||||
|
weekday: string
|
||||||
|
date: Date
|
||||||
|
isToday: boolean
|
||||||
|
id: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
rows: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomNumber(min: number, max: number) {
|
||||||
|
const r = Math.random() * (max - min) + min
|
||||||
|
return Math.floor(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyPlaceholderBars: FunctionComponent<Props> = ({ rows }) => {
|
||||||
|
const [barWidths, setBarWidths] = useState<number[]>([])
|
||||||
|
const [animationInterval, setAnimationInterval] = useState<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
const reloadWidths = useCallback(() => {
|
||||||
|
const widths = []
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
const width = randomNumber(70, 100)
|
||||||
|
widths.push(width)
|
||||||
|
}
|
||||||
|
setBarWidths(widths)
|
||||||
|
}, [setBarWidths, rows])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadWidths()
|
||||||
|
}, [rows, reloadWidths])
|
||||||
|
|
||||||
|
const onHoverEnter = useCallback(() => {
|
||||||
|
reloadWidths()
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
reloadWidths()
|
||||||
|
}, 750)
|
||||||
|
|
||||||
|
setAnimationInterval(interval)
|
||||||
|
}, [setAnimationInterval, reloadWidths])
|
||||||
|
|
||||||
|
const onHoverExit = useCallback(() => {
|
||||||
|
if (animationInterval) {
|
||||||
|
clearInterval(animationInterval)
|
||||||
|
setAnimationInterval(null)
|
||||||
|
}
|
||||||
|
}, [animationInterval, setAnimationInterval])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onMouseEnter={onHoverEnter} onMouseLeave={onHoverExit} className="w-full">
|
||||||
|
{barWidths.map((width, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ width: `${width}%` }}
|
||||||
|
key={index}
|
||||||
|
className={
|
||||||
|
'transition-slowest ease my-4 h-7 bg-passive-4-opacity-variant pb-3 transition-width duration-1000'
|
||||||
|
}
|
||||||
|
></div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function dateToDailyDayIdentifier(date: Date): string {
|
||||||
|
return date.toLocaleDateString()
|
||||||
|
}
|
||||||
@@ -14,11 +14,11 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
|||||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
filesController,
|
filesController,
|
||||||
selectionController,
|
|
||||||
hideDate,
|
hideDate,
|
||||||
hideIcon,
|
hideIcon,
|
||||||
hideTags,
|
hideTags,
|
||||||
item,
|
item,
|
||||||
|
onSelect,
|
||||||
selected,
|
selected,
|
||||||
sortBy,
|
sortBy,
|
||||||
tags,
|
tags,
|
||||||
@@ -44,7 +44,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
let shouldOpenContextMenu = selected
|
let shouldOpenContextMenu = selected
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
const { didSelect } = await selectionController.selectItem(item.uuid)
|
const { didSelect } = await onSelect(item)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
shouldOpenContextMenu = true
|
shouldOpenContextMenu = true
|
||||||
}
|
}
|
||||||
@@ -54,15 +54,15 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
openFileContextMenu(posX, posY)
|
openFileContextMenu(posX, posY)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selected, selectionController, item.uuid, openFileContextMenu],
|
[selected, onSelect, item, openFileContextMenu],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onClick = useCallback(async () => {
|
const onClick = useCallback(async () => {
|
||||||
const { didSelect } = await selectionController.selectItem(item.uuid, true)
|
const { didSelect } = await onSelect(item, true)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
toggleAppPane(AppPaneId.Editor)
|
toggleAppPane(AppPaneId.Editor)
|
||||||
}
|
}
|
||||||
}, [item.uuid, selectionController, toggleAppPane])
|
}, [item, onSelect, toggleAppPane])
|
||||||
|
|
||||||
const IconComponent = () =>
|
const IconComponent = () =>
|
||||||
getFileIconComponent(
|
getFileIconComponent(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { classNames } from '@/Utils/ConcatenateClassNames'
|
|||||||
import Popover from '@/Components/Popover/Popover'
|
import Popover from '@/Components/Popover/Popover'
|
||||||
import DisplayOptionsMenu from './DisplayOptionsMenu'
|
import DisplayOptionsMenu from './DisplayOptionsMenu'
|
||||||
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
|
import { NavigationMenuButton } from '@/Components/NavigationMenu/NavigationMenu'
|
||||||
import { IconType } from '@standardnotes/snjs'
|
import { IconType, isTag } from '@standardnotes/snjs'
|
||||||
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||||
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ const ContentListHeader = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const isDailyEntry = isTag(selectedTag) && selectedTag.isDailyEntry
|
||||||
|
|
||||||
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
|
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
|
||||||
|
|
||||||
@@ -83,7 +84,13 @@ const ContentListHeader = ({
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="absolute bottom-6 right-6 z-editor-title-bar ml-3 flex h-13 w-13 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent bg-info text-info-contrast hover:brightness-125 md:static md:h-8 md:w-8"
|
className={classNames(
|
||||||
|
'absolute bottom-6 right-6 z-editor-title-bar ml-3 flex h-13 w-13 cursor-pointer items-center',
|
||||||
|
`justify-center rounded-full border border-solid border-transparent ${
|
||||||
|
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
|
||||||
|
}`,
|
||||||
|
'hover:brightness-125 md:static md:h-8 md:w-8',
|
||||||
|
)}
|
||||||
title={addButtonLabel}
|
title={addButtonLabel}
|
||||||
aria-label={addButtonLabel}
|
aria-label={addButtonLabel}
|
||||||
onClick={addNewItem}
|
onClick={addNewItem}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import { PreferenceMode } from './PreferenceMode'
|
|||||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
|
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
|
||||||
import Button from '@/Components/Button/Button'
|
import Button from '@/Components/Button/Button'
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
|
import { isDev } from '@/Utils'
|
||||||
|
|
||||||
|
const DailyEntryModeEnabled = isDev
|
||||||
|
|
||||||
const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
||||||
closeDisplayOptionsMenu,
|
closeDisplayOptionsMenu,
|
||||||
@@ -36,6 +39,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
const [preferences, setPreferences] = useState<TagPreferences>({})
|
const [preferences, setPreferences] = useState<TagPreferences>({})
|
||||||
const hasSubscription = application.hasValidSubscription()
|
const hasSubscription = application.hasValidSubscription()
|
||||||
const controlsDisabled = currentMode === 'tag' && !hasSubscription
|
const controlsDisabled = currentMode === 'tag' && !hasSubscription
|
||||||
|
const isDailyEntry = selectedTag.preferences?.entryMode === 'daily'
|
||||||
|
|
||||||
const reloadPreferences = useCallback(() => {
|
const reloadPreferences = useCallback(() => {
|
||||||
const globalValues: TagPreferences = {
|
const globalValues: TagPreferences = {
|
||||||
@@ -162,6 +166,13 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
|
void changePreferences({ hideEditorIcon: !preferences.hideEditorIcon })
|
||||||
}, [preferences, changePreferences])
|
}, [preferences, changePreferences])
|
||||||
|
|
||||||
|
const setEntryMode = useCallback(
|
||||||
|
(mode: 'normal' | 'daily') => {
|
||||||
|
void changePreferences({ entryMode: mode })
|
||||||
|
},
|
||||||
|
[changePreferences],
|
||||||
|
)
|
||||||
|
|
||||||
const TabButton: FunctionComponent<{
|
const TabButton: FunctionComponent<{
|
||||||
label: string
|
label: string
|
||||||
mode: PreferenceMode
|
mode: PreferenceMode
|
||||||
@@ -230,7 +241,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
|
|
||||||
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div>
|
<div className="my-1 px-3 text-xs font-semibold uppercase text-text">Sort by</div>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={controlsDisabled}
|
disabled={controlsDisabled || isDailyEntry}
|
||||||
className="py-2"
|
className="py-2"
|
||||||
type={MenuItemType.RadioButton}
|
type={MenuItemType.RadioButton}
|
||||||
onClick={toggleSortByDateModified}
|
onClick={toggleSortByDateModified}
|
||||||
@@ -248,7 +259,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={controlsDisabled}
|
disabled={controlsDisabled || isDailyEntry}
|
||||||
className="py-2"
|
className="py-2"
|
||||||
type={MenuItemType.RadioButton}
|
type={MenuItemType.RadioButton}
|
||||||
onClick={toggleSortByCreationDate}
|
onClick={toggleSortByCreationDate}
|
||||||
@@ -266,7 +277,7 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={controlsDisabled}
|
disabled={controlsDisabled || isDailyEntry}
|
||||||
className="py-2"
|
className="py-2"
|
||||||
type={MenuItemType.RadioButton}
|
type={MenuItemType.RadioButton}
|
||||||
onClick={toggleSortByTitle}
|
onClick={toggleSortByTitle}
|
||||||
@@ -362,6 +373,33 @@ const DisplayOptionsMenu: FunctionComponent<DisplayOptionsMenuProps> = ({
|
|||||||
Show trashed
|
Show trashed
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
{currentMode === 'tag' && DailyEntryModeEnabled && (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
<div className="px-3 py-1 text-xs font-semibold uppercase text-text">Entry Mode</div>
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
disabled={controlsDisabled}
|
||||||
|
className="py-2"
|
||||||
|
type={MenuItemType.RadioButton}
|
||||||
|
onClick={() => setEntryMode('normal')}
|
||||||
|
checked={!selectedTag.preferences?.entryMode || selectedTag.preferences?.entryMode === 'normal'}
|
||||||
|
>
|
||||||
|
<div className="ml-2 flex flex-grow items-center justify-between">Normal</div>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
disabled={controlsDisabled}
|
||||||
|
className="py-2"
|
||||||
|
type={MenuItemType.RadioButton}
|
||||||
|
onClick={() => setEntryMode('daily')}
|
||||||
|
checked={isDailyEntry}
|
||||||
|
>
|
||||||
|
<div className="ml-2 flex flex-grow items-center justify-between">Daily</div>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuItemSeparator />
|
<MenuItemSeparator />
|
||||||
|
|
||||||
<NewNotePreferences
|
<NewNotePreferences
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { sanitizeHtmlString } from '@standardnotes/snjs'
|
||||||
|
import { FunctionComponent } from 'react'
|
||||||
|
import { ListableContentItem } from './Types/ListableContentItem'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: ListableContentItem
|
||||||
|
hidePreview: boolean
|
||||||
|
lineLimit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItemNotePreviewText: FunctionComponent<Props> = ({ item, hidePreview, lineLimit = 1 }) => {
|
||||||
|
if (item.hidePreview || item.protected || hidePreview) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`overflow-hidden overflow-ellipsis text-sm ${item.archived ? 'opacity-60' : ''}`}>
|
||||||
|
{item.preview_html && (
|
||||||
|
<div
|
||||||
|
className="my-1"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: sanitizeHtmlString(item.preview_html),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
{!item.preview_html && item.preview_plain && (
|
||||||
|
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.preview_plain}</div>
|
||||||
|
)}
|
||||||
|
{!item.preview_html && !item.preview_plain && item.text && (
|
||||||
|
<div className={`leading-1.3 line-clamp-${lineLimit} mt-1 overflow-hidden`}>{item.text}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListItemNotePreviewText
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { FunctionComponent } from 'react'
|
||||||
|
import { ListableContentItem } from './Types/ListableContentItem'
|
||||||
|
|
||||||
|
export const ListItemTitle: FunctionComponent<{ item: ListableContentItem }> = ({ item }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-start justify-between overflow-hidden text-base font-semibold leading-[1.3]">
|
||||||
|
<div className={`break-word mr-2 ${item.archived ? 'opacity-60' : ''}`}>{item.title}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||||
import { isFile, sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
import { isFile, SNNote } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
@@ -11,11 +11,13 @@ import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
|||||||
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
||||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||||
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
||||||
|
import ListItemNotePreviewText from './ListItemNotePreviewText'
|
||||||
|
import { ListItemTitle } from './ListItemTitle'
|
||||||
|
|
||||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||||
application,
|
application,
|
||||||
notesController,
|
notesController,
|
||||||
selectionController,
|
onSelect,
|
||||||
hideDate,
|
hideDate,
|
||||||
hideIcon,
|
hideIcon,
|
||||||
hideTags,
|
hideTags,
|
||||||
@@ -48,7 +50,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
let shouldOpenContextMenu = selected
|
let shouldOpenContextMenu = selected
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
const { didSelect } = await selectionController.selectItem(item.uuid)
|
const { didSelect } = await onSelect(item)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
shouldOpenContextMenu = true
|
shouldOpenContextMenu = true
|
||||||
}
|
}
|
||||||
@@ -60,11 +62,11 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onClick = useCallback(async () => {
|
const onClick = useCallback(async () => {
|
||||||
const { didSelect } = await selectionController.selectItem(item.uuid, true)
|
const { didSelect } = await onSelect(item, true)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
toggleAppPane(AppPaneId.Editor)
|
toggleAppPane(AppPaneId.Editor)
|
||||||
}
|
}
|
||||||
}, [item.uuid, selectionController, toggleAppPane])
|
}, [item, onSelect, toggleAppPane])
|
||||||
|
|
||||||
useContextMenuEvent(listItemRef, openContextMenu)
|
useContextMenuEvent(listItemRef, openContextMenu)
|
||||||
|
|
||||||
@@ -72,7 +74,7 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={listItemRef}
|
ref={listItemRef}
|
||||||
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${
|
||||||
selected && 'selected border-l-2 border-solid border-info'
|
selected && `selected border-l-2 border-solid border-accessory-tint-${tint}`
|
||||||
}`}
|
}`}
|
||||||
id={item.uuid}
|
id={item.uuid}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -85,27 +87,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
|||||||
<div className="pr-4" />
|
<div className="pr-4" />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-0">
|
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-0">
|
||||||
<div className="flex items-start justify-between overflow-hidden text-base font-semibold leading-[1.3]">
|
<ListItemTitle item={item} />
|
||||||
<div className="break-word mr-2">{item.title}</div>
|
<ListItemNotePreviewText item={item} hidePreview={hidePreview} />
|
||||||
</div>
|
|
||||||
{!hidePreview && !item.hidePreview && !item.protected && (
|
|
||||||
<div className="overflow-hidden overflow-ellipsis text-sm">
|
|
||||||
{item.preview_html && (
|
|
||||||
<div
|
|
||||||
className="my-1"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: sanitizeHtmlString(item.preview_html),
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
)}
|
|
||||||
{!item.preview_html && item.preview_plain && (
|
|
||||||
<div className="leading-1.3 line-clamp-1 mt-1 overflow-hidden">{item.preview_plain}</div>
|
|
||||||
)}
|
|
||||||
{!item.preview_html && !item.preview_plain && item.text && (
|
|
||||||
<div className="leading-1.3 line-clamp-1 mt-1 overflow-hidden">{item.text}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||||
<ListItemConflictIndicator item={item} />
|
<ListItemConflictIndicator item={item} />
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { FilesController } from '@/Controllers/FilesController'
|
import { FilesController } from '@/Controllers/FilesController'
|
||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
|
||||||
import { NotesController } from '@/Controllers/NotesController'
|
import { NotesController } from '@/Controllers/NotesController'
|
||||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
import { SortableItem, SNTag } from '@standardnotes/snjs'
|
||||||
import { SortableItem } from '@standardnotes/snjs'
|
|
||||||
import { ListableContentItem } from './ListableContentItem'
|
import { ListableContentItem } from './ListableContentItem'
|
||||||
|
|
||||||
export type AbstractListItemProps = {
|
export type AbstractListItemProps = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
filesController: FilesController
|
filesController: FilesController
|
||||||
selectionController: SelectedItemsController
|
|
||||||
navigationController: NavigationController
|
|
||||||
notesController: NotesController
|
notesController: NotesController
|
||||||
|
onSelect: (item: ListableContentItem, userTriggered?: boolean) => Promise<{ didSelect: boolean }>
|
||||||
hideDate: boolean
|
hideDate: boolean
|
||||||
hideIcon: boolean
|
hideIcon: boolean
|
||||||
hideTags: boolean
|
hideTags: boolean
|
||||||
@@ -19,4 +16,5 @@ export type AbstractListItemProps = {
|
|||||||
item: ListableContentItem
|
item: ListableContentItem
|
||||||
selected: boolean
|
selected: boolean
|
||||||
sortBy: keyof SortableItem | undefined
|
sortBy: keyof SortableItem | undefined
|
||||||
|
tags: SNTag[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
void filesController.setProtectionForFiles(hasProtectedFiles, selectionController.selectedFiles)
|
void filesController.setProtectionForFiles(hasProtectedFiles, selectionController.selectedFiles)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="password" className="mr-2 text-neutral" />
|
<Icon type="lock" className="mr-2 text-neutral" />
|
||||||
Password protection
|
Password protect
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<HorizontalSeparator classes="my-1" />
|
<HorizontalSeparator classes="my-1" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Icon type="password" className="mr-2 text-neutral" />
|
<Icon type="lock" className="mr-2 text-neutral" />
|
||||||
Password protection
|
Password protect
|
||||||
</span>
|
</span>
|
||||||
<Switch className="pointer-events-none px-0" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} checked={isFileProtected} />
|
<Switch className="pointer-events-none px-0" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} checked={isFileProtected} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
this.controller = props.controller
|
this.controller = props.controller
|
||||||
|
|
||||||
this.onEditorComponentLoad = () => {
|
this.onEditorComponentLoad = () => {
|
||||||
|
if (!this.controller || this.controller.dealloced) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.application.getDesktopService()?.redoSearch()
|
this.application.getDesktopService()?.redoSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +236,11 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
if (this.controller.isTemplateNote) {
|
if (this.controller.isTemplateNote) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.focusTitle()
|
if (this.controller.templateNoteOptions?.autofocusBehavior === 'editor') {
|
||||||
|
this.focusEditor()
|
||||||
|
} else {
|
||||||
|
this.focusTitle()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -920,6 +927,10 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
|
if (this.controller.dealloced) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state.showProtectedWarning || !this.application.isAuthorizedToRenderItem(this.note)) {
|
if (this.state.showProtectedWarning || !this.application.isAuthorizedToRenderItem(this.note)) {
|
||||||
return (
|
return (
|
||||||
<ProtectedItemOverlay
|
<ProtectedItemOverlay
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ const NotesOptions = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<Icon type="password" className={iconClass} />
|
<Icon type="lock" className={iconClass} />
|
||||||
Password protect
|
Password protect
|
||||||
</span>
|
</span>
|
||||||
<Switch className="px-0" checked={protect} />
|
<Switch className="px-0" checked={protect} />
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
WebAppEvent,
|
WebAppEvent,
|
||||||
NewNoteTitleFormat,
|
NewNoteTitleFormat,
|
||||||
useBoolean,
|
useBoolean,
|
||||||
|
TemplateNoteViewAutofocusBehavior,
|
||||||
|
isTag,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { WebApplication } from '../../Application/Application'
|
import { WebApplication } from '../../Application/Application'
|
||||||
@@ -165,18 +167,20 @@ export class ItemListController
|
|||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
application.addEventObserver(async () => {
|
application.addEventObserver(async () => {
|
||||||
void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => {
|
if (!this.completedFullSync) {
|
||||||
if (
|
void this.reloadItems(ItemsReloadSource.SyncEvent).then(() => {
|
||||||
this.notes.length === 0 &&
|
if (
|
||||||
this.navigationController.selected instanceof SmartView &&
|
this.notes.length === 0 &&
|
||||||
this.navigationController.selected.uuid === SystemViewId.AllNotes &&
|
this.navigationController.selected instanceof SmartView &&
|
||||||
this.noteFilterText === '' &&
|
this.navigationController.selected.uuid === SystemViewId.AllNotes &&
|
||||||
!this.getActiveItemController()
|
this.noteFilterText === '' &&
|
||||||
) {
|
!this.getActiveItemController()
|
||||||
this.createPlaceholderNote()?.catch(console.error)
|
) {
|
||||||
}
|
this.createPlaceholderNote()?.catch(console.error)
|
||||||
})
|
}
|
||||||
this.setCompletedFullSync(true)
|
})
|
||||||
|
this.setCompletedFullSync(true)
|
||||||
|
}
|
||||||
}, ApplicationEvent.CompletedFullSync),
|
}, ApplicationEvent.CompletedFullSync),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -211,6 +215,7 @@ export class ItemListController
|
|||||||
notesToDisplay: observable,
|
notesToDisplay: observable,
|
||||||
panelTitle: observable,
|
panelTitle: observable,
|
||||||
panelWidth: observable,
|
panelWidth: observable,
|
||||||
|
items: observable,
|
||||||
renderedItems: observable,
|
renderedItems: observable,
|
||||||
showDisplayOptionsMenu: observable,
|
showDisplayOptionsMenu: observable,
|
||||||
|
|
||||||
@@ -253,7 +258,7 @@ export class ItemListController
|
|||||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
if (event.type === CrossControllerEvent.TagChanged) {
|
if (event.type === CrossControllerEvent.TagChanged) {
|
||||||
const payload = event.payload as { userTriggered: boolean }
|
const payload = event.payload as { userTriggered: boolean }
|
||||||
this.handleTagChange(payload.userTriggered)
|
await this.handleTagChange(payload.userTriggered)
|
||||||
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
|
} else if (event.type === CrossControllerEvent.ActiveEditorChanged) {
|
||||||
this.handleEditorChange().catch(console.error)
|
this.handleEditorChange().catch(console.error)
|
||||||
}
|
}
|
||||||
@@ -375,12 +380,6 @@ export class ItemListController
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
|
||||||
return (
|
|
||||||
itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange || !this.selectionController.selectedUuids.size
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
||||||
const isSearching = this.noteFilterText.length > 0
|
const isSearching = this.noteFilterText.length > 0
|
||||||
const itemExistsInUpdatedResults = this.items.find((item) => item.uuid === activeItem?.uuid)
|
const itemExistsInUpdatedResults = this.items.find((item) => item.uuid === activeItem?.uuid)
|
||||||
@@ -404,6 +403,18 @@ export class ItemListController
|
|||||||
return activeItem && !this.selectionController.isItemSelected(activeItem)
|
return activeItem && !this.selectionController.isItemSelected(activeItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
||||||
|
const selectedTag = this.navigationController.selected
|
||||||
|
const isDailyEntry = selectedTag && isTag(selectedTag) && selectedTag.isDailyEntry
|
||||||
|
if (isDailyEntry) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const userChangedTag = itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange
|
||||||
|
const hasNoSelectedItem = !this.selectionController.selectedUuids.size
|
||||||
|
return userChangedTag || hasNoSelectedItem
|
||||||
|
}
|
||||||
|
|
||||||
private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
|
private async recomputeSelectionAfterItemsReload(itemsReloadSource: ItemsReloadSource) {
|
||||||
const activeController = this.getActiveItemController()
|
const activeController = this.getActiveItemController()
|
||||||
|
|
||||||
@@ -561,48 +572,65 @@ export class ItemListController
|
|||||||
return { didReloadItems: true }
|
return { didReloadItems: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNewNoteController(title?: string) {
|
async createNewNoteController(
|
||||||
|
title?: string,
|
||||||
|
createdAt?: Date,
|
||||||
|
autofocusBehavior?: TemplateNoteViewAutofocusBehavior,
|
||||||
|
) {
|
||||||
const selectedTag = this.navigationController.selected
|
const selectedTag = this.navigationController.selected
|
||||||
|
|
||||||
const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined
|
const activeRegularTagUuid = selectedTag instanceof SNTag ? selectedTag.uuid : undefined
|
||||||
|
|
||||||
await this.application.itemControllerGroup.createItemController({
|
return this.application.itemControllerGroup.createItemController({
|
||||||
title,
|
title,
|
||||||
tag: activeRegularTagUuid,
|
tag: activeRegularTagUuid,
|
||||||
|
createdAt,
|
||||||
|
autofocusBehavior,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewNote = async () => {
|
titleForNewNote = (createdAt?: Date) => {
|
||||||
this.notesController.unselectNotes()
|
if (this.isFiltering) {
|
||||||
|
return this.noteFilterText
|
||||||
if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) {
|
|
||||||
await this.navigationController.selectHomeNavigationView()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleFormat =
|
const titleFormat =
|
||||||
this.navigationController.selected?.preferences?.newNoteTitleFormat ||
|
this.navigationController.selected?.preferences?.newNoteTitleFormat ||
|
||||||
this.application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat])
|
this.application.getPreference(PrefKey.NewNoteTitleFormat, PrefDefaults[PrefKey.NewNoteTitleFormat])
|
||||||
|
|
||||||
let title = formatDateAndTimeForNote(new Date())
|
|
||||||
|
|
||||||
if (titleFormat === NewNoteTitleFormat.CurrentNoteCount) {
|
if (titleFormat === NewNoteTitleFormat.CurrentNoteCount) {
|
||||||
title = `Note ${this.notes.length + 1}`
|
return `Note ${this.notes.length + 1}`
|
||||||
} else if (titleFormat === NewNoteTitleFormat.CustomFormat) {
|
}
|
||||||
|
|
||||||
|
if (titleFormat === NewNoteTitleFormat.CustomFormat) {
|
||||||
const customFormat =
|
const customFormat =
|
||||||
this.navigationController.selected?.preferences?.customNoteTitleFormat ||
|
this.navigationController.selected?.preferences?.customNoteTitleFormat ||
|
||||||
this.application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat])
|
this.application.getPreference(PrefKey.CustomNoteTitleFormat, PrefDefaults[PrefKey.CustomNoteTitleFormat])
|
||||||
title = dayjs().format(customFormat)
|
|
||||||
} else if (titleFormat === NewNoteTitleFormat.Empty) {
|
return dayjs(createdAt).format(customFormat)
|
||||||
title = ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isFiltering) {
|
if (titleFormat === NewNoteTitleFormat.Empty) {
|
||||||
title = this.noteFilterText
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.createNewNoteController(title)
|
return formatDateAndTimeForNote(createdAt || new Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewNote = async (title?: string, createdAt?: Date, autofocusBehavior?: TemplateNoteViewAutofocusBehavior) => {
|
||||||
|
this.notesController.unselectNotes()
|
||||||
|
|
||||||
|
if (this.navigationController.isInSmartView() && !this.navigationController.isInHomeView()) {
|
||||||
|
await this.navigationController.selectHomeNavigationView()
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTitle = title || this.titleForNewNote(createdAt)
|
||||||
|
|
||||||
|
const controller = await this.createNewNoteController(useTitle, createdAt, autofocusBehavior)
|
||||||
|
|
||||||
this.linkingController.reloadAllLinks()
|
this.linkingController.reloadAllLinks()
|
||||||
|
|
||||||
|
this.selectionController.scrollToItem(controller.item)
|
||||||
}
|
}
|
||||||
|
|
||||||
createPlaceholderNote = () => {
|
createPlaceholderNote = () => {
|
||||||
@@ -721,7 +749,7 @@ export class ItemListController
|
|||||||
this.application.itemControllerGroup.closeItemController(controller)
|
this.application.itemControllerGroup.closeItemController(controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTagChange = (userTriggered: boolean) => {
|
handleTagChange = async (userTriggered: boolean) => {
|
||||||
const activeNoteController = this.getActiveItemController()
|
const activeNoteController = this.getActiveItemController()
|
||||||
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
|
if (activeNoteController instanceof NoteViewController && activeNoteController.isTemplateNote) {
|
||||||
this.closeItemController(activeNoteController)
|
this.closeItemController(activeNoteController)
|
||||||
@@ -739,7 +767,7 @@ export class ItemListController
|
|||||||
|
|
||||||
this.reloadNotesDisplayOptions()
|
this.reloadNotesDisplayOptions()
|
||||||
|
|
||||||
void this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
|
await this.reloadItems(userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.TagChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
onFilterEnter = () => {
|
onFilterEnter = () => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
InternalEventBus,
|
InternalEventBus,
|
||||||
InternalEventPublishStrategy,
|
InternalEventPublishStrategy,
|
||||||
VectorIconNameOrEmoji,
|
VectorIconNameOrEmoji,
|
||||||
|
isTag,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
|
import { action, computed, makeAutoObservable, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||||
import { WebApplication } from '../../Application/Application'
|
import { WebApplication } from '../../Application/Application'
|
||||||
@@ -268,6 +269,13 @@ export class NavigationController
|
|||||||
return this.selected instanceof SmartView && this.selected.uuid === id
|
return this.selected instanceof SmartView && this.selected.uuid === id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get selectedAsTag(): SNTag | undefined {
|
||||||
|
if (!this.selected || !isTag(this.selected)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this.selected
|
||||||
|
}
|
||||||
|
|
||||||
setAddingSubtagTo(tag: SNTag | undefined): void {
|
setAddingSubtagTo(tag: SNTag | undefined): void {
|
||||||
this.addingSubtagTo = tag
|
this.addingSubtagTo = tag
|
||||||
}
|
}
|
||||||
@@ -440,19 +448,21 @@ export class NavigationController
|
|||||||
|
|
||||||
this.previouslySelected_ = this.selected_
|
this.previouslySelected_ = this.selected_
|
||||||
|
|
||||||
this.setSelectedTagInstance(tag)
|
await runInAction(async () => {
|
||||||
|
this.setSelectedTagInstance(tag)
|
||||||
|
|
||||||
if (tag && this.application.items.isTemplateItem(tag)) {
|
if (tag && this.application.items.isTemplateItem(tag)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.eventBus.publishSync(
|
await this.eventBus.publishSync(
|
||||||
{
|
{
|
||||||
type: CrossControllerEvent.TagChanged,
|
type: CrossControllerEvent.TagChanged,
|
||||||
payload: { tag, previousTag: this.previouslySelected_, userTriggered: userTriggered },
|
payload: { tag, previousTag: this.previouslySelected_, userTriggered: userTriggered },
|
||||||
},
|
},
|
||||||
InternalEventPublishStrategy.SEQUENCE,
|
InternalEventPublishStrategy.SEQUENCE,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public async selectHomeNavigationView(): Promise<void> {
|
public async selectHomeNavigationView(): Promise<void> {
|
||||||
|
|||||||
@@ -287,18 +287,22 @@ export class SelectedItemsController
|
|||||||
item: {
|
item: {
|
||||||
uuid: ListableContentItem['uuid']
|
uuid: ListableContentItem['uuid']
|
||||||
},
|
},
|
||||||
{ userTriggered = false, scrollIntoView = true },
|
{ userTriggered = false, scrollIntoView = true, animated = true },
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
|
const { didSelect } = await this.selectItem(item.uuid, userTriggered)
|
||||||
|
|
||||||
if (didSelect && scrollIntoView) {
|
if (didSelect && scrollIntoView) {
|
||||||
const itemElement = document.getElementById(item.uuid)
|
this.scrollToItem(item, animated)
|
||||||
itemElement?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToItem = (item: { uuid: ListableContentItem['uuid'] }, animated = true): void => {
|
||||||
|
const itemElement = document.getElementById(item.uuid)
|
||||||
|
itemElement?.scrollIntoView({
|
||||||
|
behavior: animated ? 'smooth' : 'auto',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
selectUuids = async (uuids: UuidString[], userTriggered = false) => {
|
selectUuids = async (uuids: UuidString[], userTriggered = false) => {
|
||||||
const itemsForUuids = this.application.items.findItems(uuids)
|
const itemsForUuids = this.application.items.findItems(uuids)
|
||||||
if (itemsForUuids.length < 1) {
|
if (itemsForUuids.length < 1) {
|
||||||
|
|||||||
31
packages/web/src/javascripts/Utils/DateUtils.spec.ts
Normal file
31
packages/web/src/javascripts/Utils/DateUtils.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { addDays } from '@/Utils/DateUtils'
|
||||||
|
import { numDaysBetweenDates } from './DateUtils'
|
||||||
|
|
||||||
|
describe('date utils', () => {
|
||||||
|
describe('numDaysBetweenDates', () => {
|
||||||
|
it('should return full days diff accurately', () => {
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
expect(numDaysBetweenDates(today, addDays(today, 1))).toEqual(1)
|
||||||
|
expect(numDaysBetweenDates(today, addDays(today, 2))).toEqual(2)
|
||||||
|
expect(numDaysBetweenDates(today, addDays(today, 3))).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return absolute value of difference', () => {
|
||||||
|
const today = new Date()
|
||||||
|
|
||||||
|
expect(numDaysBetweenDates(today, addDays(today, 3))).toEqual(3)
|
||||||
|
expect(numDaysBetweenDates(addDays(today, 3), today)).toEqual(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return 1 day difference between two dates on different days but 1 hour apart', () => {
|
||||||
|
const today = new Date()
|
||||||
|
const oneHourBeforeMidnight = new Date()
|
||||||
|
oneHourBeforeMidnight.setHours(0, 0, 0, 0)
|
||||||
|
oneHourBeforeMidnight.setHours(-1, 0, 0, 0)
|
||||||
|
|
||||||
|
expect(today.toDateString()).not.toEqual(oneHourBeforeMidnight.toDateString())
|
||||||
|
expect(numDaysBetweenDates(today, oneHourBeforeMidnight)).toEqual(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -12,13 +12,84 @@ export const formatDateForContextMenu = (date: Date | undefined) => {
|
|||||||
return `${date.toDateString()} ${date.toLocaleTimeString()}`
|
return `${date.toDateString()} ${date.toLocaleTimeString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDateAndTimeForNote = (date: Date) => {
|
export const formatDateAndTimeForNote = (date: Date, includeTime = true) => {
|
||||||
return `${date.toLocaleDateString(undefined, {
|
const dateString = `${date.toLocaleDateString(undefined, {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})} at ${date.toLocaleTimeString(undefined, {
|
|
||||||
timeStyle: 'short',
|
|
||||||
})}`
|
})}`
|
||||||
|
|
||||||
|
if (includeTime) {
|
||||||
|
return `${dateString} at ${date.toLocaleTimeString(undefined, {
|
||||||
|
timeStyle: 'short',
|
||||||
|
})}`
|
||||||
|
} else {
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numHoursBetweenDates(date1: Date, date2: Date): number {
|
||||||
|
return Math.abs(date1.getTime() - date2.getTime()) / 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDateInSameDay(date1: Date, date2: Date): boolean {
|
||||||
|
return date1.toLocaleDateString() === date2.toLocaleDateString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numDaysBetweenDates(date1: Date, date2: Date): number {
|
||||||
|
if (numHoursBetweenDates(date1, date2) < 24) {
|
||||||
|
const dayOfWeekDiffers = date1.toLocaleDateString() !== date2.toLocaleDateString()
|
||||||
|
if (dayOfWeekDiffers) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const diffInMs = date1.getTime() - date2.getTime()
|
||||||
|
const diffInDays = Math.abs(diffInMs / (1000 * 60 * 60 * 24))
|
||||||
|
return Math.floor(diffInDays)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDays(date: Date, days: number) {
|
||||||
|
const result = new Date(date)
|
||||||
|
result.setDate(result.getDate() + days)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMonths(date: Date, months: number) {
|
||||||
|
const result = new Date(date)
|
||||||
|
result.setMonth(result.getMonth() + months)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeekdayName(date: Date, format: 'long' | 'short'): string {
|
||||||
|
return date.toLocaleString('default', { weekday: format })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function areDatesInSameMonth(date1: Date, date2: Date): boolean {
|
||||||
|
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberOfMonthsBetweenDates(date1: Date, date2: Date, roundUpFractionalMonths = true) {
|
||||||
|
let startDate = date1
|
||||||
|
let endDate = date2
|
||||||
|
let inverse = false
|
||||||
|
|
||||||
|
if (date1 > date2) {
|
||||||
|
startDate = date2
|
||||||
|
endDate = date1
|
||||||
|
inverse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearsDifference = endDate.getFullYear() - startDate.getFullYear()
|
||||||
|
const monthsDifference = endDate.getMonth() - startDate.getMonth()
|
||||||
|
const daysDifference = endDate.getDate() - startDate.getDate()
|
||||||
|
|
||||||
|
let monthCorrection = 0
|
||||||
|
if (roundUpFractionalMonths === true && daysDifference > 0) {
|
||||||
|
monthCorrection = 1
|
||||||
|
} else if (roundUpFractionalMonths !== true && daysDifference < 0) {
|
||||||
|
monthCorrection = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return (inverse ? -1 : 1) * (yearsDifference * 12 + monthsDifference + monthCorrection)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,7 +146,6 @@
|
|||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: var(--item-cell-selected-background-color);
|
background-color: var(--item-cell-selected-background-color);
|
||||||
border-left: 2px solid var(--item-cell-selected-border-left-color);
|
|
||||||
|
|
||||||
progress {
|
progress {
|
||||||
background-color: var(--note-preview-selected-progress-background-color);
|
background-color: var(--note-preview-selected-progress-background-color);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ module.exports = {
|
|||||||
'warning-contrast': 'var(--sn-stylekit-warning-contrast-color)',
|
'warning-contrast': 'var(--sn-stylekit-warning-contrast-color)',
|
||||||
danger: 'var(--sn-stylekit-danger-color)',
|
danger: 'var(--sn-stylekit-danger-color)',
|
||||||
'danger-contrast': 'var(--sn-stylekit-danger-contrast-color)',
|
'danger-contrast': 'var(--sn-stylekit-danger-contrast-color)',
|
||||||
|
'danger-light': 'var(--sn-stylekit-danger-light-color, var(--sn-stylekit-danger-color))',
|
||||||
default: 'var(--sn-stylekit-background-color)',
|
default: 'var(--sn-stylekit-background-color)',
|
||||||
foreground: 'var(--sn-stylekit-foreground-color)',
|
foreground: 'var(--sn-stylekit-foreground-color)',
|
||||||
contrast: 'var(--sn-stylekit-contrast-background-color)',
|
contrast: 'var(--sn-stylekit-contrast-background-color)',
|
||||||
|
|||||||
Reference in New Issue
Block a user