From 35621965c87f82e72720464ffee405b295043509 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:00:31 +1000 Subject: [PATCH 01/15] Remove EntryToolTip --- .../KnockoutExtensions/EntryToolTip.tsx | 350 ------------------ .../Shared/Partials/Album/AlbumLink.tsx | 30 +- .../Shared/Partials/Artist/ArtistLink.tsx | 34 +- .../Shared/Partials/Event/EventLink.tsx | 26 +- .../Shared/Partials/Shared/EntryLink.tsx | 31 +- .../Shared/Partials/Song/SongLink.tsx | 40 +- .../Shared/Partials/Song/SongLinkKnockout.tsx | 26 +- .../Shared/Partials/Tag/TagLink.tsx | 32 +- .../Shared/Partials/User/UserLink.tsx | 31 +- 9 files changed, 183 insertions(+), 417 deletions(-) delete mode 100644 VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx deleted file mode 100644 index 9a143398a6..0000000000 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import { BsPrefixRefForwardingComponent } from '@/Bootstrap/helpers'; -import { EntryRefContract } from '@/DataContracts/EntryRefContract'; -import { functions } from '@/Shared/GlobalFunctions'; -import $ from 'jquery'; -import _ from 'lodash'; -import 'qtip2'; -import React from 'react'; - -const allowedDomains = [ - 'http://vocadb.net', - 'https://vocadb.net', - 'http://utaitedb.net', - 'https://utaitedb.net', - 'https://touhoudb.com', -]; - -interface ToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - relativeUrl: string; - id: number; - params?: any; - foreignDomain?: string; -} - -const ToolTip = React.forwardRef( - ( - { - as: Component = 'div', - children, - relativeUrl, - id, - params, - foreignDomain, - ...props - }: ToolTipProps, - ref, - ): React.ReactElement => { - const el = React.useRef(undefined!); - React.useImperativeHandle(ref, () => el.current); - - React.useEffect(() => { - const url = - foreignDomain && - allowedDomains.some((domain) => - foreignDomain.toLocaleLowerCase().includes(domain), - ) - ? functions.mergeUrls(foreignDomain, relativeUrl) - : functions.mapAbsoluteUrl(relativeUrl); - const data = _.assign({ id: id }, params); - - $(el.current).qtip({ - content: { - text: 'Loading...' /* TODO: localize */, - ajax: { - url: url, - type: 'GET', - data: data, - dataType: foreignDomain ? 'jsonp' : undefined, - }, - }, - position: { - viewport: $(window), - }, - style: { - classes: 'tooltip-wide', - }, - }); - - return (): void => { - $('.qtip').remove(); - }; - }); - - return ( - - {children} - - ); - }, -); - -interface AlbumToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; -} - -export const AlbumToolTip: BsPrefixRefForwardingComponent< - 'div', - AlbumToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: AlbumToolTipProps, - ref, - ): React.ReactElement => { - return ( - - {children} - - ); - }, -); - -interface ArtistToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; -} - -export const ArtistToolTip: BsPrefixRefForwardingComponent< - 'div', - ArtistToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: ArtistToolTipProps, - ref, - ): React.ReactElement => { - return ( - - {children} - - ); - }, -); - -interface EventToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; -} - -export const EventToolTip: BsPrefixRefForwardingComponent< - 'div', - EventToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: EventToolTipProps, - ref, - ): React.ReactElement => { - const culture = vdb.values.uiCulture || undefined; - - return ( - - {children} - - ); - }, -); - -interface SongToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; - toolTipDomain?: string; - version?: number; -} - -export const SongToolTip: BsPrefixRefForwardingComponent< - 'div', - SongToolTipProps -> = React.forwardRef( - ( - { as, children, id, toolTipDomain, version, ...props }: SongToolTipProps, - ref, - ): React.ReactElement => { - return ( - - {children} - - ); - }, -); - -interface TagToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; -} - -export const TagToolTip: BsPrefixRefForwardingComponent< - 'div', - TagToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: TagToolTipProps, - ref, - ): React.ReactElement => { - const culture = vdb.values.uiCulture || undefined; - const lang = vdb.values.languagePreference; - - return ( - - {children} - - ); - }, -); - -interface UserToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - id: number; -} - -export const UserToolTip: BsPrefixRefForwardingComponent< - 'div', - UserToolTipProps -> = React.forwardRef( - ( - { as, children, id, ...props }: UserToolTipProps, - ref, - ): React.ReactElement => { - var culture = vdb.values.uiCulture || undefined; - - return ( - - {children} - - ); - }, -); - -interface EntryToolTipProps { - as?: React.ElementType; - children?: React.ReactNode; - value: EntryRefContract; -} - -export const EntryToolTip: BsPrefixRefForwardingComponent< - 'div', - EntryToolTipProps -> = React.forwardRef( - ( - { as, children, value, ...props }: EntryToolTipProps, - ref, - ): React.ReactElement => { - switch (value.entryType) { - case 'Album' /* TODO: enum */: - return ( - - ); - - case 'Artist' /* TODO: enum */: - return ( - - ); - - case 'ReleaseEvent' /* TODO: enum */: - return ( - - ); - - case 'Song' /* TODO: enum */: - return ( - - ); - - case 'Tag' /* TODO: enum */: - return ( - - ); - - case 'User' /* TODO: enum */: - return ( - - ); - - default: - return <>; - } - }, -); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx index d3ef983e10..923bc0c2f0 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx @@ -1,10 +1,25 @@ -import { AlbumToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { AlbumForApiContract } from '@/DataContracts/Album/AlbumForApiContract'; import { EntryType } from '@/Models/EntryType'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link } from 'react-router-dom'; +interface AlbumLinkBaseProps { + album: AlbumForApiContract; +} + +const AlbumLinkBase = ({ album }: AlbumLinkBaseProps): React.ReactElement => { + return ( + + {album.name} + + ); +}; + interface AlbumLinkProps { album: AlbumForApiContract; tooltip: boolean; @@ -15,7 +30,7 @@ export const AlbumLink = ({ tooltip = false, }: AlbumLinkProps): React.ReactElement => { return tooltip ? ( - {album.name} - + */ + ) : ( - - {album.name} - + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx index 0f8c71efc2..f27d59404c 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx @@ -1,4 +1,3 @@ -import { ArtistToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { ArtistTypeLabel } from '@/Components/Shared/Partials/Artist/ArtistTypeLabel'; import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; import { EntryType } from '@/Models/EntryType'; @@ -6,6 +5,26 @@ import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link } from 'react-router-dom'; +interface ArtistLinkBaseProps { + artist: ArtistContract; + children?: React.ReactNode; +} + +const ArtistLinkBase = ({ + artist, + children, +}: ArtistLinkBaseProps): React.ReactElement => { + return ( + + {children ?? artist.name} + + ); +}; + interface ArtistLinkProps { artist: ArtistContract; typeLabel?: boolean; @@ -26,7 +45,7 @@ export const ArtistLink = ({ {typeLabel && } {typeLabel && ' '} {tooltip ? ( - {name ?? artist.name} - + */ + {name} ) : ( - - {name ?? artist.name} - + {name} )} {releaseYear && artist.releaseDate && ( <> diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx index 196151b358..5de23c36bb 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx @@ -1,10 +1,23 @@ -import { EventToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; import { EntryType } from '@/Models/EntryType'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link } from 'react-router-dom'; +interface EventLinkBaseProps { + event: ReleaseEventContract; +} + +const EventLinkBase = ({ event }: EventLinkBaseProps): React.ReactElement => { + return ( + + {event.name} + + ); +}; + interface EventLinkProps { event: ReleaseEventContract; tooltip?: boolean; @@ -15,18 +28,15 @@ export const EventLink = ({ tooltip, }: EventLinkProps): React.ReactElement => { return tooltip ? ( - {event.name} - + */ + ) : ( - - {event.name} - + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/EntryLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/EntryLink.tsx index f3160a3033..cfb52cc555 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/EntryLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/EntryLink.tsx @@ -1,9 +1,25 @@ -import { EntryToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { EntryBaseContract } from '@/DataContracts/EntryBaseContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link, LinkProps } from 'react-router-dom'; +interface EntryLinkBaseProps extends Omit { + entry: EntryBaseContract; + children?: React.ReactNode; +} + +const EntryLinkBase = ({ + entry, + children, + ...props +}: EntryLinkBaseProps): React.ReactElement => { + return ( + + {children ?? entry.defaultName} + + ); +}; + interface EntryLinkProps extends Omit { entry: EntryBaseContract; children?: React.ReactNode; @@ -18,18 +34,21 @@ export const EntryLink = React.memo( ...props }: EntryLinkProps): React.ReactElement => { return tooltip ? ( - {children ?? entry.defaultName} - + */ + + {children} + ) : ( - - {children ?? entry.defaultName} - + + {children} + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx index cbf0944906..70454db95f 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx @@ -1,10 +1,33 @@ -import { SongToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import qs from 'qs'; import React from 'react'; import { Link } from 'react-router-dom'; +interface SongLinkBaseProps { + song: SongApiContract; + albumId?: number; + target?: string; +} + +const SongLinkBase = ({ + song, + albumId, + target, +}: SongLinkBaseProps): React.ReactElement => { + return ( + + {song.name} + + ); +}; + interface SongLinkProps { song: SongApiContract; albumId?: number; @@ -22,7 +45,7 @@ export const SongLink = React.memo( target, }: SongLinkProps): React.ReactElement => { return tooltip ? ( - {song.name} - + */ + ) : ( - - {song.name} - + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx index 1893056167..2527fd7761 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx @@ -1,8 +1,23 @@ -import { SongToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { SongLink } from '@/Components/Shared/Partials/Song/SongLink'; import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import React from 'react'; +interface SongLinkKnockoutBaseProps { + song: SongApiContract; + extUrl?: string; +} + +const SongLinkKnockoutBase = ({ + song, + extUrl, +}: SongLinkKnockoutBaseProps): React.ReactElement => { + return ( + + {song.name} + + ); +}; + interface SongLinkKnockoutProps { song: SongApiContract; albumId?: number; @@ -21,7 +36,7 @@ export const SongLinkKnockout = React.memo( }: SongLinkKnockoutProps): React.ReactElement => { return extUrl ? ( tooltip ? ( - {song.name} - + */ + ) : ( - - {song.name} - + ) ) : ( { + return ( + + {children ?? tag.name} + + ); +}; + interface TagLinkProps { tag: TagBaseContract; children?: React.ReactNode; @@ -13,21 +31,17 @@ interface TagLinkProps { export const TagLink = React.memo( ({ tag, children, tooltip }: TagLinkProps): React.ReactElement => { return tooltip ? ( - {children ?? tag.name} - + */ + {children} ) : ( - - {children ?? tag.name} - + {children} ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx index e6ed2943df..d42e9c70cf 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx @@ -1,9 +1,25 @@ -import { UserToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { UserBaseContract } from '@/DataContracts/User/UserBaseContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; import { Link, LinkProps } from 'react-router-dom'; +interface UserLinkBaseProps extends Omit { + user: UserBaseContract; + children?: React.ReactNode; +} + +const UserLinkBase = ({ + user, + children, + ...props +}: UserLinkBaseProps): React.ReactElement => { + return ( + + {children ?? user.name} + + ); +}; + interface UserLinkProps extends Omit { user: UserBaseContract; children?: React.ReactNode; @@ -18,18 +34,21 @@ export const UserLink = React.memo( ...props }: UserLinkProps): React.ReactElement => { return tooltip ? ( - {children ?? user.name} - + */ + + {children} + ) : ( - - {children ?? user.name} - + + {children} + ); }, ); From a44feee5e6c302c50e512867af3106745a515b68 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:36:14 +1000 Subject: [PATCH 02/15] Update Site.css --- VocaDbWeb/wwwroot/Content/Site.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VocaDbWeb/wwwroot/Content/Site.css b/VocaDbWeb/wwwroot/Content/Site.css index a5436a7844..0a7edc4970 100644 --- a/VocaDbWeb/wwwroot/Content/Site.css +++ b/VocaDbWeb/wwwroot/Content/Site.css @@ -147,7 +147,7 @@ div.thumbItem { overflow: hidden; }*/ -.thumbs .pictureFrame { +.thumbs .pictureFrame a { position: relative; width: 250px; height: 250px; @@ -175,7 +175,7 @@ div.thumbItem { } } -.smallThumbs .pictureFrame { +.smallThumbs .pictureFrame a { position: relative; width: 150px; height: 150px; From f5ce4f1ee26a74a7975f88e3bbc8a9183fc213a1 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Tue, 13 Sep 2022 18:15:31 +1000 Subject: [PATCH 03/15] Refactor --- VocaDbWeb/Scripts/Pages/Event/EventIndex.tsx | 12 +++++- .../Scripts/Repositories/ArtistRepository.ts | 22 ++++++++--- .../Repositories/ReleaseEventRepository.ts | 17 +++++++- .../Repositories/SongListRepository.ts | 11 +++++- .../Scripts/Repositories/SongRepository.ts | 12 +++--- .../Scripts/Repositories/TagRepository.ts | 22 ++++++++--- .../Scripts/Repositories/UserRepository.ts | 39 +++++++++++++------ .../Scripts/Stores/Album/AlbumEditStore.ts | 8 ++-- .../ReleaseEvent/ReleaseEventDetailsStore.ts | 7 +++- .../Stores/Search/ArtistSearchStore.ts | 15 +++++-- .../Scripts/Stores/Search/EventSearchStore.ts | 22 +++++++++-- .../Scripts/Stores/Search/SongSearchStore.ts | 11 ++++-- VocaDbWeb/Scripts/Stores/Search/TagFilters.ts | 1 - .../Scripts/Stores/Search/TagSearchStore.ts | 7 +++- .../Scripts/Stores/SelfDescriptionStore.ts | 7 +++- .../PlayListRepositoryForRatedSongsAdapter.ts | 2 +- .../PlayListRepositoryForSongsAdapter.ts | 2 +- .../Scripts/Stores/Song/SongDetailsStore.ts | 9 +++-- .../Stores/SongList/SongListsBaseStore.ts | 7 +++- .../Scripts/Stores/User/ListUsersStore.ts | 7 +++- .../Stores/User/RatedSongsSearchStore.ts | 17 +++++--- VocaDbWeb/Scripts/Tag/Edit.ts | 12 ++++-- .../Tests/TestSupport/FakeSongRepository.ts | 7 +++- .../ViewModels/Album/AlbumEditViewModel.ts | 10 ++++- .../ReleaseEventDetailsViewModel.ts | 10 ++++- .../Search/ArtistSearchViewModel.ts | 13 +++++-- .../ViewModels/Search/EventSearchViewModel.ts | 20 ++++++++-- .../ViewModels/Search/SongSearchViewModel.ts | 15 +++++-- .../Scripts/ViewModels/Search/TagFilters.ts | 1 - .../ViewModels/Search/TagSearchViewModel.ts | 7 +++- .../ViewModels/SelfDescriptionViewModel.ts | 7 +++- .../PlayListRepositoryForRatedSongsAdapter.ts | 2 +- .../PlayListRepositoryForSongsAdapter.ts | 2 +- .../ViewModels/Song/SongDetailsViewModel.ts | 9 +++-- .../SongList/SongListsBaseViewModel.ts | 5 ++- .../ViewModels/Tag/TagMergeViewModel.ts | 2 +- .../ViewModels/User/ListUsersViewModel.ts | 7 +++- .../User/RatedSongsSearchViewModel.ts | 13 ++++++- 38 files changed, 296 insertions(+), 103 deletions(-) diff --git a/VocaDbWeb/Scripts/Pages/Event/EventIndex.tsx b/VocaDbWeb/Scripts/Pages/Event/EventIndex.tsx index 2f96e7fcd8..e500eab81b 100644 --- a/VocaDbWeb/Scripts/Pages/Event/EventIndex.tsx +++ b/VocaDbWeb/Scripts/Pages/Event/EventIndex.tsx @@ -4,7 +4,10 @@ import { useVocaDbTitle } from '@/Components/useVocaDbTitle'; import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; import JQueryUIButton from '@/JQueryUI/JQueryUIButton'; import { LoginManager } from '@/Models/LoginManager'; -import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; +import { + ReleaseEventOptionalField, + ReleaseEventRepository, +} from '@/Repositories/ReleaseEventRepository'; import { HttpClient } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; import { EventSortRule } from '@/Stores/Search/EventSearchStore'; @@ -104,7 +107,12 @@ const EventIndex = (): React.ReactElement => { .getList({ queryParams: { lang: vdb.values.languagePreference, - fields: 'AdditionalNames,MainPicture,Series,Venue', + fields: [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ReleaseEventOptionalField.Venue, + ], afterDate: moment().subtract(2, 'days').toDate(), start: 0, maxResults: 15, diff --git a/VocaDbWeb/Scripts/Repositories/ArtistRepository.ts b/VocaDbWeb/Scripts/Repositories/ArtistRepository.ts index cfab5e042d..b0bf5e1ae8 100644 --- a/VocaDbWeb/Scripts/Repositories/ArtistRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/ArtistRepository.ts @@ -22,6 +22,18 @@ import { HeaderNames, HttpClient, MediaTypes } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; import { AdvancedSearchFilter } from '@/ViewModels/Search/AdvancedSearchFilter'; +export enum ArtistOptionalField { + 'AdditionalNames' = 'AdditionalNames', + 'ArtistLinks' = 'ArtistLinks', + 'ArtistLinksReverse' = 'ArtistLinksReverse', + 'BaseVoicebank' = 'BaseVoicebank', + 'Description' = 'Description', + 'MainPicture' = 'MainPicture', + 'Names' = 'Names', + 'Tags' = 'Tags', + 'WebLinks' = 'WebLinks', +} + // Repository for managing artists and related objects. // Corresponds to the ArtistController class. export class ArtistRepository @@ -142,7 +154,7 @@ export class ArtistRepository }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/artists/${id}`); return this.httpClient.get(url, { - fields: 'AdditionalNames', + fields: [ArtistOptionalField.AdditionalNames].join(','), lang: lang, }); }; @@ -153,12 +165,12 @@ export class ArtistRepository lang, }: { id: number; - fields: string; + fields: ArtistOptionalField[]; lang: ContentLanguagePreference; }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/artists/${id}`); return this.httpClient.get(url, { - fields: fields, + fields: fields.join(','), lang: lang, }); }; @@ -186,7 +198,7 @@ export class ArtistRepository tags: number[]; childTags: boolean; followedByUserId?: number; - fields: string; + fields: ArtistOptionalField[]; status?: string; advancedFilters: AdvancedSearchFilter[]; }): Promise> => { @@ -196,7 +208,7 @@ export class ArtistRepository getTotalCount: paging.getTotalCount, maxResults: paging.maxEntries, query: query, - fields: fields, + fields: fields.join(','), lang: lang, nameMatchMode: 'Auto', sort: sort, diff --git a/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts b/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts index 297ef72368..62567cc154 100644 --- a/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts @@ -16,6 +16,19 @@ import { functions } from '@/Shared/GlobalFunctions'; import { HttpClient } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; +export enum ReleaseEventOptionalField { + 'AdditionalNames' = 'AdditionalNames', + 'Artists' = 'Artists', + 'Description' = 'Description', + 'MainPicture' = 'MainPicture', + 'Names' = 'Names', + 'Series' = 'Series', + 'SongList' = 'SongList', + 'Tags' = 'Tags', + 'Venue' = 'Venue', + 'WebLinks' = 'WebLinks', +} + export class ReleaseEventRepository extends BaseRepository { public constructor( private readonly httpClient: HttpClient, @@ -98,7 +111,7 @@ export class ReleaseEventRepository extends BaseRepository { category: queryParams.category || undefined, tagId: queryParams.tagIds, childTags: queryParams.childTags, - fields: queryParams.fields || undefined, + fields: queryParams.fields?.join(','), userCollectionId: queryParams.userCollectionId || undefined, artistId: queryParams.artistId || undefined, childVoicebanks: queryParams.childVoicebanks || undefined, @@ -294,7 +307,7 @@ export interface EventQueryParams extends CommonQueryParams { childVoicebanks?: boolean; // Comma-separated list of optional fields - fields?: string; + fields?: ReleaseEventOptionalField[]; includeMembers?: boolean; diff --git a/VocaDbWeb/Scripts/Repositories/SongListRepository.ts b/VocaDbWeb/Scripts/Repositories/SongListRepository.ts index df275fdf91..a16b476d56 100644 --- a/VocaDbWeb/Scripts/Repositories/SongListRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/SongListRepository.ts @@ -29,6 +29,13 @@ export interface SongListGetSongsQueryParams { sort: string; } +export enum SongListOptionalField { + 'Description' = 'Description', + 'Events' = 'Events', + 'MainPicture' = 'MainPicture', + 'Tags' = 'Tags', +} + export class SongListRepository { public constructor( private readonly httpClient: HttpClient, @@ -69,7 +76,7 @@ export class SongListRepository { category: string; paging: PagingProperties; tagIds: number[]; - fields: string; + fields: SongListOptionalField[]; sort: string; }): Promise> => { var url = this.urlMapper.mapRelative('/api/songLists/featured'); @@ -82,7 +89,7 @@ export class SongListRepository { getTotalCount: paging.getTotalCount, maxResults: paging.maxEntries, tagId: tagIds, - fields: fields, + fields: fields.join(','), sort: sort, }, ); diff --git a/VocaDbWeb/Scripts/Repositories/SongRepository.ts b/VocaDbWeb/Scripts/Repositories/SongRepository.ts index 4c626a03b6..6485050981 100644 --- a/VocaDbWeb/Scripts/Repositories/SongRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/SongRepository.ts @@ -299,12 +299,12 @@ export class SongRepository lang, }: { id: number; - fields: string; + fields: SongOptionalField[]; lang: ContentLanguagePreference; }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/songs/${id}`); return this.httpClient.get(url, { - fields: fields, + fields: fields.join(','), lang: lang, }); }; @@ -318,7 +318,7 @@ export class SongRepository }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/songs/${id}`); return this.httpClient.get(url, { - fields: 'AdditionalNames', + fields: [SongOptionalField.AdditionalNames].join(','), lang: lang, }); }; @@ -342,7 +342,7 @@ export class SongRepository pvServices, queryParams, }: { - fields: string /* TODO: enum */; + fields: SongOptionalField[]; lang: ContentLanguagePreference; paging: PagingProperties; pvServices?: PVService[]; @@ -381,7 +381,7 @@ export class SongRepository getTotalCount: paging.getTotalCount, maxResults: paging.maxEntries, query: query, - fields: fields, + fields: fields.join(','), lang: lang, nameMatchMode: 'Auto', sort: sort, @@ -428,7 +428,7 @@ export class SongRepository queryParams: SongGetListQueryParams; }): Promise> => { const { items, totalCount } = await this.getList({ - fields: ['MainPicture', 'PVs'].join(',') /* TODO: enum */, + fields: [SongOptionalField.MainPicture, SongOptionalField.PVs], lang: lang, paging: paging, pvServices: pvServices, diff --git a/VocaDbWeb/Scripts/Repositories/TagRepository.ts b/VocaDbWeb/Scripts/Repositories/TagRepository.ts index ac0f78f93c..bf0c2bf5e8 100644 --- a/VocaDbWeb/Scripts/Repositories/TagRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/TagRepository.ts @@ -21,6 +21,18 @@ import { functions } from '@/Shared/GlobalFunctions'; import { HttpClient } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; +export enum TagOptionalField { + 'AliasedTo' = 'AliasedTo', + 'AdditionalNames' = 'AdditionalNames', + 'Description' = 'Description', + 'MainPicture' = 'MainPicture', + 'Names' = 'Names', + 'Parent' = 'Parent', + 'RelatedTags' = 'RelatedTags', + 'TranslatedDescription' = 'TranslatedDescription', + 'WebLinks' = 'WebLinks', +} + export class TagRepository extends BaseRepository { private readonly urlMapper: UrlMapper; @@ -62,12 +74,12 @@ export class TagRepository extends BaseRepository { lang, }: { id: number; - fields?: string; + fields?: TagOptionalField[]; lang?: ContentLanguagePreference; }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/tags/${id}`); return this.httpClient.get(url, { - fields: fields || undefined, + fields: fields?.join(','), lang: lang, }); }; @@ -94,7 +106,7 @@ export class TagRepository extends BaseRepository { `/api/entry-types/${EntryType[entryType]}/${subType}/tag`, ); return this.httpClient.get(url, { - fields: 'Description', + fields: [TagOptionalField.Description].join(','), lang: lang, }); }; @@ -112,7 +124,7 @@ export class TagRepository extends BaseRepository { getTotalCount: queryParams.getTotalCount, maxResults: queryParams.maxResults, query: queryParams.query, - fields: queryParams.fields || undefined, + fields: queryParams.fields?.join(','), nameMatchMode: NameMatchMode[nameMatchMode], allowAliases: queryParams.allowAliases, categoryName: queryParams.categoryName, @@ -238,7 +250,7 @@ export interface TagQueryParams extends CommonQueryParams { categoryName?: string; // Comma-separated list of optional fields - fields?: string; + fields?: TagOptionalField[]; sort?: string; } diff --git a/VocaDbWeb/Scripts/Repositories/UserRepository.ts b/VocaDbWeb/Scripts/Repositories/UserRepository.ts index b8189a10c1..34dcdd0c6b 100644 --- a/VocaDbWeb/Scripts/Repositories/UserRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/UserRepository.ts @@ -26,7 +26,11 @@ import { ContentLanguagePreference } from '@/Models/Globalization/ContentLanguag import { PVService } from '@/Models/PVs/PVService'; import { SongVoteRating } from '@/Models/SongVoteRating'; import { UserEventRelationshipType } from '@/Models/Users/UserEventRelationshipType'; +import { AlbumOptionalField } from '@/Repositories/AlbumRepository'; +import { ArtistOptionalField } from '@/Repositories/ArtistRepository'; import { ICommentRepository } from '@/Repositories/ICommentRepository'; +import { SongListOptionalField } from '@/Repositories/SongListRepository'; +import { SongOptionalField } from '@/Repositories/SongRepository'; import { HeaderNames, HttpClient, MediaTypes } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; import { AdvancedSearchFilter } from '@/ViewModels/Search/AdvancedSearchFilter'; @@ -51,6 +55,12 @@ export interface UserGetRatedSongsListQueryParams { sort: string; } +export enum UserOptionalField { + 'KnownLanguages' = 'KnownLanguages', + 'MainPicture' = 'MainPicture', + 'OldUsernames' = 'OldUsernames', +} + // Repository for managing users and related objects. // Corresponds to the UserController class. export class UserRepository implements ICommentRepository { @@ -232,7 +242,11 @@ export class UserRepository implements ICommentRepository { artistId: artistId, purchaseStatuses: purchaseStatuses, releaseEventId: releaseEventId || undefined, - fields: 'AdditionalNames,MainPicture,ReleaseEvent', + fields: [ + AlbumOptionalField.AdditionalNames, + AlbumOptionalField.MainPicture, + AlbumOptionalField.ReleaseEvent, + ].join(','), lang: lang, nameMatchMode: 'Auto', sort: sort, @@ -300,7 +314,10 @@ export class UserRepository implements ICommentRepository { getTotalCount: paging.getTotalCount, maxResults: paging.maxEntries, tagId: tagIds, - fields: 'AdditionalNames,MainPicture', + fields: [ + ArtistOptionalField.AdditionalNames, + ArtistOptionalField.MainPicture, + ].join(','), lang: lang, nameMatchMode: 'Auto', artistType: artistType, @@ -330,7 +347,7 @@ export class UserRepository implements ICommentRepository { onlyVerified: boolean; knowsLanguage?: string; nameMatchMode: string; - fields?: string; + fields?: UserOptionalField[]; }): Promise> => { var url = this.urlMapper.mapRelative('/api/users'); var data = { @@ -344,7 +361,7 @@ export class UserRepository implements ICommentRepository { onlyVerified: onlyVerified, knowsLanguage: knowsLanguage, groups: groups || undefined, - fields: fields || undefined, + fields: fields?.join(','), }; return this.httpClient.get>( @@ -358,11 +375,11 @@ export class UserRepository implements ICommentRepository { fields, }: { id: number; - fields?: string; + fields?: UserOptionalField[]; }): Promise => { var url = this.urlMapper.mapRelative(`/api/users/${id}`); return this.httpClient.get(url, { - fields: fields || undefined, + fields: fields?.join(','), }); }; @@ -429,7 +446,7 @@ export class UserRepository implements ICommentRepository { pvServices, queryParams, }: { - fields: string; + fields: SongOptionalField[]; lang: ContentLanguagePreference; paging: PagingProperties; pvServices?: PVService[]; @@ -462,7 +479,7 @@ export class UserRepository implements ICommentRepository { advancedFilters: advancedFilters, groupByRating: groupByRating, pvServices: pvServices?.join(','), - fields: fields, + fields: fields.join(','), lang: lang, nameMatchMode: 'Auto', sort: sort, @@ -489,7 +506,7 @@ export class UserRepository implements ICommentRepository { > > => { const { items, totalCount } = await this.getRatedSongsList({ - fields: ['MainPicture', 'PVs'].join(',') /* TODO: enum */, + fields: [SongOptionalField.MainPicture, SongOptionalField.PVs], lang: lang, paging: paging, pvServices: pvServices, @@ -534,7 +551,7 @@ export class UserRepository implements ICommentRepository { paging: PagingProperties; tagIds: number[]; sort: string; - fields?: string; + fields?: SongListOptionalField[]; }): Promise> => { var url = this.urlMapper.mapRelative(`/api/users/${userId}/songLists`); return this.httpClient.get>( @@ -546,7 +563,7 @@ export class UserRepository implements ICommentRepository { maxResults: paging.maxEntries, tagId: tagIds, sort: sort, - fields: fields, + fields: fields?.join(','), }, ); }; diff --git a/VocaDbWeb/Scripts/Stores/Album/AlbumEditStore.ts b/VocaDbWeb/Scripts/Stores/Album/AlbumEditStore.ts index cc09f58a11..bf4c5f373d 100644 --- a/VocaDbWeb/Scripts/Stores/Album/AlbumEditStore.ts +++ b/VocaDbWeb/Scripts/Stores/Album/AlbumEditStore.ts @@ -9,7 +9,10 @@ import { AlbumRepository } from '@/Repositories/AlbumRepository'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; import { PVRepository } from '@/Repositories/PVRepository'; import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import { GlobalValues } from '@/Shared/GlobalValues'; import { UrlMapper } from '@/Shared/UrlMapper'; @@ -318,8 +321,7 @@ export class AlbumEditStore { if (songId) { const song = await this.songRepo.getOneWithComponents({ id: songId, - fields: ['AdditionalNames', 'Artists'] /* TODO: enum */ - .join(','), + fields: [SongOptionalField.AdditionalNames, SongOptionalField.Artists], lang: this.values.languagePreference, }); diff --git a/VocaDbWeb/Scripts/Stores/ReleaseEvent/ReleaseEventDetailsStore.ts b/VocaDbWeb/Scripts/Stores/ReleaseEvent/ReleaseEventDetailsStore.ts index dc0741881b..184a1d1bf1 100644 --- a/VocaDbWeb/Scripts/Stores/ReleaseEvent/ReleaseEventDetailsStore.ts +++ b/VocaDbWeb/Scripts/Stores/ReleaseEvent/ReleaseEventDetailsStore.ts @@ -7,7 +7,10 @@ import { LoginManager } from '@/Models/LoginManager'; import { UserEventRelationshipType } from '@/Models/Users/UserEventRelationshipType'; import { CommentRepository } from '@/Repositories/CommentRepository'; import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; -import { UserRepository } from '@/Repositories/UserRepository'; +import { + UserOptionalField, + UserRepository, +} from '@/Repositories/UserRepository'; import { HttpClient } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; import { EditableCommentsStore } from '@/Stores/EditableCommentsStore'; @@ -120,7 +123,7 @@ export class ReleaseEventDetailsStore { this.userRepo .getOne({ id: this.loginManager.loggedUserId, - fields: 'MainPicture', + fields: [UserOptionalField.MainPicture], }) .then((user) => { runInAction(() => { diff --git a/VocaDbWeb/Scripts/Stores/Search/ArtistSearchStore.ts b/VocaDbWeb/Scripts/Stores/Search/ArtistSearchStore.ts index 68a286410f..451327f8de 100644 --- a/VocaDbWeb/Scripts/Stores/Search/ArtistSearchStore.ts +++ b/VocaDbWeb/Scripts/Stores/Search/ArtistSearchStore.ts @@ -3,7 +3,10 @@ import { PagingProperties } from '@/DataContracts/PagingPropertiesContract'; import { PartialFindResultContract } from '@/DataContracts/PartialFindResultContract'; import { ArtistHelper } from '@/Helpers/ArtistHelper'; import { ArtistType } from '@/Models/Artists/ArtistType'; -import { ArtistRepository } from '@/Repositories/ArtistRepository'; +import { + ArtistOptionalField, + ArtistRepository, +} from '@/Repositories/ArtistRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { AdvancedSearchFilter } from '@/Stores/Search/AdvancedSearchFilter'; import { ICommonSearchStore } from '@/Stores/Search/CommonSearchStore'; @@ -57,10 +60,14 @@ export class ArtistSearchStore extends SearchCategoryBaseStore< makeObservable(this); } - @computed public get fields(): string { + @computed public get fields(): ArtistOptionalField[] { return this.showTags - ? 'AdditionalNames,MainPicture,Tags' - : 'AdditionalNames,MainPicture'; + ? [ + ArtistOptionalField.AdditionalNames, + ArtistOptionalField.MainPicture, + ArtistOptionalField.Tags, + ] + : [ArtistOptionalField.AdditionalNames, ArtistOptionalField.MainPicture]; } public loadResults = ( diff --git a/VocaDbWeb/Scripts/Stores/Search/EventSearchStore.ts b/VocaDbWeb/Scripts/Stores/Search/EventSearchStore.ts index 31eab8da71..981a994a2a 100644 --- a/VocaDbWeb/Scripts/Stores/Search/EventSearchStore.ts +++ b/VocaDbWeb/Scripts/Stores/Search/EventSearchStore.ts @@ -2,7 +2,10 @@ import { PagingProperties } from '@/DataContracts/PagingPropertiesContract'; import { PartialFindResultContract } from '@/DataContracts/PartialFindResultContract'; import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; -import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; +import { + ReleaseEventOptionalField, + ReleaseEventRepository, +} from '@/Repositories/ReleaseEventRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { ArtistFilters } from '@/Stores/Search/ArtistFilters'; import { ICommonSearchStore } from '@/Stores/Search/CommonSearchStore'; @@ -64,10 +67,21 @@ export class EventSearchStore extends SearchCategoryBaseStore< this.artistFilters = new ArtistFilters(values, artistRepo); } - @computed public get fields(): string { + @computed public get fields(): ReleaseEventOptionalField[] { return this.showTags - ? 'AdditionalNames,MainPicture,Series,Venue,Tags' - : 'AdditionalNames,MainPicture,Series,Venue'; + ? [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ReleaseEventOptionalField.Venue, + ReleaseEventOptionalField.Tags, + ] + : [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ReleaseEventOptionalField.Venue, + ]; } public loadResults = ( diff --git a/VocaDbWeb/Scripts/Stores/Search/SongSearchStore.ts b/VocaDbWeb/Scripts/Stores/Search/SongSearchStore.ts index 5c99c3b6f1..53b9250a35 100644 --- a/VocaDbWeb/Scripts/Stores/Search/SongSearchStore.ts +++ b/VocaDbWeb/Scripts/Stores/Search/SongSearchStore.ts @@ -10,6 +10,7 @@ import { ArtistRepository } from '@/Repositories/ArtistRepository'; import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; import { SongGetListQueryParams, + SongOptionalField, SongRepository, } from '@/Repositories/SongRepository'; import { UserRepository } from '@/Repositories/UserRepository'; @@ -197,10 +198,14 @@ export class SongSearchStore ); } - @computed public get fields(): string { + @computed public get fields(): SongOptionalField[] { return this.showTags - ? 'AdditionalNames,MainPicture,Tags' - : 'AdditionalNames,MainPicture' /* TODO: enum */; + ? [ + SongOptionalField.AdditionalNames, + SongOptionalField.MainPicture, + SongOptionalField.Tags, + ] + : [SongOptionalField.AdditionalNames, SongOptionalField.MainPicture]; } @computed public get showUnifyEntryTypesAndTags(): boolean { diff --git a/VocaDbWeb/Scripts/Stores/Search/TagFilters.ts b/VocaDbWeb/Scripts/Stores/Search/TagFilters.ts index e0520d8b06..26be2d97e6 100644 --- a/VocaDbWeb/Scripts/Stores/Search/TagFilters.ts +++ b/VocaDbWeb/Scripts/Stores/Search/TagFilters.ts @@ -55,7 +55,6 @@ export class TagFilters { this.tagRepo .getById({ id: selectedTagId, - fields: undefined, lang: this.values.languagePreference, }) .then((tag) => { diff --git a/VocaDbWeb/Scripts/Stores/Search/TagSearchStore.ts b/VocaDbWeb/Scripts/Stores/Search/TagSearchStore.ts index 1b71bc1381..0139f7875b 100644 --- a/VocaDbWeb/Scripts/Stores/Search/TagSearchStore.ts +++ b/VocaDbWeb/Scripts/Stores/Search/TagSearchStore.ts @@ -1,7 +1,7 @@ import { PagingProperties } from '@/DataContracts/PagingPropertiesContract'; import { PartialFindResultContract } from '@/DataContracts/PartialFindResultContract'; import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; -import { TagRepository } from '@/Repositories/TagRepository'; +import { TagOptionalField, TagRepository } from '@/Repositories/TagRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { ICommonSearchStore } from '@/Stores/Search/CommonSearchStore'; import { SearchCategoryBaseStore } from '@/Stores/Search/SearchCategoryBaseStore'; @@ -56,7 +56,10 @@ export class TagSearchStore extends SearchCategoryBaseStore< sort: this.sort, allowAliases: this.allowAliases, categoryName: this.categoryName, - fields: 'AdditionalNames,MainPicture', + fields: [ + TagOptionalField.AdditionalNames, + TagOptionalField.MainPicture, + ], }, }); }; diff --git a/VocaDbWeb/Scripts/Stores/SelfDescriptionStore.ts b/VocaDbWeb/Scripts/Stores/SelfDescriptionStore.ts index 7d3f6df351..8839ce690b 100644 --- a/VocaDbWeb/Scripts/Stores/SelfDescriptionStore.ts +++ b/VocaDbWeb/Scripts/Stores/SelfDescriptionStore.ts @@ -1,6 +1,9 @@ import { ArtistApiContract } from '@/DataContracts/Artist/ArtistApiContract'; import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; -import { ArtistRepository } from '@/Repositories/ArtistRepository'; +import { + ArtistOptionalField, + ArtistRepository, +} from '@/Repositories/ArtistRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { BasicEntryLinkStore } from '@/Stores/BasicEntryLinkStore'; import { action, makeObservable, observable, runInAction } from 'mobx'; @@ -26,7 +29,7 @@ export class SelfDescriptionStore { this.author = new BasicEntryLinkStore((artistId) => artistRepo.getOneWithComponents({ id: artistId, - fields: 'MainPicture', + fields: [ArtistOptionalField.MainPicture], lang: values.languagePreference, }), ); diff --git a/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts b/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts index 9342508528..cef0208958 100644 --- a/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts +++ b/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts @@ -41,7 +41,7 @@ export class PlayListRepositoryForRatedSongsAdapter ): Promise> { return this.userRepo .getRatedSongsList({ - fields: 'MainPicture' /* TODO: enum */, + fields: [SongOptionalField.MainPicture], lang: lang, paging: paging, pvServices: pvServices, diff --git a/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForSongsAdapter.ts b/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForSongsAdapter.ts index 9e68511f84..21d7d6d0c7 100644 --- a/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForSongsAdapter.ts +++ b/VocaDbWeb/Scripts/Stores/Song/PlayList/PlayListRepositoryForSongsAdapter.ts @@ -35,7 +35,7 @@ export interface ISongsAdapterStore { minScore?: number; onlyRatedSongs: boolean; parentVersion: BasicEntryLinkStore; - fields: string; + fields: SongOptionalField[]; draftsOnly: boolean; advancedFilters: AdvancedSearchFilters; } diff --git a/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts b/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts index 023416b704..4de293b36a 100644 --- a/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts +++ b/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts @@ -11,7 +11,10 @@ import { LoginManager } from '@/Models/LoginManager'; import { SongVoteRating } from '@/Models/SongVoteRating'; import { SongType } from '@/Models/Songs/SongType'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { UserRepository } from '@/Repositories/UserRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { HttpClient } from '@/Shared/HttpClient'; @@ -270,7 +273,7 @@ export class SongDetailsStore { songRepo .getOneWithComponents({ id: this.id, - fields: 'Artists', + fields: [SongOptionalField.Artists], lang: values.languagePreference, }) .then((result) => { @@ -363,7 +366,7 @@ export class SongDetailsStore { songRepo .getOneWithComponents({ id: id, - fields: 'None', + fields: [], lang: this.values.languagePreference, }) .then((song) => { diff --git a/VocaDbWeb/Scripts/Stores/SongList/SongListsBaseStore.ts b/VocaDbWeb/Scripts/Stores/SongList/SongListsBaseStore.ts index ac78c2f525..eae6e9c181 100644 --- a/VocaDbWeb/Scripts/Stores/SongList/SongListsBaseStore.ts +++ b/VocaDbWeb/Scripts/Stores/SongList/SongListsBaseStore.ts @@ -1,5 +1,6 @@ import { SongListContract } from '@/DataContracts/Song/SongListContract'; import { TagBaseContract } from '@/DataContracts/Tag/TagBaseContract'; +import { SongListOptionalField } from '@/Repositories/SongListRepository'; import { TagRepository } from '@/Repositories/TagRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { PagedItemsStore } from '@/Stores/PagedItemsStore'; @@ -62,8 +63,10 @@ export abstract class SongListsBaseStore extends PagedItemsStore { diff --git a/VocaDbWeb/Scripts/Tag/Edit.ts b/VocaDbWeb/Scripts/Tag/Edit.ts index 512ebb8db9..ae36f0e7a9 100644 --- a/VocaDbWeb/Scripts/Tag/Edit.ts +++ b/VocaDbWeb/Scripts/Tag/Edit.ts @@ -1,4 +1,4 @@ -import { TagRepository } from '@/Repositories/TagRepository'; +import { TagOptionalField, TagRepository } from '@/Repositories/TagRepository'; import { UserRepository } from '@/Repositories/UserRepository'; import { HttpClient } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; @@ -24,8 +24,14 @@ export const TagEdit = (model: { id: number }): void => { tagRepo .getById({ id: model.id, - fields: - 'AliasedTo,TranslatedDescription,Names,Parent,RelatedTags,WebLinks', + fields: [ + TagOptionalField.AliasedTo, + TagOptionalField.TranslatedDescription, + TagOptionalField.Names, + TagOptionalField.Parent, + TagOptionalField.RelatedTags, + TagOptionalField.WebLinks, + ], lang: vdb.values.languagePreference, }) .then(function (contract) { diff --git a/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts b/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts index d997d00bd5..a6a12f0e36 100644 --- a/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts +++ b/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts @@ -2,7 +2,10 @@ import { NewSongCheckResultContract } from '@/DataContracts/NewSongCheckResultCo import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import { SongListBaseContract } from '@/DataContracts/SongListBaseContract'; import { ContentLanguagePreference } from '@/Models/Globalization/ContentLanguagePreference'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { HttpClient } from '@/Shared/HttpClient'; import { FakePromise } from '@/Tests/TestSupport/FakePromise'; import _ from 'lodash'; @@ -71,7 +74,7 @@ export class FakeSongRepository extends SongRepository { lang, }: { id: number; - fields: string; + fields: SongOptionalField[]; lang: ContentLanguagePreference; }): Promise => { return FakePromise.resolve(this.song); diff --git a/VocaDbWeb/Scripts/ViewModels/Album/AlbumEditViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Album/AlbumEditViewModel.ts index 34cfdf6a70..363a2e7981 100644 --- a/VocaDbWeb/Scripts/ViewModels/Album/AlbumEditViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Album/AlbumEditViewModel.ts @@ -12,7 +12,10 @@ import { AlbumRepository } from '@/Repositories/AlbumRepository'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; import { PVRepository } from '@/Repositories/PVRepository'; import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { UserRepository } from '@/Repositories/UserRepository'; import { IDialogService } from '@/Shared/DialogService'; import { GlobalValues } from '@/Shared/GlobalValues'; @@ -336,7 +339,10 @@ export class AlbumEditViewModel { songRepository .getOneWithComponents({ id: songId, - fields: 'AdditionalNames,Artists', + fields: [ + SongOptionalField.AdditionalNames, + SongOptionalField.Artists, + ], lang: values.languagePreference, }) .then((song) => { diff --git a/VocaDbWeb/Scripts/ViewModels/ReleaseEvent/ReleaseEventDetailsViewModel.ts b/VocaDbWeb/Scripts/ViewModels/ReleaseEvent/ReleaseEventDetailsViewModel.ts index f03acea3a2..bd42fb608e 100644 --- a/VocaDbWeb/Scripts/ViewModels/ReleaseEvent/ReleaseEventDetailsViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/ReleaseEvent/ReleaseEventDetailsViewModel.ts @@ -6,7 +6,10 @@ import { EntryType } from '@/Models/EntryType'; import { UserEventRelationshipType } from '@/Models/Users/UserEventRelationshipType'; import { CommentRepository } from '@/Repositories/CommentRepository'; import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; -import { UserRepository } from '@/Repositories/UserRepository'; +import { + UserOptionalField, + UserRepository, +} from '@/Repositories/UserRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { HttpClient } from '@/Shared/HttpClient'; import { ui } from '@/Shared/MessagesTyped'; @@ -118,7 +121,10 @@ export class ReleaseEventDetailsViewModel { }); this.eventAssociationType(UserEventRelationshipType.Attending); this.userRepo - .getOne({ id: this.values.loggedUserId, fields: 'MainPicture' }) + .getOne({ + id: this.values.loggedUserId, + fields: [UserOptionalField.MainPicture], + }) .then((user) => { this.usersAttending.push(user); }); diff --git a/VocaDbWeb/Scripts/ViewModels/Search/ArtistSearchViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Search/ArtistSearchViewModel.ts index 6b23e75190..5e2aa4b4bd 100644 --- a/VocaDbWeb/Scripts/ViewModels/Search/ArtistSearchViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Search/ArtistSearchViewModel.ts @@ -3,7 +3,10 @@ import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; import { PartialFindResultContract } from '@/DataContracts/PartialFindResultContract'; import { ArtistHelper } from '@/Helpers/ArtistHelper'; import { ArtistType } from '@/Models/Artists/ArtistType'; -import { ArtistRepository } from '@/Repositories/ArtistRepository'; +import { + ArtistOptionalField, + ArtistRepository, +} from '@/Repositories/ArtistRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { SearchCategoryBaseViewModel } from '@/ViewModels/Search/SearchCategoryBaseViewModel'; import { SearchViewModel } from '@/ViewModels/Search/SearchViewModel'; @@ -76,7 +79,11 @@ export class ArtistSearchViewModel extends SearchCategoryBaseViewModel this.searchViewModel.showTags() - ? 'AdditionalNames,MainPicture,Tags' - : 'AdditionalNames,MainPicture', + ? [ + ArtistOptionalField.AdditionalNames, + ArtistOptionalField.MainPicture, + ArtistOptionalField.Tags, + ] + : [ArtistOptionalField.AdditionalNames, ArtistOptionalField.MainPicture], ); } diff --git a/VocaDbWeb/Scripts/ViewModels/Search/EventSearchViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Search/EventSearchViewModel.ts index 68b83660e0..6736e343df 100644 --- a/VocaDbWeb/Scripts/ViewModels/Search/EventSearchViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Search/EventSearchViewModel.ts @@ -1,7 +1,10 @@ import { PartialFindResultContract } from '@/DataContracts/PartialFindResultContract'; import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; -import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; +import { + ReleaseEventOptionalField, + ReleaseEventRepository, +} from '@/Repositories/ReleaseEventRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { ArtistFilters } from '@/ViewModels/Search/ArtistFilters'; import { SearchCategoryBaseViewModel } from '@/ViewModels/Search/SearchCategoryBaseViewModel'; @@ -85,8 +88,19 @@ export class EventSearchViewModel extends SearchCategoryBaseViewModel this.searchViewModel.showTags() - ? 'AdditionalNames,MainPicture,Series,Venue,Tags' - : 'AdditionalNames,MainPicture,Series,Venue', + ? [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ReleaseEventOptionalField.Venue, + ReleaseEventOptionalField.Tags, + ] + : [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ReleaseEventOptionalField.Venue, + ], ); public getCategoryName = (event: ReleaseEventContract): string => { diff --git a/VocaDbWeb/Scripts/ViewModels/Search/SongSearchViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Search/SongSearchViewModel.ts index 14632d1dd8..beee5456cc 100644 --- a/VocaDbWeb/Scripts/ViewModels/Search/SongSearchViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Search/SongSearchViewModel.ts @@ -9,7 +9,10 @@ import { SongType } from '@/Models/Songs/SongType'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; import { ReleaseEventRepository } from '@/Repositories/ReleaseEventRepository'; import { ResourceRepository } from '@/Repositories/ResourceRepository'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { UserRepository } from '@/Repositories/UserRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { ui } from '@/Shared/MessagesTyped'; @@ -328,10 +331,14 @@ export class SongSearchViewModel extends SearchCategoryBaseViewModel(() => + public fields = ko.computed(() => this.showTags() - ? 'AdditionalNames,ThumbUrl,Tags' - : 'AdditionalNames,ThumbUrl', + ? [ + SongOptionalField.AdditionalNames, + SongOptionalField.ThumbUrl, + SongOptionalField.Tags, + ] + : [SongOptionalField.AdditionalNames, SongOptionalField.ThumbUrl], ); public getPVServiceIcons = ( diff --git a/VocaDbWeb/Scripts/ViewModels/Search/TagFilters.ts b/VocaDbWeb/Scripts/ViewModels/Search/TagFilters.ts index 0f6df723c5..028c10ec66 100644 --- a/VocaDbWeb/Scripts/ViewModels/Search/TagFilters.ts +++ b/VocaDbWeb/Scripts/ViewModels/Search/TagFilters.ts @@ -40,7 +40,6 @@ export class TagFilters { this.tagRepo .getById({ id: selectedTagId, - fields: undefined, lang: this.values.languagePreference, }) .then((tag) => { diff --git a/VocaDbWeb/Scripts/ViewModels/Search/TagSearchViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Search/TagSearchViewModel.ts index b3bb534d2d..a2e2e77a9c 100644 --- a/VocaDbWeb/Scripts/ViewModels/Search/TagSearchViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Search/TagSearchViewModel.ts @@ -1,6 +1,6 @@ import { PartialFindResultContract } from '@/DataContracts/PartialFindResultContract'; import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; -import { TagRepository } from '@/Repositories/TagRepository'; +import { TagOptionalField, TagRepository } from '@/Repositories/TagRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { SearchCategoryBaseViewModel } from '@/ViewModels/Search/SearchCategoryBaseViewModel'; import { SearchViewModel } from '@/ViewModels/Search/SearchViewModel'; @@ -35,7 +35,10 @@ export class TagSearchViewModel extends SearchCategoryBaseViewModel artistRepo.getOneWithComponents({ id: artistId, - fields: 'MainPicture', + fields: [ArtistOptionalField.MainPicture], lang: values.languagePreference, }), ); diff --git a/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts b/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts index 64e40809e0..ff188369a5 100644 --- a/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts +++ b/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForRatedSongsAdapter.ts @@ -37,7 +37,7 @@ export class PlayListRepositoryForRatedSongsAdapter ): Promise> => this.userRepo .getRatedSongsList({ - fields: 'ThumbUrl', + fields: [SongOptionalField.ThumbUrl], lang: lang, paging: paging, pvServices: pvServices, diff --git a/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForSongsAdapter.ts b/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForSongsAdapter.ts index ba78fa91e1..aa6e83c216 100644 --- a/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForSongsAdapter.ts +++ b/VocaDbWeb/Scripts/ViewModels/Song/PlayList/PlayListRepositoryForSongsAdapter.ts @@ -37,7 +37,7 @@ export class PlayListRepositoryForSongsAdapter implements IPlayListRepository { private onlyRatedSongs: Observable, private userCollectionId: number, private parentVersionId: Computed, - private fields: Computed, + private fields: Computed, private draftsOnly: Observable, private advancedFilters: ObservableArray, ) {} diff --git a/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts index 270ef82e1c..7dc0157169 100644 --- a/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts @@ -12,7 +12,10 @@ import { EntryType } from '@/Models/EntryType'; import { SongVoteRating } from '@/Models/SongVoteRating'; import { SongType } from '@/Models/Songs/SongType'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { UserRepository } from '@/Repositories/UserRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { HttpClient } from '@/Shared/HttpClient'; @@ -131,7 +134,7 @@ export class SongDetailsViewModel { repo .getOneWithComponents({ id: id, - fields: 'None', + fields: [], lang: this.values.languagePreference, }) .then((song) => { @@ -245,7 +248,7 @@ export class SongDetailsViewModel { repository .getOneWithComponents({ id: this.id, - fields: 'Artists', + fields: [SongOptionalField.Artists], lang: values.languagePreference, }) .then((result) => { diff --git a/VocaDbWeb/Scripts/ViewModels/SongList/SongListsBaseViewModel.ts b/VocaDbWeb/Scripts/ViewModels/SongList/SongListsBaseViewModel.ts index bfa895d276..f0b61a8112 100644 --- a/VocaDbWeb/Scripts/ViewModels/SongList/SongListsBaseViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/SongList/SongListsBaseViewModel.ts @@ -2,6 +2,7 @@ import { ResourcesContract } from '@/DataContracts/ResourcesContract'; import { SongListContract } from '@/DataContracts/Song/SongListContract'; import { TagBaseContract } from '@/DataContracts/Tag/TagBaseContract'; import { ResourceRepository } from '@/Repositories/ResourceRepository'; +import { SongListOptionalField } from '@/Repositories/SongListRepository'; import { TagRepository } from '@/Repositories/TagRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { PagedItemsViewModel } from '@/ViewModels/PagedItemsViewModel'; @@ -88,6 +89,8 @@ export class SongListsBaseViewModel extends PagedItemsViewModel { - return 'MainPicture' + (this.showTags() ? ',Tags' : ''); + return this.showTags() + ? [SongListOptionalField.MainPicture, SongListOptionalField.Tags] + : [SongListOptionalField.MainPicture]; }); } diff --git a/VocaDbWeb/Scripts/ViewModels/Tag/TagMergeViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Tag/TagMergeViewModel.ts index cbe181ec79..a11c78c5e5 100644 --- a/VocaDbWeb/Scripts/ViewModels/Tag/TagMergeViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Tag/TagMergeViewModel.ts @@ -8,7 +8,7 @@ import ko from 'knockout'; export class TagMergeViewModel { public constructor(tagRepo: TagRepository, private base: TagBaseContract) { this.target = new BasicEntryLinkViewModel(null!, (id) => - tagRepo.getById({ id: id, fields: undefined, lang: undefined }), + tagRepo.getById({ id: id, lang: undefined }), ); ko.computed(() => { diff --git a/VocaDbWeb/Scripts/ViewModels/User/ListUsersViewModel.ts b/VocaDbWeb/Scripts/ViewModels/User/ListUsersViewModel.ts index 9d209f3512..976d0ab5e6 100644 --- a/VocaDbWeb/Scripts/ViewModels/User/ListUsersViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/User/ListUsersViewModel.ts @@ -1,7 +1,10 @@ import { UserApiContract } from '@/DataContracts/User/UserApiContract'; import { ResourcesManager } from '@/Models/ResourcesManager'; import { ResourceRepository } from '@/Repositories/ResourceRepository'; -import { UserRepository } from '@/Repositories/UserRepository'; +import { + UserOptionalField, + UserRepository, +} from '@/Repositories/UserRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; import { ServerSidePagingViewModel } from '@/ViewModels/ServerSidePagingViewModel'; import ko, { Observable } from 'knockout'; @@ -75,7 +78,7 @@ export class ListUsersViewModel { onlyVerified: this.onlyVerifiedArtists(), knowsLanguage: this.knowsLanguage(), nameMatchMode: 'Auto', - fields: 'MainPicture', + fields: [UserOptionalField.MainPicture], }) .then((result) => { this.pauseNotifications = false; diff --git a/VocaDbWeb/Scripts/ViewModels/User/RatedSongsSearchViewModel.ts b/VocaDbWeb/Scripts/ViewModels/User/RatedSongsSearchViewModel.ts index 826ea5034b..2b45962b39 100644 --- a/VocaDbWeb/Scripts/ViewModels/User/RatedSongsSearchViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/User/RatedSongsSearchViewModel.ts @@ -6,7 +6,10 @@ import { RatedSongForUserForApiContract } from '@/DataContracts/User/RatedSongFo import { PVServiceIcons } from '@/Models/PVServiceIcons'; import { ArtistRepository } from '@/Repositories/ArtistRepository'; import { ResourceRepository } from '@/Repositories/ResourceRepository'; -import { SongRepository } from '@/Repositories/SongRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; import { TagRepository } from '@/Repositories/TagRepository'; import { UserRepository } from '@/Repositories/UserRepository'; import { GlobalValues } from '@/Shared/GlobalValues'; @@ -135,7 +138,13 @@ export class RatedSongsSearchViewModel { public viewMode = ko.observable('Details'); public fields = ko.computed(() => { - return 'AdditionalNames,ThumbUrl' + (this.showTags() ? ',Tags' : ''); + return this.showTags() + ? [ + SongOptionalField.AdditionalNames, + SongOptionalField.ThumbUrl, + SongOptionalField.Tags, + ] + : [SongOptionalField.AdditionalNames, SongOptionalField.ThumbUrl]; }); public formatDate = (dateStr: string): string => { From fe0a1cf170a82eca923ddfeb5b24477e0cc0af9a Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Sun, 4 Sep 2022 18:48:41 +1000 Subject: [PATCH 04/15] Create EntryToolTip --- .../KnockoutExtensions/EntryToolTip.tsx | 460 ++++++++++++++++++ .../Components/Shared/AlbumPopupContent.tsx | 51 ++ .../Components/Shared/ArtistPopupContent.tsx | 50 ++ .../Components/Shared/EventPopupContent.tsx | 65 +++ .../Shared/Partials/Album/AlbumLink.tsx | 14 +- .../Shared/Partials/Artist/ArtistLink.tsx | 25 +- .../Shared/Partials/Event/EventLink.tsx | 12 +- .../Shared/Partials/Shared/AlbumThumbItem.tsx | 21 +- .../Shared/Partials/Shared/EntryLink.tsx | 17 +- .../Shared/Partials/Shared/ThumbItem.tsx | 93 ++-- .../Shared/Partials/Song/SongLink.tsx | 34 +- .../Shared/Partials/Tag/TagLink.tsx | 27 +- .../Shared/Partials/User/UserLink.tsx | 17 +- .../Components/Shared/SongPopupContent.tsx | 51 ++ .../Components/Shared/TagPopupContent.tsx | 43 ++ .../Components/Shared/UserPopupContent.tsx | 37 ++ .../ReleaseEvents/ReleaseEventContract.ts | 20 +- .../DataContracts/User/UserApiContract.ts | 4 +- .../Repositories/ReleaseEventRepository.ts | 12 +- VocaDbWeb/wwwroot/Content/Site.css | 1 - 20 files changed, 893 insertions(+), 161 deletions(-) create mode 100644 VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx create mode 100644 VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx new file mode 100644 index 0000000000..54703cd3da --- /dev/null +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -0,0 +1,460 @@ +import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; +import { ArtistPopupContent } from '@/Components/Shared/ArtistPopupContent'; +import { EventPopupContent } from '@/Components/Shared/EventPopupContent'; +import { SongPopupContent } from '@/Components/Shared/SongPopupContent'; +import { TagPopupContent } from '@/Components/Shared/TagPopupContent'; +import { UserPopupContent } from '@/Components/Shared/UserPopupContent'; +import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; +import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; +import { EntryRefContract } from '@/DataContracts/EntryRefContract'; +import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; +import { SongContract } from '@/DataContracts/Song/SongContract'; +import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; +import { UserApiContract } from '@/DataContracts/User/UserApiContract'; +import { EntryType } from '@/Models/EntryType'; +import { AlbumRepository } from '@/Repositories/AlbumRepository'; +import { + ArtistOptionalField, + ArtistRepository, +} from '@/Repositories/ArtistRepository'; +import { + ReleaseEventOptionalField, + ReleaseEventRepository, +} from '@/Repositories/ReleaseEventRepository'; +import { + SongOptionalField, + SongRepository, +} from '@/Repositories/SongRepository'; +import { TagOptionalField, TagRepository } from '@/Repositories/TagRepository'; +import { + UserOptionalField, + UserRepository, +} from '@/Repositories/UserRepository'; +import { HttpClient } from '@/Shared/HttpClient'; +import { UrlMapper } from '@/Shared/UrlMapper'; +import React from 'react'; +import Overlay from 'react-overlays/Overlay'; + +const httpClient = new HttpClient(); +const urlMapper = new UrlMapper(vdb.values.baseAddress); + +const albumRepo = new AlbumRepository(httpClient, vdb.values.baseAddress); +const artistRepo = new ArtistRepository(httpClient, vdb.values.baseAddress); +const eventRepo = new ReleaseEventRepository(httpClient, urlMapper); +const songRepo = new SongRepository(httpClient, vdb.values.baseAddress); +const tagRepo = new TagRepository(httpClient, vdb.values.baseAddress); +const userRepo = new UserRepository(httpClient, urlMapper); + +type QTipProps = React.HTMLAttributes; + +const QTip = React.forwardRef( + ({ children, ...props }: QTipProps, ref): React.ReactElement => { + return ( +
+
{children}
+
+ ); + }, +); + +interface AlbumToolTipProps { + id: number; + children?: React.ReactNode; +} + +export const AlbumToolTip = React.memo( + ({ id, children }: AlbumToolTipProps): React.ReactElement => { + const triggerRef = React.useRef(undefined!); + const containerRef = React.useRef(undefined!); + + const [show, setShow] = React.useState(false); + + const [album, setAlbum] = React.useState(); + + React.useEffect(() => { + if (album) return; + + if (!show) return; + + albumRepo + .getOne({ id: id, lang: vdb.values.languagePreference }) + .then((album) => setAlbum(album)); + }, [album, show, id]); + + return ( + + setShow(true)} + onMouseLeave={(): void => setShow(false)} + > + {children} + + {show && album && ( + setShow(false)} + placement="bottom-start" + container={containerRef} + target={triggerRef} + flip + > + {({ props }): React.ReactElement => ( + + + + )} + + )} + + ); + }, +); + +interface ArtistToolTipProps { + id: number; + children?: React.ReactNode; +} + +export const ArtistToolTip = React.memo( + ({ id, children }: ArtistToolTipProps): React.ReactElement => { + const triggerRef = React.useRef(undefined!); + const containerRef = React.useRef(undefined!); + + const [show, setShow] = React.useState(false); + + const [artist, setArtist] = React.useState(); + + React.useEffect(() => { + if (artist) return; + + if (!show) return; + + artistRepo + .getOneWithComponents({ + id: id, + fields: [ + ArtistOptionalField.AdditionalNames, + ArtistOptionalField.MainPicture, + ], + lang: vdb.values.languagePreference, + }) + .then((artist) => setArtist(artist)); + }, [artist, show, id]); + + return ( + + setShow(true)} + onMouseLeave={(): void => setShow(false)} + > + {children} + + {show && artist && ( + setShow(false)} + placement="bottom-start" + container={containerRef} + target={triggerRef} + flip + > + {({ props }): React.ReactElement => ( + + + + )} + + )} + + ); + }, +); + +interface EventToolTipProps { + id: number; + children?: React.ReactNode; +} + +export const EventToolTip = React.memo( + ({ id, children }: EventToolTipProps): React.ReactElement => { + const triggerRef = React.useRef(undefined!); + const containerRef = React.useRef(undefined!); + + const [show, setShow] = React.useState(false); + + const [event, setEvent] = React.useState(); + + React.useEffect(() => { + if (event) return; + + if (!show) return; + + eventRepo + .getOne({ + id: id, + fields: [ + ReleaseEventOptionalField.AdditionalNames, + ReleaseEventOptionalField.Description, + ReleaseEventOptionalField.MainPicture, + ReleaseEventOptionalField.Series, + ], + }) + .then((event) => setEvent(event)); + }, [event, show, id]); + + return ( + + setShow(true)} + onMouseLeave={(): void => setShow(false)} + > + {children} + + {show && event && ( + setShow(false)} + placement="bottom-start" + container={containerRef} + target={triggerRef} + flip + > + {({ props }): React.ReactElement => ( + + + + )} + + )} + + ); + }, +); + +interface SongToolTipProps { + id: number; + children?: React.ReactNode; +} + +export const SongToolTip = React.memo( + ({ id, children }: SongToolTipProps): React.ReactElement => { + const triggerRef = React.useRef(undefined!); + const containerRef = React.useRef(undefined!); + + const [show, setShow] = React.useState(false); + + const [song, setSong] = React.useState(); + + React.useEffect(() => { + if (song) return; + + if (!show) return; + + songRepo + .getOneWithComponents({ + id: id, + fields: [ + SongOptionalField.AdditionalNames, + SongOptionalField.ThumbUrl, + ], + lang: vdb.values.languagePreference, + }) + .then((song) => setSong(song)); + }, [song, show, id]); + + return ( + + setShow(true)} + onMouseLeave={(): void => setShow(false)} + > + {children} + + {show && song && ( + setShow(false)} + placement="bottom-start" + container={containerRef} + target={triggerRef} + flip + > + {({ props }): React.ReactElement => ( + + + + )} + + )} + + ); + }, +); + +interface TagToolTipProps { + id: number; + children?: React.ReactNode; +} + +export const TagToolTip = React.memo( + ({ id, children }: TagToolTipProps): React.ReactElement => { + const triggerRef = React.useRef(undefined!); + const containerRef = React.useRef(undefined!); + + const [show, setShow] = React.useState(false); + + const [tag, setTag] = React.useState(); + + React.useEffect(() => { + if (tag) return; + + if (!show) return; + + tagRepo + .getById({ + id: id, + fields: [ + TagOptionalField.AdditionalNames, + TagOptionalField.Description, + TagOptionalField.MainPicture, + ], + lang: vdb.values.languagePreference, + }) + .then((tag) => setTag(tag)); + }, [tag, show, id]); + + return ( + + setShow(true)} + onMouseLeave={(): void => setShow(false)} + > + {children} + + {show && tag && ( + setShow(false)} + placement="bottom-start" + container={containerRef} + target={triggerRef} + flip + > + {({ props }): React.ReactElement => ( + + + + )} + + )} + + ); + }, +); + +interface UserToolTipProps { + id: number; + children?: React.ReactNode; +} + +export const UserToolTip = React.memo( + ({ id, children }: UserToolTipProps): React.ReactElement => { + const triggerRef = React.useRef(undefined!); + const containerRef = React.useRef(undefined!); + + const [show, setShow] = React.useState(false); + + const [user, setUser] = React.useState(); + + React.useEffect(() => { + if (user) return; + + if (!show) return; + + userRepo + .getOne({ id: id, fields: [UserOptionalField.MainPicture] }) + .then((user) => setUser(user)); + }, [user, show, id]); + + return ( + + setShow(true)} + onMouseLeave={(): void => setShow(false)} + > + {children} + + {show && user && ( + setShow(false)} + placement="bottom-start" + container={containerRef} + target={triggerRef} + flip + > + {({ props }): React.ReactElement => ( + + + + )} + + )} + + ); + }, +); + +interface EntryToolTipProps { + entry: EntryRefContract; + children?: React.ReactNode; +} + +export const EntryToolTip = ({ + entry, + children, +}: EntryToolTipProps): React.ReactElement => { + switch (entry.entryType) { + case EntryType[EntryType.Album]: + return {children}; + + case EntryType[EntryType.Artist]: + return {children}; + + case EntryType[EntryType.ReleaseEvent]: + return {children}; + + case EntryType[EntryType.Song]: + return {children}; + + case EntryType[EntryType.Tag]: + return {children}; + + case EntryType[EntryType.User]: + return {children}; + + default: + return <>; + } +}; diff --git a/VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx new file mode 100644 index 0000000000..6a12157839 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/AlbumPopupContent.tsx @@ -0,0 +1,51 @@ +import { StarsMetaSpan } from '@/Components/Shared/Partials/Shared/StarsMetaSpan'; +import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; +import { AlbumType } from '@/Models/Albums/AlbumType'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface AlbumPopupContentProps { + album: AlbumContract; +} + +export const AlbumPopupContent = React.memo( + ({ album }: AlbumPopupContentProps): React.ReactElement => { + const { t } = useTranslation([ + 'HelperRes', + 'ViewRes.Album', + 'VocaDb.Model.Resources.Albums', + ]); + + return ( + <> +

+ {album.name} + {album.additionalNames && ( + <> +
+ {album.additionalNames} + + )} +

+

+ {album.artistString} +
+ {album.discType !== AlbumType.Unknown && + t(`VocaDb.Model.Resources.Albums:DiscTypeNames.${album.discType}`)} +

+ {!album.releaseDate.isEmpty && ( +

+ {t('HelperRes:AlbumHelpers.Released')} {album.releaseDate.formatted} + {album.releaseEvent && <> ({album.releaseEvent.name})} +

+ )} + {album.ratingCount > 0 && ( + <> + ( + {album.ratingCount} {t('ViewRes.Album:Details.Ratings')}) + + )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx new file mode 100644 index 0000000000..38806162aa --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/ArtistPopupContent.tsx @@ -0,0 +1,50 @@ +import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; +import { UrlHelper } from '@/Helpers/UrlHelper'; +import { ImageSize } from '@/Models/Images/ImageSize'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ArtistPopupContentProps { + artist: ArtistContract; +} + +export const ArtistPopupContent = React.memo( + ({ artist }: ArtistPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['ViewRes.Artist', 'VocaDb.Model.Resources']); + + return ( + <> +
+ Thumb +
+ +

+ {artist.name} + + {artist.additionalNames && ( + <> +
+ {artist.additionalNames} + + )} +

+ +

+ {t(`VocaDb.Model.Resources:ArtistTypeNames.${artist.artistType}`)} +

+ + {artist.releaseDate && ( +

+ {t('ViewRes.Artist:Details.ReleaseDate')}{' '} + {moment(artist.releaseDate).format('l')} +

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx new file mode 100644 index 0000000000..f834b2117f --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/EventPopupContent.tsx @@ -0,0 +1,65 @@ +import { Markdown } from '@/Components/KnockoutExtensions/Markdown'; +import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; +import { EventCategory } from '@/Models/Events/EventCategory'; +import _ from 'lodash'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface EventPopupContentProps { + event: ReleaseEventContract; +} + +export const EventPopupContent = React.memo( + ({ event }: EventPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['VocaDb.Web.Resources.Domain.ReleaseEvents']); + + const category = event.series ? event.series.category : event.category; + + return ( + <> + {event.mainPicture && event.mainPicture.urlSmallThumb && ( +
+ Thumb +
+ )} + + {event.name} + + {event.additionalNames &&

{event.additionalNames}

} + + {category !== EventCategory.Unspecified && + category !== EventCategory.Other && ( +

+ {t( + `VocaDb.Web.Resources.Domain.ReleaseEvents:EventCategoryNames.${category}`, + )} +

+ )} + + {event.description && ( +

+ + {_.truncate(event.description, { + length: 100, + })} + +

+ )} + + {event.date && ( +

+ {t('ViewRes.Event:Details:OccurrenceDate')}{' '} + {moment(event.date).format('l')} + {event.endDate && ` - ${moment(event.endDate).format('l')}`} +

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx index 923bc0c2f0..c26a6d3648 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx @@ -1,3 +1,4 @@ +import { AlbumToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { AlbumForApiContract } from '@/DataContracts/Album/AlbumForApiContract'; import { EntryType } from '@/Models/EntryType'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; @@ -30,16 +31,9 @@ export const AlbumLink = ({ tooltip = false, }: AlbumLinkProps): React.ReactElement => { return tooltip ? ( - /* - {album.name} - */ - + + + ) : ( ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx index f27d59404c..8243e25ae2 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Artist/ArtistLink.tsx @@ -1,11 +1,12 @@ +import { ArtistToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { ArtistTypeLabel } from '@/Components/Shared/Partials/Artist/ArtistTypeLabel'; import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; import { EntryType } from '@/Models/EntryType'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; -interface ArtistLinkBaseProps { +interface ArtistLinkBaseProps extends Omit { artist: ArtistContract; children?: React.ReactNode; } @@ -13,11 +14,12 @@ interface ArtistLinkBaseProps { const ArtistLinkBase = ({ artist, children, + ...props }: ArtistLinkBaseProps): React.ReactElement => { return ( {children ?? artist.name} @@ -45,18 +47,13 @@ export const ArtistLink = ({ {typeLabel && } {typeLabel && ' '} {tooltip ? ( - /* - {name ?? artist.name} - */ - {name} + + {name} + ) : ( - {name} + + {name} + )} {releaseYear && artist.releaseDate && ( <> diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx index 5de23c36bb..cf373d9fc9 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Event/EventLink.tsx @@ -1,3 +1,4 @@ +import { EventToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; import { EntryType } from '@/Models/EntryType'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; @@ -28,14 +29,9 @@ export const EventLink = ({ tooltip, }: EventLinkProps): React.ReactElement => { return tooltip ? ( - /* - {event.name} - */ - + + + ) : ( ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx index c37a75ba45..f8a2b960bf 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/AlbumThumbItem.tsx @@ -42,9 +42,27 @@ export const AlbumThumbItem = React.memo( [album, playQueue], ); + const thumbItemRef = React.useRef(undefined!); + const [hover, setHover] = React.useState(false); const [isOpen, setIsOpen] = React.useState(false); + // HACK: https://github.com/facebook/react/issues/6807#issuecomment-1240312500. + React.useLayoutEffect(() => { + const handleMouseEnter = (): void => setHover(true); + const handleMouseLeave = (): void => setHover(false); + + const thumbItem = thumbItemRef.current; + + thumbItem.addEventListener('mouseenter', handleMouseEnter); + thumbItem.addEventListener('mouseleave', handleMouseLeave); + + return (): void => { + thumbItem.removeEventListener('mouseenter', handleMouseEnter); + thumbItem.removeEventListener('mouseleave', handleMouseLeave); + }; + }, []); + return ( setHover(true)} - onMouseLeave={(): void => setHover(false)} + ref={thumbItemRef} > {(hover || isOpen) && ( { return tooltip ? ( - /* - {children ?? entry.defaultName} - */ - - {children} - + + + {children} + + ) : ( {children} diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx index 9695ce8f1c..d414021d0d 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ThumbItem.tsx @@ -16,49 +16,54 @@ interface ThumbItemProps { export const ThumbItem: BsPrefixRefForwardingComponent/* TODO */ < 'a', ThumbItemProps -> = ({ - linkAs: LinkComponent = 'a', - linkProps, - thumbUrl, - caption, - entry, - tooltip, - children, - ...props -}: ThumbItemProps): React.ReactElement => { - return ( -
-
-
- {entry ? ( - - Preview - - ) : ( - - Preview - - )} -
+> = React.forwardRef( + ( + { + linkAs: LinkComponent = 'a', + linkProps, + thumbUrl, + caption, + entry, + tooltip, + children, + ...props + }: ThumbItemProps, + ref, + ): React.ReactElement => { + return ( +
+
+
+ {entry ? ( + + Preview + + ) : ( + + Preview + + )} +
- {children} + {children} +
+ {caption &&

{caption}

}
- {caption &&

{caption}

} -
- ); -}; + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx index 70454db95f..bd33fdf04d 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx @@ -1,27 +1,26 @@ +import { SongToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import qs from 'qs'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; -interface SongLinkBaseProps { +interface SongLinkBaseProps extends Omit { song: SongApiContract; albumId?: number; - target?: string; } const SongLinkBase = ({ song, albumId, - target, + ...props }: SongLinkBaseProps): React.ReactElement => { return ( {song.name} @@ -45,21 +44,16 @@ export const SongLink = React.memo( target, }: SongLinkProps): React.ReactElement => { return tooltip ? ( - /* - {song.name} - */ - + + + ) : ( - + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx index 66d374ad56..07cce58d93 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Tag/TagLink.tsx @@ -1,9 +1,10 @@ +import { TagToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { TagBaseContract } from '@/DataContracts/Tag/TagBaseContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; -interface TagLinkBaseProps { +interface TagLinkBaseProps extends Omit { tag: TagBaseContract; children?: React.ReactNode; } @@ -11,12 +12,10 @@ interface TagLinkBaseProps { const TagLinkBase = ({ tag, children, + ...props }: TagLinkBaseProps): React.ReactElement => { return ( - + {children ?? tag.name} ); @@ -31,17 +30,13 @@ interface TagLinkProps { export const TagLink = React.memo( ({ tag, children, tooltip }: TagLinkProps): React.ReactElement => { return tooltip ? ( - /* - {children ?? tag.name} - */ - {children} + + {children} + ) : ( - {children} + + {children} + ); }, ); diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx index d42e9c70cf..0bb6d783c2 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/User/UserLink.tsx @@ -1,3 +1,4 @@ +import { UserToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { UserBaseContract } from '@/DataContracts/User/UserBaseContract'; import { EntryUrlMapper } from '@/Shared/EntryUrlMapper'; import React from 'react'; @@ -34,17 +35,11 @@ export const UserLink = React.memo( ...props }: UserLinkProps): React.ReactElement => { return tooltip ? ( - /* - {children ?? user.name} - */ - - {children} - + + + {children} + + ) : ( {children} diff --git a/VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx new file mode 100644 index 0000000000..4abc1a365e --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/SongPopupContent.tsx @@ -0,0 +1,51 @@ +import { SongContract } from '@/DataContracts/Song/SongContract'; +import { DateTimeHelper } from '@/Helpers/DateTimeHelper'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface SongPopupContentProps { + song: SongContract; +} + +export const SongPopupContent = React.memo( + ({ song }: SongPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['ViewRes', 'VocaDb.Model.Resources.Songs']); + + return ( + <> + {song.thumbUrl && ( +
+ Thumb +
+ )} + + {song.name} + + {song.additionalNames &&

{song.additionalNames}

} + +

+ {song.artistString} +
+ {t(`VocaDb.Model.Resources.Songs:SongTypeNames.${song.songType}`)} +

+ + {song.publishDate && ( +

+ {t('ViewRes:EntryDetails.PublishDate')}{' '} + {moment(song.publishDate).format('l')} +

+ )} + + {song.lengthSeconds > 0 && ( +

{DateTimeHelper.formatFromSeconds(song.lengthSeconds)}

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx new file mode 100644 index 0000000000..0d8d34e964 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/TagPopupContent.tsx @@ -0,0 +1,43 @@ +import { Markdown } from '@/Components/KnockoutExtensions/Markdown'; +import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; +import _ from 'lodash'; +import React from 'react'; + +interface TagPopupContentProps { + tag: TagApiContract; +} + +export const TagPopupContent = React.memo( + ({ tag }: TagPopupContentProps): React.ReactElement => { + return ( + <> + {tag.mainPicture && tag.mainPicture.urlSmallThumb && ( +
+ Thumb +
+ )} + + {tag.name} + + {tag.additionalNames &&

{tag.additionalNames}

} + + {tag.categoryName &&

{tag.categoryName}

} + + {tag.description && ( +

+ + {_.truncate(tag.description, { + length: 100, + })} + +

+ )} + + ); + }, +); diff --git a/VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx new file mode 100644 index 0000000000..1824885672 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/UserPopupContent.tsx @@ -0,0 +1,37 @@ +import { ProfileIcon } from '@/Components/Shared/Partials/User/ProfileIcon'; +import { UserApiContract } from '@/DataContracts/User/UserApiContract'; +import moment from 'moment'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface UserPopupContentProps { + user: UserApiContract; +} + +export const UserPopupContent = React.memo( + ({ user }: UserPopupContentProps): React.ReactElement => { + const { t } = useTranslation(['Resources']); + + return ( + <> + {user.mainPicture && user.mainPicture.urlThumb && ( +
+ +
+ )} + + {user.name} + +

{t(`Resources:UserGroupNames.${user.groupId}`)}

+ + {user.verifiedArtist && ( +

{t('ViewRes.User:Details.VerifiedAccount')}

+ )} + +

+ Joined{/* TODO: localize */} {moment(user.memberSince).format('l')} +

+ + ); + }, +); diff --git a/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts b/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts index 5b1d7b9250..383046f906 100644 --- a/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts +++ b/VocaDbWeb/Scripts/DataContracts/ReleaseEvents/ReleaseEventContract.ts @@ -12,42 +12,24 @@ import { EventCategory } from '@/Models/Events/EventCategory'; // Matches ReleaseEventForApiContract export interface ReleaseEventContract { additionalNames?: string; - artists: ArtistForEventContract[]; - category: EventCategory; - date?: string; - defaultNameLanguage: string; - + description?: string; endDate?: string; - id: number; - mainPicture?: EntryThumbContract; - name: string; - names?: LocalizedStringWithIdContract[]; - pvs?: PVContract[]; - series?: EventSeriesContract; - songList?: SongListBaseContract; - status?: string; - tags?: TagUsageForApiContract[]; - urlSlug?: string; - venue?: VenueForApiContract; - version?: number; - venueName?: string; - webLinks: WebLinkContract[]; } diff --git a/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts b/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts index c95fc4592e..d3f5b2a262 100644 --- a/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts +++ b/VocaDbWeb/Scripts/DataContracts/User/UserApiContract.ts @@ -4,10 +4,8 @@ import { UserGroup } from '@/Models/Users/UserGroup'; export interface UserApiContract extends UserBaseContract { active?: boolean; - groupId?: UserGroup; - mainPicture?: EntryThumbContract; - memberSince?: Date; + verifiedArtist?: boolean; } diff --git a/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts b/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts index 62567cc154..0c0b3cb13b 100644 --- a/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/ReleaseEventRepository.ts @@ -131,9 +131,17 @@ export class ReleaseEventRepository extends BaseRepository { ); }; - public getOne = ({ id }: { id: number }): Promise => { + public getOne = ({ + id, + fields, + }: { + id: number; + fields?: ReleaseEventOptionalField[]; + }): Promise => { var url = functions.mergeUrls(this.baseUrl, `/api/releaseEvents/${id}`); - return this.httpClient.get(url); + return this.httpClient.get(url, { + fields: fields?.join(','), + }); }; public getOneSeries = ({ diff --git a/VocaDbWeb/wwwroot/Content/Site.css b/VocaDbWeb/wwwroot/Content/Site.css index 0a7edc4970..654f987be7 100644 --- a/VocaDbWeb/wwwroot/Content/Site.css +++ b/VocaDbWeb/wwwroot/Content/Site.css @@ -132,7 +132,6 @@ div.thumbItem { .smallThumbs { display: flex; flex-wrap: wrap; - overflow: hidden; font-size: 11px; line-height: 1.181818em; margin-left: 0; From 4d7d262850c3566b1742c5df57053e380fbb96ab Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Tue, 13 Sep 2022 19:36:17 +1000 Subject: [PATCH 05/15] Remove Gravatar --- .../Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml | 1 - 1 file changed, 1 deletion(-) diff --git a/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml b/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml index bd33ca1c7e..0ed729d00b 100644 --- a/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml +++ b/VocaDbWeb/Views/Shared/Partials/User/_ProfileIcon_IUserWithEmail.cshtml @@ -4,6 +4,5 @@ @if (Model.User != null && !string.IsNullOrEmpty(Model.User.Email)) {
- @Gravatar.GetHtml(Model.User.Email, Model.Size)
} \ No newline at end of file From b1100e8c32d40ed5d79ce2e0ede8d3db5b2cefd2 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Wed, 14 Sep 2022 23:52:51 +1000 Subject: [PATCH 06/15] Update SongLinkKnockout --- .../KnockoutExtensions/EntryToolTip.tsx | 22 ++++++++++- .../Shared/Partials/Song/SongLink.tsx | 2 - .../Shared/Partials/Song/SongLinkKnockout.tsx | 39 +++++++++---------- .../Scripts/Repositories/SongRepository.ts | 8 ++-- .../Scripts/Stores/Song/SongDetailsStore.ts | 1 - .../Tests/TestSupport/FakeSongRepository.ts | 2 +- .../ViewModels/Song/SongDetailsViewModel.ts | 1 - 7 files changed, 45 insertions(+), 30 deletions(-) diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 54703cd3da..7a43ba2fa2 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -246,13 +246,22 @@ export const EventToolTip = React.memo( }, ); +const allowedDomains = [ + 'http://vocadb.net', + 'https://vocadb.net', + 'http://utaitedb.net', + 'https://utaitedb.net', + 'https://touhoudb.com', +]; + interface SongToolTipProps { id: number; children?: React.ReactNode; + foreignDomain?: string; } export const SongToolTip = React.memo( - ({ id, children }: SongToolTipProps): React.ReactElement => { + ({ id, children, foreignDomain }: SongToolTipProps): React.ReactElement => { const triggerRef = React.useRef(undefined!); const containerRef = React.useRef(undefined!); @@ -265,8 +274,17 @@ export const SongToolTip = React.memo( if (!show) return; + const baseUrl = + foreignDomain && + allowedDomains.some((domain) => + foreignDomain.toLocaleLowerCase().includes(domain), + ) + ? foreignDomain + : undefined; + songRepo .getOneWithComponents({ + baseUrl: baseUrl, id: id, fields: [ SongOptionalField.AdditionalNames, @@ -275,7 +293,7 @@ export const SongToolTip = React.memo( lang: vdb.values.languagePreference, }) .then((song) => setSong(song)); - }, [song, show, id]); + }, [song, show, id, foreignDomain]); return ( diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx index bd33fdf04d..709c6e0da7 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLink.tsx @@ -31,7 +31,6 @@ interface SongLinkProps { song: SongApiContract; albumId?: number; tooltip?: boolean; - toolTipDomain?: string; target?: string; } @@ -40,7 +39,6 @@ export const SongLink = React.memo( song, albumId, tooltip = false, - toolTipDomain, target, }: SongLinkProps): React.ReactElement => { return tooltip ? ( diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx index 2527fd7761..6b39da106c 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Song/SongLinkKnockout.tsx @@ -1,8 +1,10 @@ +import { SongToolTip } from '@/Components/KnockoutExtensions/EntryToolTip'; import { SongLink } from '@/Components/Shared/Partials/Song/SongLink'; import { SongApiContract } from '@/DataContracts/Song/SongApiContract'; import React from 'react'; -interface SongLinkKnockoutBaseProps { +interface SongLinkKnockoutBaseProps + extends React.AnchorHTMLAttributes { song: SongApiContract; extUrl?: string; } @@ -10,9 +12,10 @@ interface SongLinkKnockoutBaseProps { const SongLinkKnockoutBase = ({ song, extUrl, + ...props }: SongLinkKnockoutBaseProps): React.ReactElement => { return ( - + {song.name} ); @@ -36,27 +39,23 @@ export const SongLinkKnockout = React.memo( }: SongLinkKnockoutProps): React.ReactElement => { return extUrl ? ( tooltip ? ( - /* - {song.name} - */ - + + + ) : ( - + ) ) : ( - + ); }, ); diff --git a/VocaDbWeb/Scripts/Repositories/SongRepository.ts b/VocaDbWeb/Scripts/Repositories/SongRepository.ts index 6485050981..67506d891c 100644 --- a/VocaDbWeb/Scripts/Repositories/SongRepository.ts +++ b/VocaDbWeb/Scripts/Repositories/SongRepository.ts @@ -294,17 +294,19 @@ export class SongRepository private getJSON: (relative: string, params: any) => Promise; public getOneWithComponents = ({ + baseUrl, id, fields, lang, }: { + baseUrl?: string; id: number; - fields: SongOptionalField[]; + fields?: SongOptionalField[]; lang: ContentLanguagePreference; }): Promise => { - var url = functions.mergeUrls(this.baseUrl, `/api/songs/${id}`); + var url = functions.mergeUrls(baseUrl ?? this.baseUrl, `/api/songs/${id}`); return this.httpClient.get(url, { - fields: fields.join(','), + fields: fields?.join(','), lang: lang, }); }; diff --git a/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts b/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts index 4de293b36a..b309c175b7 100644 --- a/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts +++ b/VocaDbWeb/Scripts/Stores/Song/SongDetailsStore.ts @@ -366,7 +366,6 @@ export class SongDetailsStore { songRepo .getOneWithComponents({ id: id, - fields: [], lang: this.values.languagePreference, }) .then((song) => { diff --git a/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts b/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts index a6a12f0e36..003bfdc72a 100644 --- a/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts +++ b/VocaDbWeb/Scripts/Tests/TestSupport/FakeSongRepository.ts @@ -74,7 +74,7 @@ export class FakeSongRepository extends SongRepository { lang, }: { id: number; - fields: SongOptionalField[]; + fields?: SongOptionalField[]; lang: ContentLanguagePreference; }): Promise => { return FakePromise.resolve(this.song); diff --git a/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts b/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts index 7dc0157169..65b245f04a 100644 --- a/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts +++ b/VocaDbWeb/Scripts/ViewModels/Song/SongDetailsViewModel.ts @@ -134,7 +134,6 @@ export class SongDetailsViewModel { repo .getOneWithComponents({ id: id, - fields: [], lang: this.values.languagePreference, }) .then((song) => { From 8545d9cf38cd61e90de2ff88ba6d374d9f18d0aa Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:06:00 +1000 Subject: [PATCH 07/15] Create SongWithVotePopupContent --- .../KnockoutExtensions/EntryToolTip.tsx | 19 +++++++++++---- .../Shared/SongWithVotePopupContent.tsx | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 7a43ba2fa2..11b189c4a3 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -1,14 +1,14 @@ import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; import { ArtistPopupContent } from '@/Components/Shared/ArtistPopupContent'; import { EventPopupContent } from '@/Components/Shared/EventPopupContent'; -import { SongPopupContent } from '@/Components/Shared/SongPopupContent'; +import { SongWithVotePopupContent } from '@/Components/Shared/SongWithVotePopupContent'; import { TagPopupContent } from '@/Components/Shared/TagPopupContent'; import { UserPopupContent } from '@/Components/Shared/UserPopupContent'; import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; import { ArtistContract } from '@/DataContracts/Artist/ArtistContract'; import { EntryRefContract } from '@/DataContracts/EntryRefContract'; import { ReleaseEventContract } from '@/DataContracts/ReleaseEvents/ReleaseEventContract'; -import { SongContract } from '@/DataContracts/Song/SongContract'; +import { SongWithPVAndVoteContract } from '@/DataContracts/Song/SongWithPVAndVoteContract'; import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; import { UserApiContract } from '@/DataContracts/User/UserApiContract'; import { EntryType } from '@/Models/EntryType'; @@ -267,7 +267,7 @@ export const SongToolTip = React.memo( const [show, setShow] = React.useState(false); - const [song, setSong] = React.useState(); + const [song, setSong] = React.useState(); React.useEffect(() => { if (song) return; @@ -292,7 +292,16 @@ export const SongToolTip = React.memo( ], lang: vdb.values.languagePreference, }) - .then((song) => setSong(song)); + .then(async (song) => { + const vote = vdb.values.loggedUser + ? await userRepo.getSongRating({ + userId: vdb.values.loggedUser.id, + songId: song.id, + }) + : 'Nothing'; + + setSong({ ...song, pvs: [], vote: vote }); + }); }, [song, show, id, foreignDomain]); return ( @@ -316,7 +325,7 @@ export const SongToolTip = React.memo( > {({ props }): React.ReactElement => ( - + )} diff --git a/VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx new file mode 100644 index 0000000000..f19dbdea37 --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/SongWithVotePopupContent.tsx @@ -0,0 +1,24 @@ +import { SongPopupContent } from '@/Components/Shared/SongPopupContent'; +import { SongWithPVAndVoteContract } from '@/DataContracts/Song/SongWithPVAndVoteContract'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface SongWithVotePopupContentProps { + song: SongWithPVAndVoteContract; +} + +export const SongWithVotePopupContent = ({ + song, +}: SongWithVotePopupContentProps): React.ReactElement => { + const { t } = useTranslation(['Resources']); + + return ( + <> + + + {song.vote !== 'Nothing' && ( +

{t(`Resources:SongVoteRatingNames.${song.vote}`)}

+ )} + + ); +}; From edcca4bcdc5559dfb5b0e4276723ae8285944ff8 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:29:18 +1000 Subject: [PATCH 08/15] Create AlbumWithCoverPopupContent --- .../KnockoutExtensions/EntryToolTip.tsx | 4 +-- .../Shared/AlbumWithCoverPopupContent.tsx | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 11b189c4a3..49a44779a3 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -1,4 +1,4 @@ -import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; +import { AlbumWithCoverPopupContent } from '@/Components/Shared/AlbumWithCoverPopupContent'; import { ArtistPopupContent } from '@/Components/Shared/ArtistPopupContent'; import { EventPopupContent } from '@/Components/Shared/EventPopupContent'; import { SongWithVotePopupContent } from '@/Components/Shared/SongWithVotePopupContent'; @@ -111,7 +111,7 @@ export const AlbumToolTip = React.memo( > {({ props }): React.ReactElement => ( - + )} diff --git a/VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx b/VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx new file mode 100644 index 0000000000..7fa43e3b0f --- /dev/null +++ b/VocaDbWeb/Scripts/Components/Shared/AlbumWithCoverPopupContent.tsx @@ -0,0 +1,26 @@ +import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; +import { AlbumContract } from '@/DataContracts/Album/AlbumContract'; +import { UrlHelper } from '@/Helpers/UrlHelper'; +import { ImageSize } from '@/Models/Images/ImageSize'; +import React from 'react'; + +interface AlbumWithCoverPopupContentProps { + album: AlbumContract; +} + +export const AlbumWithCoverPopupContent = ({ + album, +}: AlbumWithCoverPopupContentProps): React.ReactElement => { + return ( + <> + Cover +
+
+ + + ); +}; From f0d41d0e1ed26464e55a70dba3250e26a228f967 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:53:26 +1000 Subject: [PATCH 09/15] Fix AlbumToolTip --- .../KnockoutExtensions/EntryToolTip.tsx | 24 +++++++++++++++---- .../Shared/Partials/Album/AlbumLink.tsx | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 49a44779a3..61ab6064af 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -1,3 +1,4 @@ +import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; import { AlbumWithCoverPopupContent } from '@/Components/Shared/AlbumWithCoverPopupContent'; import { ArtistPopupContent } from '@/Components/Shared/ArtistPopupContent'; import { EventPopupContent } from '@/Components/Shared/EventPopupContent'; @@ -12,7 +13,10 @@ import { SongWithPVAndVoteContract } from '@/DataContracts/Song/SongWithPVAndVot import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; import { UserApiContract } from '@/DataContracts/User/UserApiContract'; import { EntryType } from '@/Models/EntryType'; -import { AlbumRepository } from '@/Repositories/AlbumRepository'; +import { + AlbumOptionalField, + AlbumRepository, +} from '@/Repositories/AlbumRepository'; import { ArtistOptionalField, ArtistRepository, @@ -69,10 +73,11 @@ const QTip = React.forwardRef( interface AlbumToolTipProps { id: number; children?: React.ReactNode; + withCover?: boolean; } export const AlbumToolTip = React.memo( - ({ id, children }: AlbumToolTipProps): React.ReactElement => { + ({ id, children, withCover }: AlbumToolTipProps): React.ReactElement => { const triggerRef = React.useRef(undefined!); const containerRef = React.useRef(undefined!); @@ -86,7 +91,14 @@ export const AlbumToolTip = React.memo( if (!show) return; albumRepo - .getOne({ id: id, lang: vdb.values.languagePreference }) + .getOneWithComponents({ + id: id, + fields: [ + AlbumOptionalField.AdditionalNames, + AlbumOptionalField.MainPicture, + ], + lang: vdb.values.languagePreference, + }) .then((album) => setAlbum(album)); }, [album, show, id]); @@ -111,7 +123,11 @@ export const AlbumToolTip = React.memo( > {({ props }): React.ReactElement => ( - + {withCover ? ( + + ) : ( + + )} )} diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx index c26a6d3648..7b80a0599a 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Album/AlbumLink.tsx @@ -31,7 +31,7 @@ export const AlbumLink = ({ tooltip = false, }: AlbumLinkProps): React.ReactElement => { return tooltip ? ( - + ) : ( From 38c600a607ebdf8b5ed291186044440b90da1472 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 19:15:47 +1000 Subject: [PATCH 10/15] Create OverlayTrigger --- VocaDbWeb/Scripts/Bootstrap/Overlay.tsx | 138 ++++++++ .../Scripts/Bootstrap/OverlayTrigger.tsx | 326 ++++++++++++++++++ .../Scripts/Bootstrap/safeFindDOMNode.ts | 11 + VocaDbWeb/Scripts/Bootstrap/types.tsx | 12 + .../Scripts/Bootstrap/useOverlayOffset.tsx | 33 ++ 5 files changed, 520 insertions(+) create mode 100644 VocaDbWeb/Scripts/Bootstrap/Overlay.tsx create mode 100644 VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx create mode 100644 VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts create mode 100644 VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx diff --git a/VocaDbWeb/Scripts/Bootstrap/Overlay.tsx b/VocaDbWeb/Scripts/Bootstrap/Overlay.tsx new file mode 100644 index 0000000000..096212652d --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/Overlay.tsx @@ -0,0 +1,138 @@ +// Code from: https://github.com/react-bootstrap/react-bootstrap/blob/33f037ba1e9870463f1bd33a4fe66b8e2a7586f6/src/Overlay.tsx. +import safeFindDOMNode from '@/Bootstrap/safeFindDOMNode'; +import { Placement, PopperRef, RootCloseEvent } from '@/Bootstrap/types'; +import useOverlayOffset from '@/Bootstrap/useOverlayOffset'; +import useCallbackRef from '@restart/hooks/useCallbackRef'; +import useEventCallback from '@restart/hooks/useEventCallback'; +import useIsomorphicEffect from '@restart/hooks/useIsomorphicEffect'; +import useMergedRefs from '@restart/hooks/useMergedRefs'; +import BaseOverlay, { + OverlayProps as BaseOverlayProps, + OverlayArrowProps, +} from '@restart/ui/Overlay'; +import { State } from '@restart/ui/usePopper'; +import classNames from 'classnames'; +import * as React from 'react'; +import { useRef } from 'react'; + +export interface OverlayInjectedProps { + ref: React.RefCallback; + style: React.CSSProperties; + 'aria-labelledby'?: string; + + arrowProps: Partial; + + show: boolean; + placement: Placement | undefined; + popper: PopperRef; + [prop: string]: any; +} + +export type OverlayChildren = + | React.ReactElement + | ((injected: OverlayInjectedProps) => React.ReactNode); + +export interface OverlayProps + extends Omit { + children: OverlayChildren; + placement?: Placement; + rootCloseEvent?: RootCloseEvent; +} + +const defaultProps: Partial = { + rootClose: false, + show: false, + placement: 'top', +}; + +function wrapRefs(props: any, arrowProps: any): void { + const { ref } = props; + const { ref: aRef } = arrowProps; + + props.ref = + ref.__wrapped || (ref.__wrapped = (r: any): any => ref(safeFindDOMNode(r))); + arrowProps.ref = + aRef.__wrapped || + (aRef.__wrapped = (r: any): any => aRef(safeFindDOMNode(r))); +} + +const Overlay = React.forwardRef( + ({ children: overlay, popperConfig = {}, ...outerProps }, outerRef) => { + const popperRef = useRef>({}); + const [firstRenderedState, setFirstRenderedState] = useCallbackRef(); + const [ref, modifiers] = useOverlayOffset(outerProps.offset); + const mergedRef = useMergedRefs( + outerRef as React.MutableRefObject, + ref, + ); + + const handleFirstUpdate = useEventCallback((state) => { + setFirstRenderedState(state); + popperConfig?.onFirstUpdate?.(state); + }); + + useIsomorphicEffect(() => { + if (firstRenderedState) { + popperRef.current.scheduleUpdate?.(); + } + }, [firstRenderedState]); + + return ( + + {( + overlayProps, + { arrowProps, popper: popperObj, show }, + ): React.ReactNode => { + wrapRefs(overlayProps, arrowProps); + // Need to get placement from popper object, handling case when overlay is flipped using 'flip' prop + const updatedPlacement = popperObj?.placement; + const popper = Object.assign(popperRef.current, { + state: popperObj?.state, + scheduleUpdate: popperObj?.update, + placement: updatedPlacement, + outOfBoundaries: + popperObj?.state?.modifiersData.hide?.isReferenceHidden || false, + }); + + if (typeof overlay === 'function') + return overlay({ + ...overlayProps, + placement: updatedPlacement, + show, + ...(show && { className: 'show' }), + popper, + arrowProps, + }); + + return React.cloneElement(overlay as React.ReactElement, { + ...overlayProps, + placement: updatedPlacement, + arrowProps, + popper, + className: classNames( + (overlay as React.ReactElement).props.className, + show && 'show', + ), + style: { + ...(overlay as React.ReactElement).props.style, + ...overlayProps.style, + }, + }); + }} + + ); + }, +); + +Overlay.displayName = 'Overlay'; +Overlay.defaultProps = defaultProps; + +export default Overlay; diff --git a/VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx b/VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx new file mode 100644 index 0000000000..370fc95807 --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/OverlayTrigger.tsx @@ -0,0 +1,326 @@ +// Code from: https://github.com/react-bootstrap/react-bootstrap/blob/33f037ba1e9870463f1bd33a4fe66b8e2a7586f6/src/OverlayTrigger.tsx. +import Overlay, { OverlayChildren, OverlayProps } from '@/Bootstrap/Overlay'; +import safeFindDOMNode from '@/Bootstrap/safeFindDOMNode'; +import useMergedRefs from '@restart/hooks/useMergedRefs'; +import useTimeout from '@restart/hooks/useTimeout'; +import contains from 'dom-helpers/contains'; +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { cloneElement, useCallback, useRef } from 'react'; +import { useUncontrolledProp } from 'uncontrollable'; +import warning from 'warning'; + +export type OverlayTriggerType = 'hover' | 'click' | 'focus'; + +export type OverlayDelay = number | { show: number; hide: number }; + +export type OverlayInjectedProps = { + onFocus?: (...args: any[]) => any; +}; + +export type OverlayTriggerRenderProps = OverlayInjectedProps & { + ref: React.Ref; +}; + +export interface OverlayTriggerProps + extends Omit { + children: + | React.ReactElement + | ((props: OverlayTriggerRenderProps) => React.ReactNode); + trigger?: OverlayTriggerType | OverlayTriggerType[]; + delay?: OverlayDelay; + show?: boolean; + defaultShow?: boolean; + onToggle?: (nextShow: boolean) => void; + flip?: boolean; + overlay: OverlayChildren; + + target?: never; + onHide?: never; +} + +function normalizeDelay( + delay?: OverlayDelay, +): { show?: number; hide?: number } { + return delay && typeof delay === 'object' + ? delay + : { + show: delay, + hide: delay, + }; +} + +// Simple implementation of mouseEnter and mouseLeave. +// React's built version is broken: https://github.com/facebook/react/issues/4251 +// for cases when the trigger is disabled and mouseOut/Over can cause flicker +// moving from one child element to another. +function handleMouseOverOut( + // eslint-disable-next-line @typescript-eslint/no-shadow + handler: (...args: [React.MouseEvent, ...any[]]) => any, + args: [React.MouseEvent, ...any[]], + relatedNative: 'fromElement' | 'toElement', +): void { + const [e] = args; + const target = e.currentTarget; + const related = (e.relatedTarget || + e.nativeEvent[relatedNative as keyof Event]) as Element; + + if ((!related || related !== target) && !contains(target, related)) { + handler(...args); + } +} + +const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']); + +const propTypes = { + children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, + + /** + * Specify which action or actions trigger Overlay visibility + * + * The `click` trigger ignores the configured `delay`. + * + * @type {'hover' | 'click' |'focus' | Array<'hover' | 'click' |'focus'>} + */ + trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]), + + /** + * A millisecond delay amount to show and hide the Overlay once triggered + */ + delay: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + show: PropTypes.number, + hide: PropTypes.number, + }), + ]), + + /** + * The visibility of the Overlay. `show` is a _controlled_ prop so should be paired + * with `onToggle` to avoid breaking user interactions. + * + * Manually toggling `show` does **not** wait for `delay` to change the visibility. + * + * @controllable onToggle + */ + show: PropTypes.bool, + + /** + * The initial visibility state of the Overlay. + */ + defaultShow: PropTypes.bool, + + /** + * A callback that fires when the user triggers a change in tooltip visibility. + * + * `onToggle` is called with the desired next `show`, and generally should be passed + * back to the `show` prop. `onToggle` fires _after_ the configured `delay` + * + * @controllable `show` + */ + onToggle: PropTypes.func, + + /** + The initial flip state of the Overlay. + */ + flip: PropTypes.bool, + + /** + * An element or text to overlay next to the target. + */ + overlay: PropTypes.oneOfType([PropTypes.func, PropTypes.element.isRequired]), + + /** + * A Popper.js config object passed to the the underlying popper instance. + */ + popperConfig: PropTypes.object, + + // Overridden props from ``. + /** + * @private + */ + target: PropTypes.oneOf([null]), + + /** + * @private + */ + onHide: PropTypes.oneOf([null]), + + /** + * The placement of the Overlay in relation to it's `target`. + */ + placement: PropTypes.oneOf([ + 'auto-start', + 'auto', + 'auto-end', + 'top-start', + 'top', + 'top-end', + 'right-start', + 'right', + 'right-end', + 'bottom-end', + 'bottom', + 'bottom-start', + 'left-end', + 'left', + 'left-start', + ]), +}; + +const defaultProps = { + defaultShow: false, + trigger: ['hover', 'focus'], +}; + +function OverlayTrigger({ + trigger, + overlay, + children, + popperConfig = {}, + + show: propsShow, + defaultShow = false, + onToggle, + + delay: propsDelay, + placement, + flip = placement && placement.indexOf('auto') !== -1, + ...props +}: OverlayTriggerProps): React.ReactElement { + const triggerNodeRef = useRef(null); + const mergedRef = useMergedRefs( + triggerNodeRef, + (children as any).ref, + ); + const timeout = useTimeout(); + const hoverStateRef = useRef(''); + + const [show, setShow] = useUncontrolledProp(propsShow, defaultShow, onToggle); + + const delay = normalizeDelay(propsDelay); + + const { onFocus, onBlur, onClick } = + typeof children !== 'function' + ? React.Children.only(children).props + : ({} as any); + + const attachRef = ( + r: React.ComponentClass | Element | null | undefined, + ): void => { + mergedRef(safeFindDOMNode(r)); + }; + + const handleShow = useCallback(() => { + timeout.clear(); + hoverStateRef.current = 'show'; + + if (!delay.show) { + setShow(true); + return; + } + + timeout.set(() => { + if (hoverStateRef.current === 'show') setShow(true); + }, delay.show); + }, [delay.show, setShow, timeout]); + + const handleHide = useCallback(() => { + timeout.clear(); + hoverStateRef.current = 'hide'; + + if (!delay.hide) { + setShow(false); + return; + } + + timeout.set(() => { + if (hoverStateRef.current === 'hide') setShow(false); + }, delay.hide); + }, [delay.hide, setShow, timeout]); + + const handleFocus = useCallback( + (...args: any[]) => { + handleShow(); + onFocus?.(...args); + }, + [handleShow, onFocus], + ); + + const handleBlur = useCallback( + (...args: any[]) => { + handleHide(); + onBlur?.(...args); + }, + [handleHide, onBlur], + ); + + const handleClick = useCallback( + (...args: any[]) => { + setShow(!show); + onClick?.(...args); + }, + [onClick, setShow, show], + ); + + const handleMouseOver = useCallback( + (...args: [React.MouseEvent, ...any[]]) => { + handleMouseOverOut(handleShow, args, 'fromElement'); + }, + [handleShow], + ); + + const handleMouseOut = useCallback( + (...args: [React.MouseEvent, ...any[]]) => { + handleMouseOverOut(handleHide, args, 'toElement'); + }, + [handleHide], + ); + + const triggers: string[] = trigger == null ? [] : [].concat(trigger as any); + const triggerProps: any = { + ref: attachRef, + }; + + if (triggers.indexOf('click') !== -1) { + triggerProps.onClick = handleClick; + } + + if (triggers.indexOf('focus') !== -1) { + triggerProps.onFocus = handleFocus; + triggerProps.onBlur = handleBlur; + } + + if (triggers.indexOf('hover') !== -1) { + warning( + triggers.length > 1, + '[react-bootstrap] Specifying only the `"hover"` trigger limits the visibility of the overlay to just mouse users. Consider also including the `"focus"` trigger so that touch and keyboard only users can see the overlay as well.', + ); + triggerProps.onMouseOver = handleMouseOver; + triggerProps.onMouseOut = handleMouseOut; + } + + return ( + <> + {typeof children === 'function' + ? children(triggerProps) + : cloneElement(children, triggerProps)} + + {overlay} + + + ); +} + +OverlayTrigger.propTypes = propTypes; +OverlayTrigger.defaultProps = defaultProps; + +export default OverlayTrigger; diff --git a/VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts b/VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts new file mode 100644 index 0000000000..aea18b1b08 --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/safeFindDOMNode.ts @@ -0,0 +1,11 @@ +// Code from: https://github.com/react-bootstrap/react-bootstrap/blob/33f037ba1e9870463f1bd33a4fe66b8e2a7586f6/src/safeFindDOMNode.ts. +import ReactDOM from 'react-dom'; + +export default function safeFindDOMNode( + componentOrElement: React.ComponentClass | Element | null | undefined, +): Element | Text | null { + if (componentOrElement && 'setState' in componentOrElement) { + return ReactDOM.findDOMNode(componentOrElement); + } + return (componentOrElement ?? null) as Element | Text | null; +} diff --git a/VocaDbWeb/Scripts/Bootstrap/types.tsx b/VocaDbWeb/Scripts/Bootstrap/types.tsx index 700e4cf54d..88467ef0a7 100644 --- a/VocaDbWeb/Scripts/Bootstrap/types.tsx +++ b/VocaDbWeb/Scripts/Bootstrap/types.tsx @@ -1,4 +1,5 @@ // Code from: https://github.com/react-bootstrap/react-bootstrap/blob/8a7e095e8032fdeac4fd1fdb41e6dfb452ae4494/src/types.tsx +import { State } from '@restart/ui/usePopper'; export type Variant = | 'primary' @@ -11,3 +12,14 @@ export type Variant = export type ButtonVariant = Variant; export type EventKey = string | number; + +export type Placement = import('@restart/ui/usePopper').Placement; + +export type RootCloseEvent = 'click' | 'mousedown'; + +export interface PopperRef { + state: State | undefined; + outOfBoundaries: boolean; + placement: Placement | undefined; + scheduleUpdate?: () => void; +} diff --git a/VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx b/VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx new file mode 100644 index 0000000000..2ed09ac2c0 --- /dev/null +++ b/VocaDbWeb/Scripts/Bootstrap/useOverlayOffset.tsx @@ -0,0 +1,33 @@ +import { useBootstrapPrefix } from '@/Bootstrap/ThemeProvider'; +import { Offset, Options } from '@restart/ui/usePopper'; +import hasClass from 'dom-helpers/hasClass'; +import { useMemo, useRef } from 'react'; + +// This is meant for internal use. +// This applies a custom offset to the overlay if it's a popover. +export default function useOverlayOffset( + customOffset?: Offset, +): [React.RefObject, Options['modifiers']] { + const overlayRef = useRef(null); + const popoverClass = useBootstrapPrefix(undefined, 'popover'); + + const offset = useMemo( + () => ({ + name: 'offset', + options: { + offset: (): Offset => { + if ( + overlayRef.current && + hasClass(overlayRef.current, popoverClass) + ) { + return customOffset || [0, 8]; + } + return customOffset || [0, 0]; + }, + }, + }), + [customOffset, popoverClass], + ); + + return [overlayRef, [offset]]; +} From 9ca417946e99bd26f2b1e97cecbd587a859d2f94 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 19:36:52 +1000 Subject: [PATCH 11/15] Update EntryToolTip --- .../KnockoutExtensions/EntryToolTip.tsx | 295 +++++++----------- 1 file changed, 114 insertions(+), 181 deletions(-) diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 61ab6064af..81a98abf97 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -1,3 +1,4 @@ +import OverlayTrigger from '@/Bootstrap/OverlayTrigger'; import { AlbumPopupContent } from '@/Components/Shared/AlbumPopupContent'; import { AlbumWithCoverPopupContent } from '@/Components/Shared/AlbumWithCoverPopupContent'; import { ArtistPopupContent } from '@/Components/Shared/ArtistPopupContent'; @@ -37,7 +38,6 @@ import { import { HttpClient } from '@/Shared/HttpClient'; import { UrlMapper } from '@/Shared/UrlMapper'; import React from 'react'; -import Overlay from 'react-overlays/Overlay'; const httpClient = new HttpClient(); const urlMapper = new UrlMapper(vdb.values.baseAddress); @@ -51,11 +51,10 @@ const userRepo = new UserRepository(httpClient, urlMapper); type QTipProps = React.HTMLAttributes; -const QTip = React.forwardRef( +const QTip = React.forwardRef( ({ children, ...props }: QTipProps, ref): React.ReactElement => { return (
{ - const triggerRef = React.useRef(undefined!); - const containerRef = React.useRef(undefined!); - const [show, setShow] = React.useState(false); const [album, setAlbum] = React.useState(); @@ -103,36 +99,28 @@ export const AlbumToolTip = React.memo( }, [album, show, id]); return ( - - setShow(true)} - onMouseLeave={(): void => setShow(false)} - > - {children} - - {show && album && ( - setShow(false)} - placement="bottom-start" - container={containerRef} - target={triggerRef} - flip - > - {({ props }): React.ReactElement => ( - - {withCover ? ( - - ) : ( - - )} - - )} - - )} - + + {withCover ? ( + + ) : ( + + )} + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} + > + {children} + ); }, ); @@ -144,9 +132,6 @@ interface ArtistToolTipProps { export const ArtistToolTip = React.memo( ({ id, children }: ArtistToolTipProps): React.ReactElement => { - const triggerRef = React.useRef(undefined!); - const containerRef = React.useRef(undefined!); - const [show, setShow] = React.useState(false); const [artist, setArtist] = React.useState(); @@ -169,32 +154,24 @@ export const ArtistToolTip = React.memo( }, [artist, show, id]); return ( - - setShow(true)} - onMouseLeave={(): void => setShow(false)} - > - {children} - - {show && artist && ( - setShow(false)} - placement="bottom-start" - container={containerRef} - target={triggerRef} - flip - > - {({ props }): React.ReactElement => ( - - - - )} - - )} - + + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} + > + {children} + ); }, ); @@ -206,9 +183,6 @@ interface EventToolTipProps { export const EventToolTip = React.memo( ({ id, children }: EventToolTipProps): React.ReactElement => { - const triggerRef = React.useRef(undefined!); - const containerRef = React.useRef(undefined!); - const [show, setShow] = React.useState(false); const [event, setEvent] = React.useState(); @@ -232,32 +206,24 @@ export const EventToolTip = React.memo( }, [event, show, id]); return ( - - setShow(true)} - onMouseLeave={(): void => setShow(false)} - > - {children} - - {show && event && ( - setShow(false)} - placement="bottom-start" - container={containerRef} - target={triggerRef} - flip - > - {({ props }): React.ReactElement => ( - - - - )} - - )} - + + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} + > + {children} + ); }, ); @@ -278,9 +244,6 @@ interface SongToolTipProps { export const SongToolTip = React.memo( ({ id, children, foreignDomain }: SongToolTipProps): React.ReactElement => { - const triggerRef = React.useRef(undefined!); - const containerRef = React.useRef(undefined!); - const [show, setShow] = React.useState(false); const [song, setSong] = React.useState(); @@ -321,32 +284,24 @@ export const SongToolTip = React.memo( }, [song, show, id, foreignDomain]); return ( - - setShow(true)} - onMouseLeave={(): void => setShow(false)} - > - {children} - - {show && song && ( - setShow(false)} - placement="bottom-start" - container={containerRef} - target={triggerRef} - flip - > - {({ props }): React.ReactElement => ( - - - - )} - - )} - + + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} + > + {children} + ); }, ); @@ -358,9 +313,6 @@ interface TagToolTipProps { export const TagToolTip = React.memo( ({ id, children }: TagToolTipProps): React.ReactElement => { - const triggerRef = React.useRef(undefined!); - const containerRef = React.useRef(undefined!); - const [show, setShow] = React.useState(false); const [tag, setTag] = React.useState(); @@ -384,32 +336,24 @@ export const TagToolTip = React.memo( }, [tag, show, id]); return ( - - setShow(true)} - onMouseLeave={(): void => setShow(false)} - > - {children} - - {show && tag && ( - setShow(false)} - placement="bottom-start" - container={containerRef} - target={triggerRef} - flip - > - {({ props }): React.ReactElement => ( - - - - )} - - )} - + + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} + > + {children} + ); }, ); @@ -421,9 +365,6 @@ interface UserToolTipProps { export const UserToolTip = React.memo( ({ id, children }: UserToolTipProps): React.ReactElement => { - const triggerRef = React.useRef(undefined!); - const containerRef = React.useRef(undefined!); - const [show, setShow] = React.useState(false); const [user, setUser] = React.useState(); @@ -439,32 +380,24 @@ export const UserToolTip = React.memo( }, [user, show, id]); return ( - - setShow(true)} - onMouseLeave={(): void => setShow(false)} - > - {children} - - {show && user && ( - setShow(false)} - placement="bottom-start" - container={containerRef} - target={triggerRef} - flip - > - {({ props }): React.ReactElement => ( - - - - )} - - )} - + + + + ) : ( + <> + ) + } + onToggle={(nextShow): void => setShow(nextShow)} + > + {children} + ); }, ); From 1172de2a3338d2c4b1d5c855b03c309bd085d980 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 20:59:17 +1000 Subject: [PATCH 12/15] Update HelpLabel and ValidationErrorIcon --- .../KnockoutExtensions/EntryToolTip.tsx | 45 ++++++------------- .../Shared/Partials/Shared/HelpLabel.tsx | 32 +++++++------ .../Partials/Shared/ValidationErrorIcon.tsx | 30 ++++++------- VocaDbWeb/Scripts/QTip/QTipToolTip.tsx | 21 +++++++++ 4 files changed, 64 insertions(+), 64 deletions(-) create mode 100644 VocaDbWeb/Scripts/QTip/QTipToolTip.tsx diff --git a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx index 81a98abf97..0571ac9f7b 100644 --- a/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx +++ b/VocaDbWeb/Scripts/Components/KnockoutExtensions/EntryToolTip.tsx @@ -14,6 +14,7 @@ import { SongWithPVAndVoteContract } from '@/DataContracts/Song/SongWithPVAndVot import { TagApiContract } from '@/DataContracts/Tag/TagApiContract'; import { UserApiContract } from '@/DataContracts/User/UserApiContract'; import { EntryType } from '@/Models/EntryType'; +import { QTipToolTip } from '@/QTip/QTipToolTip'; import { AlbumOptionalField, AlbumRepository, @@ -49,26 +50,6 @@ const songRepo = new SongRepository(httpClient, vdb.values.baseAddress); const tagRepo = new TagRepository(httpClient, vdb.values.baseAddress); const userRepo = new UserRepository(httpClient, urlMapper); -type QTipProps = React.HTMLAttributes; - -const QTip = React.forwardRef( - ({ children, ...props }: QTipProps, ref): React.ReactElement => { - return ( -
-
{children}
-
- ); - }, -); - interface AlbumToolTipProps { id: number; children?: React.ReactNode; @@ -106,13 +87,13 @@ export const AlbumToolTip = React.memo( offset={[0, 8]} overlay={ album ? ( - + {withCover ? ( ) : ( )} - + ) : ( <> ) @@ -161,9 +142,9 @@ export const ArtistToolTip = React.memo( offset={[0, 8]} overlay={ artist ? ( - + - + ) : ( <> ) @@ -213,9 +194,9 @@ export const EventToolTip = React.memo( offset={[0, 8]} overlay={ event ? ( - + - + ) : ( <> ) @@ -291,9 +272,9 @@ export const SongToolTip = React.memo( offset={[0, 8]} overlay={ song ? ( - + - + ) : ( <> ) @@ -343,9 +324,9 @@ export const TagToolTip = React.memo( offset={[0, 8]} overlay={ tag ? ( - + - + ) : ( <> ) @@ -387,9 +368,9 @@ export const UserToolTip = React.memo( offset={[0, 8]} overlay={ user ? ( - + - + ) : ( <> ) diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx index c620e9bfc1..c0c5d13244 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx @@ -1,5 +1,5 @@ -import $ from 'jquery'; -import 'qtip2'; +import OverlayTrigger from '@/Bootstrap/OverlayTrigger'; +import { QTipToolTip } from '@/QTip/QTipToolTip'; import React from 'react'; interface HelpLabelProps { @@ -14,21 +14,19 @@ export const HelpLabel = ({ title, forElem, }: HelpLabelProps): React.ReactElement => { - const el = React.useRef(undefined!); - - React.useEffect(() => { - $(el.current).qtip({ - style: { classes: 'tooltip-wider' }, - }); - - return (): void => { - $('.qtip').remove(); - }; - }, []); - return ( - + {title}} + > + + + + ); }; diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx index 851960d16a..365c90b9d0 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/ValidationErrorIcon.tsx @@ -1,5 +1,5 @@ -import $ from 'jquery'; -import 'qtip2'; +import OverlayTrigger from '@/Bootstrap/OverlayTrigger'; +import { QTipToolTip } from '@/QTip/QTipToolTip'; import React from 'react'; interface ValidationErrorIconProps { @@ -10,17 +10,17 @@ interface ValidationErrorIconProps { export const ValidationErrorIcon = ({ title, }: ValidationErrorIconProps): React.ReactElement => { - const el = React.useRef(undefined!); - - React.useEffect(() => { - $(el.current).qtip({ - style: { classes: 'tooltip-wider' }, - }); - - return (): void => { - $('.qtip').remove(); - }; - }, []); - - return ; + return ( + {title}} + > + + + + + ); }; diff --git a/VocaDbWeb/Scripts/QTip/QTipToolTip.tsx b/VocaDbWeb/Scripts/QTip/QTipToolTip.tsx new file mode 100644 index 0000000000..27f7f90584 --- /dev/null +++ b/VocaDbWeb/Scripts/QTip/QTipToolTip.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type QTipToolTipProps = React.HTMLAttributes; + +export const QTipToolTip = React.forwardRef( + ({ children, ...props }: QTipToolTipProps, ref): React.ReactElement => { + return ( +
+
{children}
+
+ ); + }, +); From ce66c9a32413ea42b672cb15172dcbfc3bc08b43 Mon Sep 17 00:00:00 2001 From: Aigamo <51428094+ycanardeau@users.noreply.github.com> Date: Thu, 15 Sep 2022 21:16:09 +1000 Subject: [PATCH 13/15] Use dangerouslySetInnerHTML --- .../Shared/KnockoutPartials/NamesEditor.tsx | 4 +- .../Shared/Partials/Shared/HelpLabel.tsx | 12 +++-- .../Partials/Shared/ValidationErrorIcon.tsx | 12 +++-- VocaDbWeb/Scripts/Pages/Album/AlbumEdit.tsx | 47 +++++++++++------ VocaDbWeb/Scripts/Pages/Artist/ArtistEdit.tsx | 51 +++++++++++++------ VocaDbWeb/Scripts/Pages/Event/EventEdit.tsx | 27 +++++++--- .../Scripts/Pages/Event/EventEditSeries.tsx | 12 +++-- .../Pages/Song/Partials/LyricsForSongEdit.tsx | 10 +++- VocaDbWeb/Scripts/Pages/Song/SongCreate.tsx | 4 +- VocaDbWeb/Scripts/Pages/Song/SongEdit.tsx | 44 +++++++++++----- .../Scripts/Pages/SongList/SongListEdit.tsx | 8 ++- VocaDbWeb/Scripts/Pages/Tag/TagEdit.tsx | 23 +++++++-- VocaDbWeb/Scripts/Pages/Venue/VenueEdit.tsx | 12 +++-- 13 files changed, 194 insertions(+), 72 deletions(-) diff --git a/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx b/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx index 7b745d4e60..74f4d07f7f 100644 --- a/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/KnockoutPartials/NamesEditor.tsx @@ -115,7 +115,9 @@ export const NamesEditor = observer( {showAliases && ( <> diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx index c0c5d13244..eb74b3a482 100644 --- a/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx +++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Shared/HelpLabel.tsx @@ -4,14 +4,16 @@ import React from 'react'; interface HelpLabelProps { label: string; - title: string; + dangerouslySetInnerHTML: { + __html: string; + }; forElem?: string; } // Displays label element with attached qTip tooltip export const HelpLabel = ({ label, - title, + dangerouslySetInnerHTML, forElem, }: HelpLabelProps): React.ReactElement => { return ( @@ -20,7 +22,11 @@ export const HelpLabel = ({ delay={{ show: 250, hide: 0 }} flip offset={[0, 8]} - overlay={{title}} + overlay={ + + + + } >