refactor: move tags to react (#753)

* refactor: move Tags list to react

* refactor: extract TagsListItem and simplify hooks

* refactor: remove comment & dead code

* fix: mobx warnings & safari bug

* fix: text select on non-safari

* fix: remove unecessary comments

* style: apply prettier format

* style: apply formatting on tags_view

* refactor: remove the angular tags rendering

* feat: add back the "select previous tag" behavior

* style: simplify code and avoid important

* style: remove note on state
This commit is contained in:
Laurent Senta
2021-11-29 17:33:00 +01:00
committed by GitHub
parent 6f3a749e52
commit 4d8ba3320a
7 changed files with 480 additions and 286 deletions

View File

@@ -0,0 +1,136 @@
import { confirmDialog } from '@/services/alertService';
import { STRING_DELETE_TAG } from '@/strings';
import { WebApplication } from '@/ui_models/application';
import { AppState } from '@/ui_models/app_state';
import { SNTag, TagMutator } from '@standardnotes/snjs';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useCallback } from 'preact/hooks';
import { TagsListItem } from './TagsListItem';
import { toDirective } from './utils';
type Props = {
application: WebApplication;
appState: AppState;
};
const tagsWithOptionalTemplate = (
template: SNTag | undefined,
tags: SNTag[]
): SNTag[] => {
if (!template) {
return tags;
}
return [template, ...tags];
};
export const TagsList: FunctionComponent<Props> = observer(
({ application, appState }) => {
const templateTag = appState.templateTag;
const tags = appState.tags.tags;
const allTags = tagsWithOptionalTemplate(templateTag, tags);
const selectTag = useCallback(
(tag: SNTag) => {
appState.setSelectedTag(tag);
},
[appState]
);
const saveTag = useCallback(
async (tag: SNTag, newTitle: string) => {
const templateTag = appState.templateTag;
const hasEmptyTitle = newTitle.length === 0;
const hasNotChangedTitle = newTitle === tag.title;
const isTemplateChange = templateTag && tag.uuid === templateTag.uuid;
const hasDuplicatedTitle = !!application.findTagByTitle(newTitle);
runInAction(() => {
appState.templateTag = undefined;
appState.editingTag = undefined;
});
if (hasEmptyTitle || hasNotChangedTitle) {
if (isTemplateChange) {
appState.undoCreateNewTag();
}
return;
}
if (hasDuplicatedTitle) {
if (isTemplateChange) {
appState.undoCreateNewTag();
}
application.alertService?.alert(
'A tag with this name already exists.'
);
return;
}
if (isTemplateChange) {
const insertedTag = await application.insertItem(templateTag);
const changedTag = await application.changeItem<TagMutator>(
insertedTag.uuid,
(m) => {
m.title = newTitle;
}
);
selectTag(changedTag as SNTag);
await application.saveItem(insertedTag.uuid);
} else {
await application.changeAndSaveItem<TagMutator>(
tag.uuid,
(mutator) => {
mutator.title = newTitle;
}
);
}
},
[appState, application, selectTag]
);
const removeTag = useCallback(
async (tag: SNTag) => {
if (
await confirmDialog({
text: STRING_DELETE_TAG,
confirmButtonStyle: 'danger',
})
) {
appState.removeTag(tag);
}
},
[appState]
);
return (
<>
{allTags.length === 0 ? (
<div className="no-tags-placeholder">
No tags. Create one using the add button above.
</div>
) : (
<>
{allTags.map((tag) => {
return (
<TagsListItem
key={tag.uuid}
tag={tag}
selectTag={selectTag}
saveTag={saveTag}
removeTag={removeTag}
appState={appState}
/>
);
})}
</>
)}
</>
);
}
);
export const TagsListDirective = toDirective<Props>(TagsList);

View File

@@ -0,0 +1,134 @@
import { SNTag } from '@standardnotes/snjs';
import { runInAction } from 'mobx';
import { observer } from 'mobx-react-lite';
import { FunctionComponent, JSX } from 'preact';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
type Props = {
tag: SNTag;
selectTag: (tag: SNTag) => void;
removeTag: (tag: SNTag) => void;
saveTag: (tag: SNTag, newTitle: string) => void;
appState: TagsListState;
};
export type TagsListState = {
readonly selectedTag: SNTag | undefined;
editingTag: SNTag | undefined;
};
export const TagsListItem: FunctionComponent<Props> = observer(
({ tag, selectTag, saveTag, removeTag, appState }) => {
const [title, setTitle] = useState(tag.title || '');
const inputRef = useRef<HTMLInputElement>(null);
const isSelected = appState.selectedTag === tag;
const isEditing = appState.editingTag === tag;
const noteCounts = tag.noteCount;
useEffect(() => {
setTitle(tag.title || '');
}, [setTitle, tag]);
const selectCurrentTag = useCallback(() => {
if (isEditing || isSelected) {
return;
}
selectTag(tag);
}, [isSelected, isEditing, selectTag, tag]);
const onBlur = useCallback(() => {
saveTag(tag, title);
}, [tag, saveTag, title]);
const onInput = useCallback(
(e: JSX.TargetedEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value;
setTitle(value);
},
[setTitle]
);
const onKeyUp = useCallback(
(e: KeyboardEvent) => {
if (e.code === 'Enter') {
inputRef.current?.blur();
e.preventDefault();
}
},
[inputRef]
);
useEffect(() => {
if (isEditing) {
inputRef.current?.focus();
}
}, [inputRef, isEditing]);
const onClickRename = useCallback(() => {
runInAction(() => {
appState.editingTag = tag;
});
}, [appState, tag]);
const onClickSave = useCallback(() => {
inputRef.current?.blur();
}, [inputRef]);
const onClickDelete = useCallback(() => {
removeTag(tag);
}, [removeTag, tag]);
return (
<div
className={`tag ${isSelected ? 'selected' : ''}`}
onClick={selectCurrentTag}
>
{!tag.errorDecrypting ? (
<div className="tag-info">
<div className="tag-icon">#</div>
<input
className={`title ${isEditing ? 'editing' : ''}`}
id={`react-tag-${tag.uuid}`}
onBlur={onBlur}
onInput={onInput}
value={title}
onKeyUp={onKeyUp}
spellCheck={false}
ref={inputRef}
/>
<div className="count">{noteCounts}</div>
</div>
) : null}
{tag.conflictOf && (
<div className="danger small-text font-bold">
Conflicted Copy {tag.conflictOf}
</div>
)}
{tag.errorDecrypting && !tag.waitingForKey && (
<div className="danger small-text font-bold">Missing Keys</div>
)}
{tag.errorDecrypting && tag.waitingForKey && (
<div className="info small-text font-bold">Waiting For Keys</div>
)}
{isSelected && (
<div className="menu">
{!isEditing && (
<a className="item" onClick={onClickRename}>
Rename
</a>
)}
{isEditing && (
<a className="item" onClick={onClickSave}>
Save
</a>
)}
<a className="item" onClick={onClickDelete}>
Delete
</a>
</div>
)}
</div>
);
}
);