feat: New notes list design (#780)

This commit is contained in:
Aman Harwara
2022-01-04 17:29:10 +05:30
committed by GitHub
parent 954f39992d
commit 7dd4a60595
20 changed files with 296 additions and 155 deletions

View File

@@ -13,6 +13,7 @@ import { useState } from 'preact/hooks';
export type DropdownItem = {
icon?: IconType;
iconClassName?: string;
label: string;
value: string;
};
@@ -25,10 +26,7 @@ type DropdownProps = {
onChange: (value: string) => void;
};
type ListboxButtonProps = {
icon?: IconType;
value: string | null;
label: string;
type ListboxButtonProps = DropdownItem & {
isExpanded: boolean;
};
@@ -36,12 +34,13 @@ const CustomDropdownButton: FunctionComponent<ListboxButtonProps> = ({
label,
isExpanded,
icon,
iconClassName = '',
}) => (
<>
<div className="sn-dropdown-button-label">
{icon ? (
<div className="flex mr-2">
<Icon type={icon} className="sn-icon--small" />
<Icon type={icon} className={`sn-icon--small ${iconClassName}`} />
</div>
) : null}
<div className="dropdown-selected-label">{label}</div>
@@ -85,11 +84,13 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({
children={({ value, label, isExpanded }) => {
const current = items.find((item) => item.value === value);
const icon = current ? current?.icon : null;
const iconClassName = current ? current?.iconClassName : null;
return CustomDropdownButton({
value,
value: value ? value : label.toLowerCase(),
label,
isExpanded,
...(icon ? { icon } : null),
...(iconClassName ? { iconClassName } : null),
});
}}
/>
@@ -104,7 +105,10 @@ export const Dropdown: FunctionComponent<DropdownProps> = ({
>
{item.icon ? (
<div className="flex mr-3">
<Icon type={item.icon} className="sn-icon--small" />
<Icon
type={item.icon}
className={`sn-icon--small ${item.iconClassName ?? ''}`}
/>
</div>
) : null}
<div className="text-input">{item.label}</div>

View File

@@ -3,7 +3,9 @@ import PencilOffIcon from '../../icons/ic-pencil-off.svg';
import PlainTextIcon from '../../icons/ic-text-paragraph.svg';
import RichTextIcon from '../../icons/ic-text-rich.svg';
import TrashIcon from '../../icons/ic-trash.svg';
import TrashFilledIcon from '../../icons/ic-trash-filled.svg';
import PinIcon from '../../icons/ic-pin.svg';
import PinFilledIcon from '../../icons/ic-pin-filled.svg';
import UnpinIcon from '../../icons/ic-pin-off.svg';
import ArchiveIcon from '../../icons/ic-archive.svg';
import UnarchiveIcon from '../../icons/ic-unarchive.svg';
@@ -52,6 +54,7 @@ import ServerIcon from '../../icons/ic-server.svg';
import EyeIcon from '../../icons/ic-eye.svg';
import EyeOffIcon from '../../icons/ic-eye-off.svg';
import LockIcon from '../../icons/ic-lock.svg';
import LockFilledIcon from '../../icons/ic-lock-filled.svg';
import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg';
import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg';
import WindowIcon from '../../icons/ic-window.svg';
@@ -69,6 +72,7 @@ const ICONS = {
'arrows-sort-up': ArrowsSortUpIcon,
'arrows-sort-down': ArrowsSortDownIcon,
lock: LockIcon,
'lock-filled': LockFilledIcon,
eye: EyeIcon,
'eye-off': EyeOffIcon,
server: ServerIcon,
@@ -89,7 +93,9 @@ const ICONS = {
spreadsheets: SpreadsheetsIcon,
tasks: TasksIcon,
trash: TrashIcon,
'trash-filled': TrashFilledIcon,
pin: PinIcon,
'pin-filled': PinFilledIcon,
unpin: UnpinIcon,
archive: ArchiveIcon,
unarchive: UnarchiveIcon,
@@ -130,11 +136,22 @@ export type IconType = keyof typeof ICONS;
type Props = {
type: IconType;
className?: string;
ariaLabel?: string;
};
export const Icon: FunctionalComponent<Props> = ({ type, className = '' }) => {
export const Icon: FunctionalComponent<Props> = ({
type,
className = '',
ariaLabel,
}) => {
const IconComponent = ICONS[type];
return <IconComponent className={`sn-icon ${className}`} />;
return (
<IconComponent
className={`sn-icon ${className}`}
role="img"
{...(ariaLabel ? { 'aria-label': ariaLabel } : {})}
/>
);
};
export const IconDirective = toDirective<Props>(Icon, {

View File

@@ -1,3 +1,4 @@
import { WebApplication } from '@/ui_models/application';
import { KeyboardKey } from '@/services/ioService';
import { AppState } from '@/ui_models/app_state';
import { DisplayOptions } from '@/ui_models/app_state/notes_view_state';
@@ -7,6 +8,7 @@ import { FunctionComponent } from 'preact';
import { NotesListItem } from './NotesListItem';
type Props = {
application: WebApplication;
appState: AppState;
notes: SNNote[];
selectedNotes: Record<string, SNNote>;
@@ -18,23 +20,30 @@ const FOCUSABLE_BUT_NOT_TABBABLE = -1;
const NOTES_LIST_SCROLL_THRESHOLD = 200;
export const NotesList: FunctionComponent<Props> = observer(
({ appState, notes, selectedNotes, displayOptions, paginate }) => {
({
application,
appState,
notes,
selectedNotes,
displayOptions,
paginate,
}) => {
const { selectPreviousNote, selectNextNote } = appState.notesView;
const { hideTags, hideDate, hideNotePreview, sortBy } = displayOptions;
const tagsStringForNote = (note: SNNote): string => {
const tagsForNote = (note: SNNote): string[] => {
if (hideTags) {
return '';
return [];
}
const selectedTag = appState.selectedTag;
if (!selectedTag) {
return '';
return [];
}
const tags = appState.getNoteTags(note);
if (!selectedTag.isSmartTag && tags.length === 1) {
return '';
return [];
}
return tags.map((tag) => `#${tag.title}`).join(' ');
return tags.map((tag) => tag.title);
};
const openNoteContextMenu = (posX: number, posY: number) => {
@@ -46,11 +55,9 @@ export const NotesList: FunctionComponent<Props> = observer(
appState.notes.setContextMenuOpen(true);
};
const onContextMenu = async (note: SNNote, posX: number, posY: number) => {
await appState.notes.selectNote(note.uuid, true);
if (selectedNotes[note.uuid]) {
openNoteContextMenu(posX, posY);
}
const onContextMenu = (note: SNNote, posX: number, posY: number) => {
appState.notes.selectNote(note.uuid, true);
openNoteContextMenu(posX, posY);
};
const onScroll = (e: Event) => {
@@ -84,9 +91,10 @@ export const NotesList: FunctionComponent<Props> = observer(
>
{notes.map((note) => (
<NotesListItem
application={application}
key={note.uuid}
note={note}
tags={tagsStringForNote(note)}
tags={tagsForNote(note)}
selected={!!selectedNotes[note.uuid]}
hideDate={hideDate}
hidePreview={hideNotePreview}

View File

@@ -1,13 +1,17 @@
import { getIconAndTintForEditor } from '@/preferences/panes/general-segments';
import { WebApplication } from '@/ui_models/application';
import {
CollectionSort,
sanitizeHtmlString,
SNNote,
} from '@standardnotes/snjs';
import { FunctionComponent } from 'preact';
import { Icon } from './Icon';
type Props = {
application: WebApplication;
note: SNNote;
tags: string;
tags: string[];
hideDate: boolean;
hidePreview: boolean;
hideTags: boolean;
@@ -24,30 +28,6 @@ type NoteFlag = {
const flagsForNote = (note: SNNote) => {
const flags = [] as NoteFlag[];
if (note.pinned) {
flags.push({
text: 'Pinned',
class: 'info',
});
}
if (note.archived) {
flags.push({
text: 'Archived',
class: 'warning',
});
}
if (note.locked) {
flags.push({
text: 'Editing Disabled',
class: 'neutral',
});
}
if (note.trashed) {
flags.push({
text: 'Deleted',
class: 'danger',
});
}
if (note.conflictOf) {
flags.push({
text: 'Conflicted Copy',
@@ -77,6 +57,7 @@ const flagsForNote = (note: SNNote) => {
};
export const NotesListItem: FunctionComponent<Props> = ({
application,
hideDate,
hidePreview,
hideTags,
@@ -89,6 +70,9 @@ export const NotesListItem: FunctionComponent<Props> = ({
}) => {
const flags = flagsForNote(note);
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt;
const editorForNote = application.componentManager.editorForNote(note);
const editorName = editorForNote?.name ?? 'Plain editor';
const [icon, tint] = getIconAndTintForEditor(editorForNote?.identifier);
return (
<div
@@ -97,52 +81,107 @@ export const NotesListItem: FunctionComponent<Props> = ({
onClick={onClick}
onContextMenu={onContextMenu}
>
{flags && flags.length > 0 ? (
<div className="note-flags flex flex-wrap">
{flags.map((flag) => (
<div className={`flag ${flag.class}`}>
<div className="label">{flag.text}</div>
</div>
))}
<div className="icon">
<Icon
ariaLabel={`Icon for ${editorName}`}
type={icon}
className={`color-accessory-tint-${tint}`}
/>
</div>
<div className="meta">
<div className="name">
<div>{note.title}</div>
<div className="flag-icons">
{note.locked && (
<span title="Editing Disabled">
<Icon
ariaLabel="Editing Disabled"
type="pencil-off"
className="sn-icon--small color-info"
/>
</span>
)}
{note.trashed && (
<span title="Trashed">
<Icon
ariaLabel="Trashed"
type="trash-filled"
className="sn-icon--small color-danger"
/>
</span>
)}
{note.archived && (
<span title="Archived">
<Icon
ariaLabel="Archived"
type="archive"
className="sn-icon--mid color-accessory-tint-3"
/>
</span>
)}
{note.pinned && (
<span title="Pinned">
<Icon
ariaLabel="Pinned"
type="pin-filled"
className="sn-icon--small color-info"
/>
</span>
)}
</div>
</div>
) : null}
<div className="name">{note.title}</div>
{!hidePreview && !note.hidePreview && !note.protected ? (
<div className="note-preview">
{note.preview_html ? (
<div
className="html-preview"
dangerouslySetInnerHTML={{
__html: sanitizeHtmlString(note.preview_html),
}}
></div>
) : null}
{!note.preview_html && note.preview_plain ? (
<div className="plain-preview">{note.preview_plain}</div>
) : null}
{!note.preview_html && !note.preview_plain ? (
<div className="default-preview">{note.text}</div>
) : null}
</div>
) : null}
{!hideDate || note.protected ? (
<div className="bottom-info faded">
{note.protected ? (
<span>Protected {hideDate ? '' : ' • '}</span>
) : null}
{!hideDate && showModifiedDate ? (
<span>Modified {note.updatedAtString || 'Now'}</span>
) : null}
{!hideDate && !showModifiedDate ? (
<span>{note.createdAtString || 'Now'}</span>
) : null}
</div>
) : null}
{!hideTags && (
<div className="tags-string">
<div className="faded">{tags}</div>
</div>
)}
{!hidePreview && !note.hidePreview && !note.protected && (
<div className="note-preview">
{note.preview_html && (
<div
className="html-preview"
dangerouslySetInnerHTML={{
__html: sanitizeHtmlString(note.preview_html),
}}
></div>
)}
{!note.preview_html && note.preview_plain && (
<div className="plain-preview">{note.preview_plain}</div>
)}
{!note.preview_html && !note.preview_plain && note.text && (
<div className="default-preview">{note.text}</div>
)}
</div>
)}
{!hideDate || note.protected ? (
<div className="bottom-info faded">
{note.protected && <span>Protected {hideDate ? '' : ' • '}</span>}
{!hideDate && showModifiedDate && (
<span>Modified {note.updatedAtString || 'Now'}</span>
)}
{!hideDate && !showModifiedDate && (
<span>{note.createdAtString || 'Now'}</span>
)}
</div>
) : null}
{!hideTags && tags.length ? (
<div className="tags-string">
{tags.map((tag) => (
<span className="tag color-foreground">
<Icon
type="hashtag"
className="sn-icon--small color-grey-1 mr-1"
/>
<span>{tag}</span>
</span>
))}
</div>
) : null}
{flags.length ? (
<div className="note-flags flex flex-wrap">
{flags.map((flag) => (
<div className={`flag ${flag.class}`}>
<div className="label">{flag.text}</div>
</div>
))}
</div>
) : null}
</div>
</div>
);
};

View File

@@ -230,6 +230,7 @@ const NotesView: FunctionComponent<Props> = observer(
<NotesList
notes={renderedNotes}
selectedNotes={selectedNotes}
application={application}
appState={appState}
displayOptions={displayOptions}
paginate={paginate}