feat: starred notes (#1813)
This commit is contained in:
@@ -13,6 +13,7 @@ export interface ItemContent {
|
||||
trashed?: boolean
|
||||
pinned?: boolean
|
||||
archived?: boolean
|
||||
starred?: boolean
|
||||
locked?: boolean
|
||||
appData?: AppData
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export class DecryptedItem<C extends ItemContent = ItemContent>
|
||||
public readonly pinned: boolean = false
|
||||
public readonly archived: boolean = false
|
||||
public readonly locked: boolean = false
|
||||
public readonly starred: boolean = false
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<C>) {
|
||||
super(payload)
|
||||
@@ -32,6 +33,7 @@ export class DecryptedItem<C extends ItemContent = ItemContent>
|
||||
this.updatedAtString = dateToLocalizedString(this.userModifiedDate)
|
||||
this.protected = useBoolean(this.payload.content.protected, 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.archived = this.getAppDomainValueWithDefault(AppDataField.Archived, false)
|
||||
this.locked = this.getAppDomainValueWithDefault(AppDataField.Locked, false)
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface DecryptedItemInterface<C extends ItemContent = ItemContent>
|
||||
readonly pinned: boolean
|
||||
readonly archived: boolean
|
||||
readonly locked: boolean
|
||||
readonly starred: boolean
|
||||
readonly userModifiedDate: Date
|
||||
readonly references: ContentReference[]
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ export class DecryptedItemMutator<C extends ItemContent = ItemContent> extends I
|
||||
this.mutableContent.trashed = trashed
|
||||
}
|
||||
|
||||
set starred(starred: boolean) {
|
||||
this.mutableContent.starred = starred
|
||||
}
|
||||
|
||||
public set pinned(pinned: boolean) {
|
||||
this.setAppDataItem(AppDataField.Pinned, pinned)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum SystemViewId {
|
||||
ArchivedNotes = 'archived-notes',
|
||||
TrashedNotes = 'trashed-notes',
|
||||
UntaggedNotes = 'untagged-notes',
|
||||
StarredNotes = 'starred-notes',
|
||||
}
|
||||
|
||||
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) {
|
||||
return [notes, files, archived, trash, untagged]
|
||||
return [notes, starred, files, archived, trash, untagged]
|
||||
} else {
|
||||
return [notes, archived, trash, untagged]
|
||||
return [notes, starred, archived, trash, untagged]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,3 +189,22 @@ function untaggedNotesPredicate(options: FilterDisplayOptions) {
|
||||
|
||||
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']
|
||||
archived: ListableContentItem['archived']
|
||||
pinned: ListableContentItem['pinned']
|
||||
starred: ListableContentItem['starred']
|
||||
}
|
||||
hasFiles?: boolean
|
||||
}
|
||||
@@ -40,6 +41,11 @@ const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false })
|
||||
<Icon ariaLabel="Files" type="attachment-file" className="text-info" size="small" />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@ const NotesOptions = ({
|
||||
const notTrashed = notes.some((note) => !note.trashed)
|
||||
const pinned = notes.some((note) => note.pinned)
|
||||
const unpinned = notes.some((note) => !note.pinned)
|
||||
const starred = notes.some((note) => note.starred)
|
||||
|
||||
const editorForNote = useMemo(
|
||||
() => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined),
|
||||
@@ -330,6 +331,17 @@ const NotesOptions = ({
|
||||
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 && (
|
||||
<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"
|
||||
|
||||
@@ -27,23 +27,24 @@ const PADDING_BASE_PX = 14
|
||||
const PADDING_PER_LEVEL_PX = 21
|
||||
|
||||
const smartViewIconType = (view: SmartView, isSelected: boolean): IconType => {
|
||||
if (view.uuid === SystemViewId.AllNotes) {
|
||||
return isSelected ? 'notes-filled' : 'notes'
|
||||
}
|
||||
if (view.uuid === SystemViewId.Files) {
|
||||
return 'folder'
|
||||
}
|
||||
if (view.uuid === SystemViewId.ArchivedNotes) {
|
||||
return 'archive'
|
||||
}
|
||||
if (view.uuid === SystemViewId.TrashedNotes) {
|
||||
return 'trash'
|
||||
}
|
||||
if (view.uuid === SystemViewId.UntaggedNotes) {
|
||||
return 'hashtag-off'
|
||||
const mapping: Record<SystemViewId, IconType> = {
|
||||
[SystemViewId.AllNotes]: isSelected ? 'notes-filled' : 'notes',
|
||||
[SystemViewId.Files]: 'folder',
|
||||
[SystemViewId.ArchivedNotes]: 'archive',
|
||||
[SystemViewId.TrashedNotes]: 'trash',
|
||||
[SystemViewId.UntaggedNotes]: 'hashtag-off',
|
||||
[SystemViewId.StarredNotes]: 'star-filled',
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
@@ -108,6 +109,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
|
||||
const isFaded = false
|
||||
const iconType = smartViewIconType(view, isSelected)
|
||||
const iconClass = getIconClass(view, isSelected)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -122,7 +124,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState }) => {
|
||||
>
|
||||
<div className="tag-info">
|
||||
<div className={'tag-icon mr-2'}>
|
||||
<Icon type={iconType} className={isSelected ? 'text-info' : 'text-neutral'} />
|
||||
<Icon type={iconType} className={iconClass} />
|
||||
</div>
|
||||
{isEditing ? (
|
||||
<input
|
||||
|
||||
@@ -259,6 +259,12 @@ export class NotesController extends AbstractViewController {
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
||||
setStarSelectedNotes(starred: boolean): void {
|
||||
this.changeSelectedNotes((mutator) => {
|
||||
mutator.starred = starred
|
||||
}).catch(console.error)
|
||||
}
|
||||
|
||||
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
||||
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
||||
this.application.alertService
|
||||
|
||||
Reference in New Issue
Block a user