Skip to content

Commit

Permalink
Add searchTags GQL query and alias matching to tag searches (#413)
Browse files Browse the repository at this point in the history
* Add `searchTags` GQL query

* implement `names` for `queryTags`

* Update TagList to search aliases (non-FTS)

* Use `searchTag` in `TagSelect`

* Sort tag search results by name

* Refactor TagSelect results

- Add description
- Aliases as title when hovering over result
- Make the menu larger on SceneForm, to accomodate the additional text
- Don't allow deleted tags by default
  (can be achieved by searching a deleted tag's ID)

* Restore font-size CSS

* remove 'else if' between name/names filters
  • Loading branch information
peolic authored Aug 8, 2022
1 parent 50613d2 commit e431a6f
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 20 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/list/TagList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const TagList: FC<TagListProps> = ({ tagFilter, showCategoryLink = false }) => {
const { page, setPage } = usePagination();
const { loading, data } = useTags({
input: {
name: name.trim(),
names: name.trim(),
page,
per_page: PER_PAGE,
sort: TagSortEnum.NAME,
Expand Down
48 changes: 29 additions & 19 deletions frontend/src/components/tagSelect/TagSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { OnChangeValue, MenuPlacement } from "react-select";
import { useApolloClient } from "@apollo/client";
import debounce from "p-debounce";

import TagsQuery from "src/graphql/queries/Tags.gql";
import SearchTagsQuery from "src/graphql/queries/SearchTags.gql";

import {
Tags_queryTags_tags as Tag,
Tags,
TagsVariables,
} from "src/graphql/definitions/Tags";
import { SortDirectionEnum, TagSortEnum } from "src/graphql";
SearchTags_searchTag as Tag,
SearchTags,
SearchTagsVariables,
} from "src/graphql/definitions/SearchTags";
import { TagLink } from "src/components/fragments";
import { tagHref } from "src/utils/route";
import { compareByName } from "src/utils";
Expand All @@ -28,12 +27,13 @@ interface TagSelectProps {
message?: string;
excludeTags?: string[];
menuPlacement?: MenuPlacement;
allowDeleted?: boolean;
}

interface SearchResult {
value: Tag;
label: string;
subLabel: string;
sublabel: string;
}

const CLASSNAME = "TagSelect";
Expand All @@ -47,6 +47,7 @@ const TagSelect: FC<TagSelectProps> = ({
message = "Add tag:",
excludeTags = [],
menuPlacement = "auto",
allowDeleted = false,
}) => {
const client = useApolloClient();
const [tags, setTags] = useState(initialTags);
Expand Down Expand Up @@ -79,30 +80,38 @@ const TagSelect: FC<TagSelectProps> = ({
));

const handleSearch = async (term: string) => {
const { data } = await client.query<Tags, TagsVariables>({
query: TagsQuery,
const { data } = await client.query<SearchTags, SearchTagsVariables>({
query: SearchTagsQuery,
variables: {
input: {
page: 1,
per_page: 25,
name: term,
sort: TagSortEnum.NAME,
direction: SortDirectionEnum.ASC,
},
term,
limit: 25,
},
});

return data.queryTags.tags
.filter((tag) => !excluded.includes(tag.id))
return data.searchTag
.filter(
(tag) => !excluded.includes(tag.id) && (allowDeleted || !tag.deleted)
)
.map((tag) => ({
label: tag.name,
value: tag,
subLabel: tag.description ?? "",
sublabel: tag.description ?? "",
}));
};

const debouncedLoadOptions = debounce(handleSearch, 400);

const formatOptionLabel = ({ label, sublabel, value }: SearchResult) => {
return (
<div title={value.aliases.map((a) => `\u{2022} ${a}`).join("\n")}>
<div className={`${CLASSNAME_SELECT}-value`}>
{value.deleted ? <del>{label}</del> : label}
</div>
<div className={`${CLASSNAME_SELECT}-subvalue`}>{sublabel}</div>
</div>
);
};

return (
<div className={CLASSNAME}>
<div className={CLASSNAME_LIST}>{tagList}</div>
Expand All @@ -119,6 +128,7 @@ const TagSelect: FC<TagSelectProps> = ({
}
menuPlacement={menuPlacement}
controlShouldRenderValue={false}
formatOptionLabel={formatOptionLabel}
/>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/components/tagSelect/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,15 @@
display: inline-block;
margin-left: auto;
width: 25rem;

&-value {
font-size: 14px;
font-weight: 500;
}

&-subvalue {
font-size: 12px;
color: $text-muted;
}
}
}
26 changes: 26 additions & 0 deletions frontend/src/graphql/definitions/SearchTags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.

// ====================================================
// GraphQL query operation: SearchTags
// ====================================================

export interface SearchTags_searchTag {
__typename: "Tag";
deleted: boolean;
id: string;
name: string;
description: string | null;
aliases: string[];
}

export interface SearchTags {
searchTag: SearchTags_searchTag[];
}

export interface SearchTagsVariables {
term: string;
limit?: number | null;
}
9 changes: 9 additions & 0 deletions frontend/src/graphql/queries/SearchTags.gql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query SearchTags($term: String!, $limit: Int = 5) {
searchTag(term: $term, limit: $limit) {
deleted
id
name
description
aliases
}
}
7 changes: 7 additions & 0 deletions frontend/src/graphql/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
SearchPerformers,
SearchPerformersVariables,
} from "../definitions/SearchPerformers";
import { SearchTags, SearchTagsVariables } from "../definitions/SearchTags";
import { Studio, StudioVariables } from "../definitions/Studio";
import { Studios, StudiosVariables } from "../definitions/Studios";
import { Tag, TagVariables } from "../definitions/Tag";
Expand Down Expand Up @@ -64,6 +65,7 @@ import ScenesQuery from "./Scenes.gql";
import ScenesWithoutCountQuery from "./ScenesWithoutCount.gql";
import SearchAllQuery from "./SearchAll.gql";
import SearchPerformersQuery from "./SearchPerformers.gql";
import SearchTagsQuery from "./SearchTags.gql";
import StudioQuery from "./Studio.gql";
import StudiosQuery from "./Studios.gql";
import TagQuery from "./Tag.gql";
Expand Down Expand Up @@ -165,6 +167,11 @@ export const useLazySearchPerformers = (
options?: LazyQueryHookOptions<SearchPerformers, SearchPerformersVariables>
) => useLazyQuery(SearchPerformersQuery, options);

export const useSearchTags = (variables: SearchTagsVariables) =>
useQuery<SearchTags, SearchTagsVariables>(SearchTagsQuery, {
variables,
});

export const useStudio = (variables: StudioVariables, skip = false) =>
useQuery<Studio, StudioVariables>(StudioQuery, {
variables,
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/pages/scenes/sceneForm/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,12 @@
.studio-select .invalid-feedback {
display: block;
}

.TagSelect .react-select__menu {
// min-width should match .TagSelect-select
min-width: 25rem;
width: max-content;
max-width: 45rem;
right: 0;
}
}
1 change: 1 addition & 0 deletions graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Query {
### Full text search ###
searchPerformer(term: String!, limit: Int): [Performer!]! @hasRole(role: READ)
searchScene(term: String!, limit: Int): [Scene!]! @hasRole(role: READ)
searchTag(term: String!, limit: Int): [Tag!]! @hasRole(role: READ)

### Drafts ###
findDraft(id: ID!): Draft @hasRole(role: READ)
Expand Down
23 changes: 23 additions & 0 deletions pkg/api/resolver_query_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,26 @@ func (r *queryResolver) SearchScene(ctx context.Context, term string, limit *int

return qb.SearchScenes(trimmedQuery, searchLimit)
}

func (r *queryResolver) SearchTag(ctx context.Context, term string, limit *int) ([]*models.Tag, error) {
fac := r.getRepoFactory(ctx)
qb := fac.Tag()

trimmedQuery := strings.TrimSpace(term)
tagID, err := uuid.FromString(trimmedQuery)
if err == nil {
var tags []*models.Tag
tag, err := qb.Find(tagID)
if tag != nil {
tags = append(tags, tag)
}
return tags, err
}

searchLimit := 10
if limit != nil {
searchLimit = *limit
}

return qb.SearchTags(trimmedQuery, searchLimit)
}
59 changes: 59 additions & 0 deletions pkg/api/search_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,55 @@ func (s *searchTestRunner) testSearchSceneByID() {
s.fieldMismatch(createdScene.ID, scenes[0].ID, "ID")
}
}

func (s *searchTestRunner) testSearchTagByTerm() {
createdTag, err := s.createTestTag(nil)
if err != nil {
return
}

tags, err := s.resolver.Query().SearchTag(s.ctx, createdTag.Name, nil)
if err != nil {
s.t.Errorf("Error finding tag: %s", err.Error())
return
}

// ensure returned tag is not nil
if len(tags) == 0 {
s.t.Error("Did not find tag by name search")
return
}

// ensure values were set
if createdTag.UUID() != tags[0].ID {
s.fieldMismatch(createdTag.ID, tags[0].ID, "ID")
}
}

func (s *searchTestRunner) testSearchTagByID() {
createdTag, err := s.createTestTag(nil)
if err != nil {
return
}

tags, err := s.resolver.Query().SearchTag(s.ctx, " "+createdTag.ID, nil)
if err != nil {
s.t.Errorf("Error finding tag: %s", err.Error())
return
}

// ensure returned tag is not nil
if len(tags) == 0 {
s.t.Error("Did not find tag by name search")
return
}

// ensure values were set
if createdTag.UUID() != tags[0].ID {
s.fieldMismatch(createdTag.ID, tags[0].ID, "ID")
}
}

func TestSearchPerformerByTerm(t *testing.T) {
pt := createSearchTestRunner(t)
pt.testSearchPerformerByTerm()
Expand All @@ -146,3 +195,13 @@ func TestSearchSceneByID(t *testing.T) {
pt := createSearchTestRunner(t)
pt.testSearchSceneByID()
}

func TestSearchTagByTerm(t *testing.T) {
pt := createSearchTestRunner(t)
pt.testSearchTagByTerm()
}

func TestSearchTagByID(t *testing.T) {
pt := createSearchTestRunner(t)
pt.testSearchTagByID()
}
Loading

0 comments on commit e431a6f

Please sign in to comment.