feat: starred notes (#1813)
This commit is contained in:
@@ -13,6 +13,7 @@ export interface ItemContent {
|
|||||||
trashed?: boolean
|
trashed?: boolean
|
||||||
pinned?: boolean
|
pinned?: boolean
|
||||||
archived?: boolean
|
archived?: boolean
|
||||||
|
starred?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
appData?: AppData
|
appData?: AppData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class DecryptedItem<C extends ItemContent = ItemContent>
|
|||||||
public readonly pinned: boolean = false
|
public readonly pinned: boolean = false
|
||||||
public readonly archived: boolean = false
|
public readonly archived: boolean = false
|
||||||
public readonly locked: boolean = false
|
public readonly locked: boolean = false
|
||||||
|
public readonly starred: boolean = false
|
||||||
|
|
||||||
constructor(payload: DecryptedPayloadInterface<C>) {
|
constructor(payload: DecryptedPayloadInterface<C>) {
|
||||||
super(payload)
|
super(payload)
|
||||||
@@ -32,6 +33,7 @@ export class DecryptedItem<C extends ItemContent = ItemContent>
|
|||||||
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
||||||
this.protected = useBoolean(this.payload.content.protected, false)
|
this.protected = useBoolean(this.payload.content.protected, false)
|
||||||
this.trashed = useBoolean(this.payload.content.trashed, false)
|
this.trashed = useBoolean(this.payload.content.trashed, false)
|
||||||
|
this.starred = useBoolean(this.payload.content.starred, false)
|
||||||
this.pinned = this.getAppDomainValueWithDefault(AppDataField.Pinned, false)
|
this.pinned = this.getAppDomainValueWithDefault(AppDataField.Pinned, false)
|
||||||
this.archived = this.getAppDomainValueWithDefault(AppDataField.Archived, false)
|
this.archived = this.getAppDomainValueWithDefault(AppDataField.Archived, false)
|
||||||
this.locked = this.getAppDomainValueWithDefault(AppDataField.Locked, false)
|
this.locked = this.getAppDomainValueWithDefault(AppDataField.Locked, false)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface DecryptedItemInterface<C extends ItemContent = ItemContent>
|
|||||||
readonly pinned: boolean
|
readonly pinned: boolean
|
||||||
readonly archived: boolean
|
readonly archived: boolean
|
||||||
readonly locked: boolean
|
readonly locked: boolean
|
||||||
|
readonly starred: boolean
|
||||||
readonly userModifiedDate: Date
|
readonly userModifiedDate: Date
|
||||||
readonly references: ContentReference[]
|
readonly references: ContentReference[]
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ export class DecryptedItemMutator<C extends ItemContent = ItemContent> extends I
|
|||||||
this.mutableContent.trashed = trashed
|
this.mutableContent.trashed = trashed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set starred(starred: boolean) {
|
||||||
|
this.mutableContent.starred = starred
|
||||||
|
}
|
||||||
|
|
||||||
public set pinned(pinned: boolean) {
|
public set pinned(pinned: boolean) {
|
||||||
this.setAppDataItem(AppDataField.Pinned, pinned)
|
this.setAppDataItem(AppDataField.Pinned, pinned)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export enum SystemViewId {
|
|||||||
ArchivedNotes = 'archived-notes',
|
ArchivedNotes = 'archived-notes',
|
||||||
TrashedNotes = 'trashed-notes',
|
TrashedNotes = 'trashed-notes',
|
||||||
UntaggedNotes = 'untagged-notes',
|
UntaggedNotes = 'untagged-notes',
|
||||||
|
StarredNotes = 'starred-notes',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SmartViewContent extends ItemContent {
|
export interface SmartViewContent extends ItemContent {
|
||||||
|
|||||||
@@ -74,10 +74,22 @@ export function BuildSmartViews(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const starred = new SmartView(
|
||||||
|
new DecryptedPayload({
|
||||||
|
uuid: SystemViewId.StarredNotes,
|
||||||
|
content_type: ContentType.SmartView,
|
||||||
|
...PayloadTimestampDefaults(),
|
||||||
|
content: FillItemContent<SmartViewContent>({
|
||||||
|
title: 'Starred',
|
||||||
|
predicate: starredNotesPredicate(options).toJson(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
if (supportsFileNavigation) {
|
if (supportsFileNavigation) {
|
||||||
return [notes, files, archived, trash, untagged]
|
return [notes, starred, files, archived, trash, untagged]
|
||||||
} else {
|
} else {
|
||||||
return [notes, archived, trash, untagged]
|
return [notes, starred, archived, trash, untagged]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,3 +189,22 @@ function untaggedNotesPredicate(options: FilterDisplayOptions) {
|
|||||||
|
|
||||||
return predicate
|
return predicate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function starredNotesPredicate(options: FilterDisplayOptions) {
|
||||||
|
const subPredicates: Predicate<SNNote>[] = [
|
||||||
|
new Predicate('starred', '=', true),
|
||||||
|
new Predicate('content_type', '=', ContentType.Note),
|
||||||
|
]
|
||||||
|
if (options.includeTrashed === false) {
|
||||||
|
subPredicates.push(new Predicate('trashed', '=', false))
|
||||||
|
}
|
||||||
|
if (options.includeProtected === false) {
|
||||||
|
subPredicates.push(new Predicate('protected', '=', false))
|
||||||
|
}
|
||||||
|
if (options.includePinned === false) {
|
||||||
|
subPredicates.push(new Predicate('pinned', '=', false))
|
||||||
|
}
|
||||||
|
const predicate = new CompoundPredicate('and', subPredicates)
|
||||||
|
|
||||||
|
return predicate
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type Props = {
|
|||||||
trashed: ListableContentItem['trashed']
|
trashed: ListableContentItem['trashed']
|
||||||
archived: ListableContentItem['archived']
|
archived: ListableContentItem['archived']
|
||||||
pinned: ListableContentItem['pinned']
|
pinned: ListableContentItem['pinned']
|
||||||
|
starred: ListableContentItem['starred']
|
||||||
}
|
}
|
||||||
hasFiles?: boolean
|
hasFiles?: boolean
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,11 @@ const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false })
|
|||||||
<Icon ariaLabel="Files" type="attachment-file" className="text-info" size="small" />
|
<Icon ariaLabel="Files" type="attachment-file" className="text-info" size="small" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{item.starred && (
|
||||||
|
<span className="ml-1.5 flex items-center" title="Starred">
|
||||||
|
<Icon ariaLabel="Starred" type="star-filled" className="text-warning" size="small" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ const NotesOptions = ({
|
|||||||
const notTrashed = notes.some((note) => !note.trashed)
|
const notTrashed = notes.some((note) => !note.trashed)
|
||||||
const pinned = notes.some((note) => note.pinned)
|
const pinned = notes.some((note) => note.pinned)
|
||||||
const unpinned = notes.some((note) => !note.pinned)
|
const unpinned = notes.some((note) => !note.pinned)
|
||||||
|
const starred = notes.some((note) => note.starred)
|
||||||
|
|
||||||
const editorForNote = useMemo(
|
const editorForNote = useMemo(
|
||||||
() => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined),
|
() => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined),
|
||||||
@@ -330,6 +331,17 @@ const NotesOptions = ({
|
|||||||
linkingController={linkingController}
|
linkingController={linkingController}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item"
|
||||||
|
onClick={() => {
|
||||||
|
notesController.setStarSelectedNotes(!starred)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="star" className={iconClass} />
|
||||||
|
{starred ? 'Unstar' : 'Star'}
|
||||||
|
</button>
|
||||||
|
|
||||||
{unpinned && (
|
{unpinned && (
|
||||||
<button
|
<button
|
||||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item"
|
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-menu-item"
|
||||||
|
|||||||
@@ -27,23 +27,24 @@ const PADDING_BASE_PX = 14
|
|||||||
const PADDING_PER_LEVEL_PX = 21
|
const PADDING_PER_LEVEL_PX = 21
|
||||||
|
|
||||||
const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
|
const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
|
||||||
if (view.uuid === SystemViewId.AllNotes) {
|
const mapping: Record<SystemViewId, IconType> = {
|
||||||
return isSelected ? 'notes-filled' : 'notes'
|
[SystemViewId.AllNotes]: isSelected ? 'notes-filled' : 'notes',
|
||||||
}
|
[SystemViewId.Files]: 'folder',
|
||||||
if (view.uuid === SystemViewId.Files) {
|
[SystemViewId.ArchivedNotes]: 'archive',
|
||||||
return 'folder'
|
[SystemViewId.TrashedNotes]: 'trash',
|
||||||
}
|
[SystemViewId.UntaggedNotes]: 'hashtag-off',
|
||||||
if (view.uuid === SystemViewId.ArchivedNotes) {
|
[SystemViewId.StarredNotes]: 'star-filled',
|
||||||
return 'archive'
|
|
||||||
}
|
|
||||||
if (view.uuid === SystemViewId.TrashedNotes) {
|
|
||||||
return 'trash'
|
|
||||||
}
|
|
||||||
if (view.uuid === SystemViewId.UntaggedNotes) {
|
|
||||||
return 'hashtag-off'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'hashtag'
|
return mapping[view.uuid as SystemViewId] || 'hashtag'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIconClass = (view: SmartView, isSelected: boolean): string => {
|
||||||
|
const mapping: Partial<Record<SystemViewId, string>> = {
|
||||||
|
[SystemViewId.StarredNotes]: 'text-warning',
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping[view.uuid as SystemViewId] || (isSelected ? 'text-info' : 'text-neutral')
|
||||||
}
|
}
|
||||||
|
|
||||||
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||||
@@ -108,6 +109,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
|||||||
|
|
||||||
const isFaded = false
|
const isFaded = false
|
||||||
const iconType = smartViewIconType(view, isSelected)
|
const iconType = smartViewIconType(view, isSelected)
|
||||||
|
const iconClass = getIconClass(view, isSelected)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -122,7 +124,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
|||||||
>
|
>
|
||||||
<div className="tag-info">
|
<div className="tag-info">
|
||||||
<div className={'tag-icon mr-2'}>
|
<div className={'tag-icon mr-2'}>
|
||||||
<Icon type={iconType} className={isSelected ? 'text-info' : 'text-neutral'} />
|
<Icon type={iconType} className={iconClass} />
|
||||||
</div>
|
</div>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -259,6 +259,12 @@ export class NotesController extends AbstractViewController {
|
|||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStarSelectedNotes(starred: boolean): void {
|
||||||
|
this.changeSelectedNotes((mutator) => {
|
||||||
|
mutator.starred = starred
|
||||||
|
}).catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
||||||
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
||||||
this.application.alertService
|
this.application.alertService
|
||||||
|
|||||||
Reference in New Issue
Block a user