Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add descriptions to tags and display tag cards on hover #2708

Merged
1 change: 1 addition & 0 deletions graphql/documents/data/tag.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
fragment TagData on Tag {
id
name
description
aliases
ignore_auto_tag
image_path
Expand Down
3 changes: 3 additions & 0 deletions graphql/schema/types/tag.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type Tag {
id: ID!
name: String!
description: String
aliases: [String!]!
ignore_auto_tag: Boolean!
created_at: Time!
Expand All @@ -19,6 +20,7 @@ type Tag {

input TagCreateInput {
name: String!
description: String
aliases: [String!]
ignore_auto_tag: Boolean

Expand All @@ -32,6 +34,7 @@ input TagCreateInput {
input TagUpdateInput {
id: ID!
name: String
description: String
aliases: [String!]
ignore_auto_tag: Boolean

Expand Down
7 changes: 7 additions & 0 deletions internal/api/resolver_model_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import (
"github.com/stashapp/stash/pkg/models"
)

func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string, error) {
if obj.Description.Valid {
return &obj.Description.String, nil
}
return nil, nil
}

func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)
Expand Down
7 changes: 7 additions & 0 deletions internal/api/resolver_mutation_tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
Expand Down Expand Up @@ -34,6 +35,10 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}

if input.Description != nil {
newTag.Description = sql.NullString{String: *input.Description, Valid: true}
}

if input.IgnoreAutoTag != nil {
newTag.IgnoreAutoTag = *input.IgnoreAutoTag
}
Expand Down Expand Up @@ -195,6 +200,8 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
updatedTag.Name = input.Name
}

updatedTag.Description = translator.nullString(input.Description, "description")

t, err = qb.Update(ctx, updatedTag)
if err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions pkg/models/jsonschema/tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

type Tag struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Image string `json:"image,omitempty"`
Parents []string `json:"parents,omitempty"`
Expand Down
7 changes: 6 additions & 1 deletion pkg/models/model_tag.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package models

import "time"
import (
"database/sql"
"time"
)

type Tag struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"` // TODO make schema not null
Description sql.NullString `db:"description" json:"description"`
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Expand All @@ -13,6 +17,7 @@ type Tag struct {
type TagPartial struct {
ID int `db:"id" json:"id"`
Name *string `db:"name" json:"name"` // TODO make schema not null
Description *sql.NullString `db:"description" json:"description"`
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Expand Down
2 changes: 1 addition & 1 deletion pkg/sqlite/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
)

var appSchemaVersion uint = 35
var appSchemaVersion uint = 36

//go:embed migrations/*.sql
var migrationsBox embed.FS
Expand Down
1 change: 1 addition & 0 deletions pkg/sqlite/migrations/36_tags_description.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `tags` ADD COLUMN `description` text;
1 change: 1 addition & 0 deletions pkg/tag/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type FinderAliasImageGetter interface {
func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag) (*jsonschema.Tag, error) {
newTagJSON := jsonschema.Tag{
Name: tag.Name,
Description: tag.Description.String,
IgnoreAutoTag: tag.IgnoreAutoTag,
CreatedAt: json.JSONTime{Time: tag.CreatedAt.Timestamp},
UpdatedAt: json.JSONTime{Time: tag.UpdatedAt.Timestamp},
Expand Down
15 changes: 12 additions & 3 deletions pkg/tag/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tag

import (
"context"
"database/sql"
"errors"

"github.com/stashapp/stash/pkg/models"
Expand All @@ -23,7 +24,10 @@ const (
errParentsID = 6
)

const tagName = "testTag"
const (
tagName = "testTag"
description = "description"
)

var (
autoTagIgnored = true
Expand All @@ -33,8 +37,12 @@ var (

func createTag(id int) models.Tag {
return models.Tag{
ID: id,
Name: tagName,
ID: id,
Name: tagName,
Description: sql.NullString{
String: description,
Valid: true,
},
IgnoreAutoTag: autoTagIgnored,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
Expand All @@ -48,6 +56,7 @@ func createTag(id int) models.Tag {
func createJSONTag(aliases []string, image string, parents []string) *jsonschema.Tag {
return &jsonschema.Tag{
Name: tagName,
Description: description,
Aliases: aliases,
IgnoreAutoTag: autoTagIgnored,
CreatedAt: json.JSONTime{
Expand Down
2 changes: 2 additions & 0 deletions pkg/tag/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tag

import (
"context"
"database/sql"
"fmt"

"github.com/stashapp/stash/pkg/models"
Expand Down Expand Up @@ -42,6 +43,7 @@ type Importer struct {
func (i *Importer) PreImport(ctx context.Context) error {
i.tag = models.Tag{
Name: i.Input.Name,
Description: sql.NullString{String: i.Input.Description, Valid: true},
IgnoreAutoTag: i.Input.IgnoreAutoTag,
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
Expand Down
1 change: 1 addition & 0 deletions pkg/tag/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestImporterPreImport(t *testing.T) {
i := Importer{
Input: jsonschema.Tag{
Name: tagName,
Description: description,
Image: invalidImage,
IgnoreAutoTag: autoTagIgnored,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ export const SettingsInterfacePanel: React.FC = () => {
/>
</SettingSection>
<SettingSection headingID="config.ui.tag_panel.heading">
<BooleanSetting
id="show-tag-card-on-hover"
headingID="config.ui.show_tag_card_on_hover.heading"
subHeadingID="config.ui.show_tag_card_on_hover.description"
checked={ui.showTagCardOnHover ?? true}
onChange={(v) => saveUI({ showTagCardOnHover: v })}
/>
<BooleanSetting
id="show-child-tagged-content"
headingID="config.ui.tag_panel.options.show_child_tagged_content.heading"
Expand Down
7 changes: 6 additions & 1 deletion ui/v2.5/src/components/Shared/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import { TagPopover } from "../Tags/TagPopover";

export type ValidTypes =
| GQL.SlimPerformerDataFragment
Expand Down Expand Up @@ -659,7 +660,11 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
};
}

return <reactSelectComponents.Option {...thisOptionProps} />;
return (
<TagPopover id={optionProps.data.value}>
<reactSelectComponents.Option {...thisOptionProps} />
</TagPopover>
);
};

const filterOption = (option: Option, rawInput: string): boolean => {
Expand Down
7 changes: 6 additions & 1 deletion ui/v2.5/src/components/Shared/TagLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import TextUtils from "src/utils/text";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import * as GQL from "src/core/generated-graphql";
import { TagPopover } from "../Tags/TagPopover";

interface IFile {
path: string;
Expand All @@ -37,6 +38,7 @@ interface IProps {
}

export const TagLink: React.FC<IProps> = (props: IProps) => {
let id: string = "";
let link: string = "#";
let title: string = "";
if (props.tag) {
Expand All @@ -55,6 +57,7 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
link = NavUtils.makeTagImagesUrl(props.tag);
break;
}
id = props.tag.id || "";
title = props.tag.name || "";
} else if (props.performer) {
link = NavUtils.makePerformerScenesUrl(props.performer);
Expand All @@ -76,7 +79,9 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
}
return (
<Badge className={cx("tag-item", props.className)} variant="secondary">
<Link to={link}>{title}</Link>
<TagPopover id={id}>
<Link to={link}>{title}</Link>
</TagPopover>
</Badge>
);
};
15 changes: 14 additions & 1 deletion ui/v2.5/src/components/Tags/TagCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { NavUtils } from "src/utils";
import { FormattedMessage } from "react-intl";
import { Icon } from "../Shared";
import { Icon, TruncatedText } from "../Shared";
import { GridCard } from "../Shared/GridCard";
import { PopoverCountButton } from "../Shared/PopoverCountButton";
import { faMapMarkerAlt, faUser } from "@fortawesome/free-solid-svg-icons";
Expand All @@ -24,6 +24,18 @@ export const TagCard: React.FC<IProps> = ({
selected,
onSelectedChanged,
}) => {
function maybeRenderDescription() {
if (tag.description) {
return (
<TruncatedText
className="tag-description"
text={tag.description}
lineCount={3}
/>
);
}
}

function maybeRenderParents() {
if (tag.parents.length === 1) {
const parent = tag.parents[0];
Expand Down Expand Up @@ -181,6 +193,7 @@ export const TagCard: React.FC<IProps> = ({
}
details={
<>
{maybeRenderDescription()}
{maybeRenderParents()}
{maybeRenderChildren()}
{maybeRenderPopoverButtonGroup()}
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/src/components/Tags/TagDetails/Tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
renderImage()
)}
<h2>{tag.name}</h2>
<p>{tag.description}</p>
</div>
{!isEditing ? (
<>
Expand Down
16 changes: 16 additions & 0 deletions ui/v2.5/src/components/Tags/TagDetails/TagEditPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({

const schema = yup.object({
name: yup.string().required(),
description: yup.string().optional().nullable(),
aliases: yup
.array(yup.string().required())
.optional()
Expand All @@ -65,6 +66,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({

const initialValues = {
name: tag?.name,
description: tag?.description,
aliases: tag?.aliases,
parent_ids: (tag?.parents ?? []).map((t) => t.id),
child_ids: (tag?.children ?? []).map((t) => t.id),
Expand Down Expand Up @@ -167,6 +169,20 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
</Col>
</Form.Group>

<Form.Group controlId="description" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "description" }),
})}
<Col xs={9}>
<Form.Control
as="textarea"
className="text-input"
placeholder={intl.formatMessage({ id: "description" })}
{...formik.getFieldProps("description")}
/>
</Col>
</Form.Group>

<Form.Group controlId="parent_tags" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "parent_tags" }),
Expand Down
Loading