diff options
| author | Jake Mannens <jake@asger.xyz> | 2023-09-05 01:01:24 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2023-09-05 01:01:24 +1000 |
| commit | acb74202f5391272c2e1823dfe04a044c7f7a9a7 (patch) | |
| tree | 09dc032ba526306d8916035bf92336c095246741 | |
| parent | 1b080819b26e4e84e5e9c88445806e43698b6757 (diff) | |
Tag names and aliases are now verified to be unique
| -rw-r--r-- | Exception.cs | 34 | ||||
| -rw-r--r-- | IDialog.cs | 8 | ||||
| -rw-r--r-- | Pages/Component/AboutDialog.razor | 1 | ||||
| -rw-r--r-- | Pages/Component/Dialog.razor | 4 | ||||
| -rw-r--r-- | Pages/Component/TagEditDialog.razor | 96 | ||||
| -rw-r--r-- | Pages/Component/TagEditDialog.razor.css | 11 | ||||
| -rw-r--r-- | Pages/Component/TagSelectDialog.razor | 9 | ||||
| -rw-r--r-- | Pages/TagDefinitions.razor | 89 | ||||
| -rw-r--r-- | Pages/TagDefinitions.razor.css | 3 | ||||
| -rw-r--r-- | Services/TagService.cs | 35 | ||||
| -rw-r--r-- | Todo.md | 4 | ||||
| -rw-r--r-- | wwwroot/styles/global.css | 1 |
12 files changed, 206 insertions, 89 deletions
diff --git a/Exception.cs b/Exception.cs index 2005b1a..1e070eb 100644 --- a/Exception.cs +++ b/Exception.cs @@ -10,14 +10,44 @@ public class HBException : Exception { } public class ObjectNotFoundException : HBException { - public Guid Guid { get; set; } + public Guid Guid { get; private init; } public ObjectNotFoundException(Guid guid) : base($"Object not found: {guid}") {} } +public class TagException : HBException { + public TagDefinition? TagDefinition { get; private init; } + + public TagException(string message) : base(message) {} + public TagException(string message, TagDefinition tagDefinition) + : base(message) => + TagDefinition = tagDefinition; +} + +public class TagDuplicateException : TagException { + public bool NameExists { get; private init; } + public bool AliasExists { get; private init; } + + public TagDuplicateException(bool nameExists, bool aliasExists) + : base(GenerateMessage(nameExists, aliasExists)) { + + NameExists = nameExists; + AliasExists = aliasExists; + } + + private static string GenerateMessage(bool nameExists, bool aliasExists) { + if(nameExists && aliasExists) + return $"Both tag name and alias already exist!"; + else if(nameExists) + return $"Tag name already exists!"; + else + return $"Tag alias already exists"; + } +} + public class MediaException : HBException { - public Media? Media { get; set; } + public Media? Media { get; private init; } public MediaException(string message) : base(message) {} public MediaException(string message, Media media) : base(message) => diff --git a/IDialog.cs b/IDialog.cs new file mode 100644 index 0000000..41e86a8 --- /dev/null +++ b/IDialog.cs @@ -0,0 +1,8 @@ +namespace HyperBooru; + +public interface IDialog { + public bool Visible { get; set; } + + public void Show(); + public void Hide(); +} diff --git a/Pages/Component/AboutDialog.razor b/Pages/Component/AboutDialog.razor index 9823761..9ffbad4 100644 --- a/Pages/Component/AboutDialog.razor +++ b/Pages/Component/AboutDialog.razor @@ -1,6 +1,7 @@ @using System.Reflection @using Microsoft.AspNetCore.Hosting @inject IHostingEnvironment hostingEnvironment +@implements IDialog <Dialog @ref=dialog> <p id="title">@Title</p> diff --git a/Pages/Component/Dialog.razor b/Pages/Component/Dialog.razor index ded2d2d..f479368 100644 --- a/Pages/Component/Dialog.razor +++ b/Pages/Component/Dialog.razor @@ -1,4 +1,6 @@ -<div style="@(heightStyle + visiblilityStyle)"> +@implements IDialog + +<div style="@(heightStyle + visiblilityStyle)"> @if(Title is not null) { <p>@Title</p> <hr/> diff --git a/Pages/Component/TagEditDialog.razor b/Pages/Component/TagEditDialog.razor new file mode 100644 index 0000000..2e443d4 --- /dev/null +++ b/Pages/Component/TagEditDialog.razor @@ -0,0 +1,96 @@ +@inject IDbContextFactory<HBContext> dbFactory; +@inject ITagService tagService +@implements IDialog + +<Dialog Title=@Title @ref=dialog> + <label> + Name + @if(nameExists) { + <p class="error">Tag with that name already exists!</p> + } + </label> + <input type="text" @bind=tagName required/> + <label>Namespace</label> + <input type="text" @bind=tagNamespace/> + <label> + Alias + @if(aliasExists) { + <p class="error">Tag with that alias already exists!</p> + } + </label> + <input type="text" @bind=tagAlias/> + <ButtonContainer> + <button @onclick=Hide class="secondary">Cancel</button> + <button @onclick=Submit>@(TagDefinition is null ? "Create" : "Apply")</button> + </ButtonContainer> +</Dialog> + +@code { + [Parameter] + public TagDefinition? TagDefinition { get; set; } + + [Parameter] + public EventHandler OnTagUpdate { get; set; } + + public bool Visible { + get => visible; + set { + if(value) + Load(); + visible = dialog.Visible = value; + } + } + + private string Title => + TagDefinition is null ? "Create a new tag definition" : "Edit tag definition"; + + private Dialog dialog; + + private string? tagName; + private string? tagNamespace; + private string? tagAlias; + + private bool nameExists = false; + private bool aliasExists = false; + + private bool visible = false; + + public void Show() => Visible = true; + public void Hide() => Visible = false; + + public void Show(TagDefinition? toEdit) { + TagDefinition = toEdit; + Visible = true; + } + + public void Show(string? @namespace) { + TagDefinition = null; + Visible = true; + tagNamespace = @namespace; + } + + private void Load() { + tagName = TagDefinition?.Name; + tagNamespace = TagDefinition?.Namespace; + tagAlias = TagDefinition?.Alias; + nameExists = false; + aliasExists = false; + } + + private void Submit() { + try { + if(TagDefinition is null) { + tagService.CreateTagDefinition(tagName, tagNamespace, tagAlias); + } else { + tagService.UpdateTagDefinition(TagDefinition, tagName, tagNamespace, tagAlias); + } + } catch(TagDuplicateException e) { + nameExists = e.NameExists; + aliasExists = e.AliasExists; + return; + } + + OnTagUpdate.Invoke(this, new EventArgs()); + Hide(); + } +} diff --git a/Pages/Component/TagEditDialog.razor.css b/Pages/Component/TagEditDialog.razor.css new file mode 100644 index 0000000..02781c0 --- /dev/null +++ b/Pages/Component/TagEditDialog.razor.css @@ -0,0 +1,11 @@ +p.error { + color: var(--col-error-pri); + display: inline; + font-size: 8pt; + margin-left: 5px; + vertical-align: middle; +} + +input { + width: 100%; +} diff --git a/Pages/Component/TagSelectDialog.razor b/Pages/Component/TagSelectDialog.razor index 916a070..88e1471 100644 --- a/Pages/Component/TagSelectDialog.razor +++ b/Pages/Component/TagSelectDialog.razor @@ -2,6 +2,7 @@ @inject ITagService tagService @inject IUserService userService @implements IDisposable +@implements IDialog <link rel="stylesheet" href="@(nameof(HyperBooru)).styles.css"/> @@ -36,11 +37,11 @@ </Dialog> @code { - [Parameter] - public string? Title { get; set; } + [Parameter] + public string? Title { get; set; } - [Parameter] - public EventCallback<TagDefinition[]> OnSubmit { get; set; } + [Parameter] + public EventCallback<TagDefinition[]> OnSubmit { get; set; } public TagDefinition[] SelectedTags { get; set; } = Array.Empty<TagDefinition>(); diff --git a/Pages/TagDefinitions.razor b/Pages/TagDefinitions.razor index 75bcba0..95253b7 100644 --- a/Pages/TagDefinitions.razor +++ b/Pages/TagDefinitions.razor @@ -8,7 +8,7 @@ <div style="padding:var(--size-default-gap);"> <ButtonContainer> - <button @onclick=PromptToCreate>Create</button> + <button @onclick=PromptTagCreate>Create</button> </ButtonContainer> <TabContainer @ref=tabContainer> @@ -45,7 +45,7 @@ </i> </td> <td class="actions"> - <a href="javascript:;" @onclick=@(() => PromptToEdit(tagDef))>Edit</a> + <a href="javascript:;" @onclick=@(() => tagEditDialog.Show(tagDef))>Edit</a> <a href="javascript:;" @onclick=@(() => PromptToDelete(tagDef))> Delete </a> @@ -66,36 +66,6 @@ </TabContainer> </div> -<Dialog Title="Create a new tag definition" @ref=createTagDialog> - <form @onsubmit=CreateTagDefinition> - <label>Name</label> - <input type="text" @bind=tagName required/> - <label>Namespace</label> - <input type="text" @bind=tagNamespace/> - <label>Alias</label> - <input type="text" @bind=tagAlias/> - <ButtonContainer> - <button class="secondary" @onclick=@(() => createTagDialog.Hide())>Cancel</button> - <button type="submit">Create</button> - </ButtonContainer> - </form> -</Dialog> - -<Dialog Title="Edit tag" @ref=editTagDialog> - <form @onsubmit=EditTagDefinition> - <label>Name</label> - <input type="text" @bind=tagName required/> - <label>Namespace</label> - <input type="text" @bind=tagNamespace/> - <label>Alias</label> - <input type="text" @bind=tagAlias/> - <ButtonContainer> - <button class="secondary" @onclick=@(() => editTagDialog.Hide())>Cancel</button> - <button type="submit">Apply</button> - </ButtonContainer> - </form> -</Dialog> - <Dialog Title="Are you sure you want to delete this tag definition?" @ref=deleteTagDialog> <ButtonContainer> <button @onclick=@(() => deleteTagDialog.Hide()) class="secondary">Cancel</button> @@ -103,23 +73,19 @@ </ButtonContainer> </Dialog> +<TagEditDialog OnTagUpdate=TagUpdated @ref=tagEditDialog/> + <TagSelectDialog OnSubmit=SetImplicitTags @ref=implicitTagDialog /> @code { private TabContainer tabContainer; - private Dialog createTagDialog; private Dialog deleteTagDialog; - private Dialog editTagDialog; + private TagEditDialog tagEditDialog; private TagSelectDialog implicitTagDialog; - private string tagName; - private string? tagNamespace; - private string? tagAlias; - private TagDefinition? toDelete; - private TagDefinition? toEdit; private TagDefinition? toEditImplicit; private TagDefinition[] tagDefinitions; @@ -157,39 +123,6 @@ .ToArray(); } - private void PromptToCreate() { - var curTitle = tabContainer.ActivePane?.Title; - tagNamespace = curTitle == "Default" ? null : curTitle; - createTagDialog.Show(); - } - - private void CreateTagDefinition() { - if(string.IsNullOrEmpty(tagNamespace)) - tagNamespace = null; - - tagService.CreateTagDefinition(tagName, tagNamespace, tagAlias); - createTagDialog.Hide(); - LoadTags(); - StateHasChanged(); - } - - private void PromptToEdit(TagDefinition toEdit) { - this.toEdit = toEdit; - tagName = toEdit.Name; - tagNamespace = toEdit.Namespace; - tagAlias = toEdit.Alias; - editTagDialog.Show(); - } - - private void EditTagDefinition() { - if(toEdit is null) - return; - - tagService.UpdateTagDefinition(toEdit, tagName, tagNamespace, tagAlias); - LoadTags(); - StateHasChanged(); - } - private void PromptToDelete(TagDefinition toDelete) { this.toDelete = toDelete; deleteTagDialog.Show(); @@ -205,6 +138,18 @@ StateHasChanged(); } + private void PromptTagCreate() { + string? @namespace = tabContainer.ActivePane?.Title; + if(@namespace == "Default") + @namespace = null; + tagEditDialog.Show(@namespace); + } + + private void TagUpdated(object? sender, EventArgs e) { + LoadTags(); + StateHasChanged(); + } + private void PromptImplicitTags(TagDefinition toEditImplicit) { this.toEditImplicit = toEditImplicit; implicitTagDialog.SelectedTags = diff --git a/Pages/TagDefinitions.razor.css b/Pages/TagDefinitions.razor.css deleted file mode 100644 index b66d491..0000000 --- a/Pages/TagDefinitions.razor.css +++ /dev/null @@ -1,3 +0,0 @@ -form > input { - width: 100%; -}
\ No newline at end of file diff --git a/Services/TagService.cs b/Services/TagService.cs index 2648bb3..a3c4b29 100644 --- a/Services/TagService.cs +++ b/Services/TagService.cs @@ -126,14 +126,29 @@ public class TagService : ITagService { public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null) { using var db = dbFactory.CreateDbContext(); - TagDefinition tagdef = new() { + if(string.IsNullOrEmpty(@namespace)) + @namespace = null; + if(string.IsNullOrEmpty(alias)) + alias = null; + + TagDefinition tagDef = new() { Source = TagSource.UserTag, Namespace = @namespace, Name = name, Alias = alias }; - if(!db.TagDefinitions.Contains(tagdef)) - db.TagDefinitions.Add(tagdef); + + bool nameExists = db.TagDefinitions.Any(td => td.Name.ToLower() == name.ToLower()); + bool aliasExists = false; + if(alias is not null) + aliasExists = db.TagDefinitions + .Where(td => td.Alias != null) + .Any(td => td.Alias!.ToLower() == alias.ToLower()); + if(nameExists || aliasExists) + throw new TagDuplicateException(nameExists, aliasExists); + + if(!db.TagDefinitions.Contains(tagDef)) + db.TagDefinitions.Add(tagDef); db.SaveChanges(); } @@ -162,8 +177,22 @@ public class TagService : ITagService { if(string.IsNullOrEmpty(@namespace)) @namespace = null; + if(string.IsNullOrEmpty(alias)) + alias = null; var tag = db.TagDefinitions.First(td => td.Guid == tagDef); + + TagDefinition? nameExisting = db.TagDefinitions.FirstOrDefault(td => td.Name.ToLower() == name.ToLower()); + TagDefinition? aliasExisting = null; + if(alias is not null) + aliasExisting = db.TagDefinitions + .Where(td => td.Alias != null) + .FirstOrDefault(td => td.Alias!.ToLower() == alias.ToLower()); + bool nameExists = nameExisting is not null && nameExisting != tag; + bool aliasExists = aliasExisting is not null && aliasExisting != tag; + if(nameExists || aliasExists) + throw new TagDuplicateException(nameExists, aliasExists); + tag.Name = name; tag.Namespace = @namespace; tag.Alias = alias; @@ -1,15 +1,11 @@ # Bugs - Input not focused - - Tag aliases/names not verified to be unique - - Tag edit dialog doesn't close when clicking Apply button - - Tag selection dialog has no max height - Setting implicit tags removes builtin tags - UserService listeners don't seem to be removed after disposal - Cancelling tag creation creates the tag anyway - Prevent marking tagging complete unless there are actually user tags # Short-term Features - - Progressive page loading - Proper thumbnail generation - Video support - User/security support diff --git a/wwwroot/styles/global.css b/wwwroot/styles/global.css index 50f20f1..a8c4202 100644 --- a/wwwroot/styles/global.css +++ b/wwwroot/styles/global.css @@ -3,6 +3,7 @@ :root { --col-accent-pri: #0aa; --col-accent-pri-hl: #0cc; + --col-error-pri: #ffaa00; --col-bg: #222; --col-dialog-bg: #333; --col-navbar-bg: var(--col-accent-pri); |
