Skip to content

Commit

Permalink
feat: allows duplicate names in tags folder & smart tags (#792)
Browse files Browse the repository at this point in the history
* feat: tag rendering & validation uses hierarchy

* feat: add prefix to autocomplete
  • Loading branch information
laurentsenta authored Jan 4, 2022
1 parent c3772e0 commit a165fa9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 6 deletions.
8 changes: 6 additions & 2 deletions app/assets/javascripts/components/AutocompleteTagResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const AutocompleteTagResult = observer(

const tagResultRef = useRef<HTMLButtonElement>(null);

const title = tagResult.title;
const prefixTitle = appState.noteTags.getPrefixTitle(tagResult);

const onTagOptionClick = async (tag: SNTag) => {
await appState.noteTags.addTagToActiveNote(tag);
appState.noteTags.clearAutocompleteSearch();
Expand Down Expand Up @@ -86,9 +89,10 @@ export const AutocompleteTagResult = observer(
>
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
{prefixTitle && <span className="grey-2">{prefixTitle}</span>}
{autocompleteSearchQuery === ''
? tagResult.title
: tagResult.title
? title
: title
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
.map((substring, index) => (
<span
Expand Down
12 changes: 10 additions & 2 deletions app/assets/javascripts/components/NoteTag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ type Props = {
};

export const NoteTag = observer(({ appState, tag }: Props) => {
const { autocompleteInputFocused, focusedTagUuid, tags } = appState.noteTags;
const noteTags = appState.noteTags;

const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags;

const [showDeleteButton, setShowDeleteButton] = useState(false);
const [tagClicked, setTagClicked] = useState(false);
const deleteTagRef = useRef<HTMLButtonElement>(null);

const tagRef = useRef<HTMLButtonElement>(null);

const title = tag.title;
const prefixTitle = noteTags.getPrefixTitle(tag);
const longTitle = noteTags.getLongTitle(tag);

const deleteTag = () => {
appState.noteTags.focusPreviousTag(tag);
appState.noteTags.removeTagFromActiveNote(tag);
Expand Down Expand Up @@ -97,10 +103,12 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
onFocus={onFocus}
onBlur={onBlur}
tabIndex={getTabIndex()}
title={longTitle}
>
<Icon type="hashtag" className="sn-icon--small color-info mr-1" />
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis max-w-290px">
{tag.title}
{prefixTitle && <span className="color-grey-1">{prefixTitle}</span>}
{title}
</span>
{showDeleteButton && (
<button
Expand Down
39 changes: 38 additions & 1 deletion app/assets/javascripts/ui_models/app_state/note_tags_state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SNNote, ContentType, SNTag, UuidString } from '@standardnotes/snjs';
import { ContentType, SNNote, SNTag, UuidString } from '@standardnotes/snjs';
import { action, computed, makeObservable, observable } from 'mobx';
import { WebApplication } from '../application';
import { AppState } from './app_state';
Expand Down Expand Up @@ -194,4 +194,41 @@ export class NoteTagsState {
this.reloadTags();
}
}

getSortedTagsForNote(note: SNNote): SNTag[] {
const tags = this.application.getSortedTagsForNote(note);

const sortFunction = (tagA: SNTag, tagB: SNTag): number => {
const a = this.getLongTitle(tagA);
const b = this.getLongTitle(tagB);

if (a < b) {
return -1;
}
if (b > a) {
return 1;
}
return 0;
};

return tags.sort(sortFunction);
}

getPrefixTitle(tag: SNTag): string | undefined {
const hierarchy = this.application.getTagParentChain(tag);

if (hierarchy.length === 0) {
return undefined;
}

const prefixTitle = hierarchy.map((tag) => tag.title).join('/');
return `${prefixTitle}/`;
}

getLongTitle(tag: SNTag): string {
const hierarchy = this.application.getTagParentChain(tag);
const tags = [...hierarchy, tag];
const longTitle = tags.map((tag) => tag.title).join('/');
return longTitle;
}
}
64 changes: 63 additions & 1 deletion app/assets/javascripts/ui_models/app_state/tags_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ComponentAction,
ContentType,
MessageData,
SNApplication,
SNSmartTag,
SNTag,
TagMutator,
Expand All @@ -22,6 +23,48 @@ import { FeaturesState, SMART_TAGS_FEATURE_NAME } from './features_state';

type AnyTag = SNTag | SNSmartTag;

const rootTags = (application: SNApplication): SNTag[] => {
const hasNoParent = (tag: SNTag) => !application.getTagParent(tag);

const allTags = application.getDisplayableItems(ContentType.Tag) as SNTag[];
const rootTags = allTags.filter(hasNoParent);

return rootTags;
};

const tagSiblings = (application: SNApplication, tag: SNTag): SNTag[] => {
const withoutCurrentTag = (tags: SNTag[]) =>
tags.filter((other) => other.uuid !== tag.uuid);

const isTemplateTag = application.isTemplateItem(tag);
const parentTag = !isTemplateTag && application.getTagParent(tag);

if (parentTag) {
const siblingsAndTag = application.getTagChildren(parentTag);
return withoutCurrentTag(siblingsAndTag);
}

return withoutCurrentTag(rootTags(application));
};

const isValidFutureSiblings = (
application: SNApplication,
futureSiblings: SNTag[],
tag: SNTag
): boolean => {
const siblingWithSameName = futureSiblings.find(
(otherTag) => otherTag.title === tag.title
);

if (siblingWithSameName) {
application.alertService?.alert(
`A tag with the name ${tag.title} already exists at this destination. Please rename this tag before moving and try again.`
);
return false;
}
return true;
};

export class TagsState {
tags: SNTag[] = [];
smartTags: SNSmartTag[] = [];
Expand Down Expand Up @@ -144,12 +187,27 @@ export class TagsState {
): Promise<void> {
const tag = this.application.findItem(tagUuid) as SNTag;

const currentParent = this.application.getTagParent(tag);
const currentParentUuid = currentParent?.parentId;

if (currentParentUuid === parentUuid) {
return;
}

const parent =
parentUuid && (this.application.findItem(parentUuid) as SNTag);

if (!parent) {
const futureSiblings = rootTags(this.application);
if (!isValidFutureSiblings(this.application, futureSiblings, tag)) {
return;
}
await this.application.unsetTagParent(tag);
} else {
const futureSiblings = this.application.getTagChildren(parent);
if (!isValidFutureSiblings(this.application, futureSiblings, tag)) {
return;
}
await this.application.setTagParent(parent, tag);
}

Expand Down Expand Up @@ -249,7 +307,11 @@ export class TagsState {
const hasEmptyTitle = newTitle.length === 0;
const hasNotChangedTitle = newTitle === tag.title;
const isTemplateChange = this.application.isTemplateItem(tag);
const hasDuplicatedTitle = !!this.application.findTagByTitle(newTitle);

const siblings = tagSiblings(this.application, tag);
const hasDuplicatedTitle = siblings.some(
(other) => other.title.toLowerCase() === newTitle.toLowerCase()
);

runInAction(() => {
this.editing_ = undefined;
Expand Down

0 comments on commit a165fa9

Please sign in to comment.