From 374ae4de6ec8a90df5095407898e52238e10fae7 Mon Sep 17 00:00:00 2001 From: Yiannis <1515939+styiannis@users.noreply.github.com> Date: Wed, 11 Mar 2026 02:14:45 +0200 Subject: [PATCH] refactor(frontend): replace legacy settings init/settings pattern with typed config functions --- frontend/src/static/js/utils/settings/api.js | 59 ---- frontend/src/static/js/utils/settings/api.ts | 45 +++ .../src/static/js/utils/settings/config.js | 113 ------- .../src/static/js/utils/settings/config.ts | 50 ++++ .../src/static/js/utils/settings/contents.js | 121 -------- .../src/static/js/utils/settings/contents.ts | 67 +++++ .../src/static/js/utils/settings/media.js | 49 --- .../src/static/js/utils/settings/media.ts | 33 +++ .../src/static/js/utils/settings/member.js | 135 --------- .../src/static/js/utils/settings/member.ts | 98 ++++++ .../static/js/utils/settings/notifications.js | 32 -- .../static/js/utils/settings/notifications.ts | 27 ++ .../js/utils/settings/optionsEmbedded.js | 42 --- .../js/utils/settings/optionsEmbedded.ts | 45 +++ .../static/js/utils/settings/optionsPages.js | 108 ------- .../static/js/utils/settings/optionsPages.ts | 63 ++++ .../src/static/js/utils/settings/pages.js | 50 ---- .../src/static/js/utils/settings/pages.ts | 30 ++ .../src/static/js/utils/settings/playlists.js | 35 --- .../src/static/js/utils/settings/playlists.ts | 19 ++ .../src/static/js/utils/settings/sidebar.js | 27 -- .../src/static/js/utils/settings/sidebar.ts | 9 + frontend/src/static/js/utils/settings/site.js | 42 --- frontend/src/static/js/utils/settings/site.ts | 10 + .../static/js/utils/settings/taxonomies.js | 34 --- .../static/js/utils/settings/taxonomies.ts | 25 ++ .../src/static/js/utils/settings/theme.js | 65 ---- .../src/static/js/utils/settings/theme.ts | 51 ++++ frontend/src/static/js/utils/settings/url.js | 13 - frontend/src/static/js/utils/settings/url.ts | 59 ++++ frontend/tests/utils/settings/api.test.ts | 76 ++--- frontend/tests/utils/settings/config.test.ts | 280 +++++++++--------- .../tests/utils/settings/contents.test.ts | 65 +++- frontend/tests/utils/settings/media.test.ts | 18 +- frontend/tests/utils/settings/member.test.ts | 31 +- .../utils/settings/notifications.test.ts | 38 +-- .../utils/settings/optionsEmbedded.test.ts | 31 +- .../tests/utils/settings/optionsPages.test.ts | 89 +++--- frontend/tests/utils/settings/pages.test.ts | 27 +- .../tests/utils/settings/playlists.test.ts | 30 +- frontend/tests/utils/settings/sidebar.test.ts | 15 +- frontend/tests/utils/settings/site.test.ts | 22 +- .../tests/utils/settings/taxonomies.test.ts | 90 +++--- frontend/tests/utils/settings/theme.test.ts | 19 +- frontend/tests/utils/settings/url.test.ts | 121 ++++---- 45 files changed, 1137 insertions(+), 1371 deletions(-) delete mode 100755 frontend/src/static/js/utils/settings/api.js create mode 100755 frontend/src/static/js/utils/settings/api.ts delete mode 100644 frontend/src/static/js/utils/settings/config.js create mode 100644 frontend/src/static/js/utils/settings/config.ts delete mode 100755 frontend/src/static/js/utils/settings/contents.js create mode 100755 frontend/src/static/js/utils/settings/contents.ts delete mode 100755 frontend/src/static/js/utils/settings/media.js create mode 100755 frontend/src/static/js/utils/settings/media.ts delete mode 100755 frontend/src/static/js/utils/settings/member.js create mode 100755 frontend/src/static/js/utils/settings/member.ts delete mode 100644 frontend/src/static/js/utils/settings/notifications.js create mode 100644 frontend/src/static/js/utils/settings/notifications.ts delete mode 100755 frontend/src/static/js/utils/settings/optionsEmbedded.js create mode 100755 frontend/src/static/js/utils/settings/optionsEmbedded.ts delete mode 100755 frontend/src/static/js/utils/settings/optionsPages.js create mode 100755 frontend/src/static/js/utils/settings/optionsPages.ts delete mode 100755 frontend/src/static/js/utils/settings/pages.js create mode 100755 frontend/src/static/js/utils/settings/pages.ts delete mode 100755 frontend/src/static/js/utils/settings/playlists.js create mode 100755 frontend/src/static/js/utils/settings/playlists.ts delete mode 100644 frontend/src/static/js/utils/settings/sidebar.js create mode 100644 frontend/src/static/js/utils/settings/sidebar.ts delete mode 100755 frontend/src/static/js/utils/settings/site.js create mode 100755 frontend/src/static/js/utils/settings/site.ts delete mode 100755 frontend/src/static/js/utils/settings/taxonomies.js create mode 100755 frontend/src/static/js/utils/settings/taxonomies.ts delete mode 100755 frontend/src/static/js/utils/settings/theme.js create mode 100755 frontend/src/static/js/utils/settings/theme.ts delete mode 100755 frontend/src/static/js/utils/settings/url.js create mode 100755 frontend/src/static/js/utils/settings/url.ts diff --git a/frontend/src/static/js/utils/settings/api.js b/frontend/src/static/js/utils/settings/api.js deleted file mode 100755 index 73e2bb7d..00000000 --- a/frontend/src/static/js/utils/settings/api.js +++ /dev/null @@ -1,59 +0,0 @@ -const urlParse = require('url-parse'); - -let BASE_URL = null; -let ENDPOINTS = null; - -function endpointsIter(ret, endpoints) { - const baseUrl = BASE_URL.toString().replace(/\/+$/, ''); - - for (let k in endpoints) { - if ('string' === typeof endpoints[k]) { - ret[k] = baseUrl + '/' + endpoints[k].replace(/^\//g, ''); - } else { - endpointsIter(ret[k], endpoints[k]); - } - } -} - -function formatEndpoints(endpoints) { - const baseUrl = BASE_URL.toString(); - const ret = endpoints; - endpointsIter(ret, endpoints); - return ret; -} - -export function init(base_url, endpoints) { - BASE_URL = urlParse(base_url); - - ENDPOINTS = formatEndpoints({ - media: endpoints.media, - featured: endpoints.media + '?show=featured', - recommended: endpoints.media + '?show=recommended', - playlists: endpoints.playlists, - users: endpoints.members, - user: { - liked: endpoints.liked, - history: endpoints.history, - playlists: endpoints.playlists + '?author=', - }, - archive: { - tags: endpoints.tags, - categories: endpoints.categories, - }, - manage: { - media: endpoints.manage_media, - users: endpoints.manage_users, - comments: endpoints.manage_comments, - }, - search: { - query: endpoints.search + '?q=', - titles: endpoints.search + '?show=titles&q=', - tag: endpoints.search + '?t=', - category: endpoints.search + '?c=', - }, - }); -} - -export function endpoints() { - return ENDPOINTS; -} diff --git a/frontend/src/static/js/utils/settings/api.ts b/frontend/src/static/js/utils/settings/api.ts new file mode 100755 index 00000000..dcaebbff --- /dev/null +++ b/frontend/src/static/js/utils/settings/api.ts @@ -0,0 +1,45 @@ +import urlParse from 'url-parse'; // @todo: It's not necessary, 'URL.parse(...)' is sufficient +import { GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +function formatEndpoints(baseUrl: string, endpoints: Record) { + for (let k in endpoints) { + endpoints[k] = baseUrl + '/' + endpoints[k].replace(/^\//g, ''); + } + return endpoints; +} + +export function apiConfig( + apiUrl: GlobalMediaCMS['site']['api'], + endpoints: GlobalMediaCMS['api'] +): MediaCMSConfig['api'] { + const baseUrl = urlParse(apiUrl).toString().replace(/\/+$/, ''); + return { + ...formatEndpoints(baseUrl, { + media: endpoints.media, + featured: endpoints.media + '?show=featured', + recommended: endpoints.media + '?show=recommended', + playlists: endpoints.playlists, + users: endpoints.members, + }), + user: formatEndpoints(baseUrl, { + liked: endpoints.liked, + history: endpoints.history, + playlists: endpoints.playlists + '?author=', + }), + archive: formatEndpoints(baseUrl, { + tags: endpoints.tags, + categories: endpoints.categories, + }), + manage: formatEndpoints(baseUrl, { + media: endpoints.manage_media, + users: endpoints.manage_users, + comments: endpoints.manage_comments, + }), + search: formatEndpoints(baseUrl, { + query: endpoints.search + '?q=', + titles: endpoints.search + '?show=titles&q=', + tag: endpoints.search + '?t=', + category: endpoints.search + '?c=', + }), + }; +} diff --git a/frontend/src/static/js/utils/settings/config.js b/frontend/src/static/js/utils/settings/config.js deleted file mode 100644 index 3a0b9a94..00000000 --- a/frontend/src/static/js/utils/settings/config.js +++ /dev/null @@ -1,113 +0,0 @@ -import * as api from './api.js'; -import * as media from './media.js'; -import * as site from './site.js'; -import * as theme from './theme.js'; -import * as url from './url.js'; -import * as member from './member.js'; -import * as contents from './contents.js'; -import * as pages from './pages.js'; -import * as sidebar from './sidebar.js'; -import * as taxonomies from './taxonomies.js'; -import * as optionsPages from './optionsPages.js'; -import * as optionsEmbedded from './optionsEmbedded.js'; -import * as playlists from './playlists.js'; -import * as notifications from './notifications.js'; - -let DATA = null; - -export function config(glbl) { - if (DATA) { - return DATA; - } - - pages.init({ ...glbl.site.pages, ...glbl.site.userPages }); - optionsPages.init(glbl.pages.home, glbl.pages.search, glbl.pages.media, glbl.pages.profile, pages.settings()); - - url.init({ - home: glbl.url.home, - admin: !glbl.user.is.anonymous && glbl.user.is.admin ? glbl.url.admin : '', - error404: glbl.url.error404, - embed: glbl.site.url.replace(/\/+$/, '') + '/embed?m=', - latest: glbl.url.latestMedia, - featured: glbl.url.featuredMedia, - recommended: glbl.url.recommendedMedia, - signin: glbl.url.signin, - signout: !glbl.user.is.anonymous ? glbl.url.signout : '', - register: glbl.url.register, - changePassword: !glbl.user.is.anonymous ? glbl.url.changePassword : '', - members: glbl.url.members, - search: { - base: glbl.url.search, - query: glbl.url.search + '?q=', - tag: glbl.url.search + '?t=', - category: glbl.url.search + '?c=', - }, - profile: !!glbl.site.devEnv - ? { - media: glbl.user.pages.media, - about: glbl.user.pages.about, - playlists: glbl.user.pages.playlists, - shared_by_me: glbl.user.pages.media + '/shared_by_me', - shared_with_me: glbl.user.pages.media + '/shared_with_me', - } - : { - media: glbl.site.url.replace(/\/$/, '') + '/user/' + glbl.profileId, - about: glbl.site.url.replace(/\/$/, '') + '/user/' + glbl.profileId + '/about', - playlists: glbl.site.url.replace(/\/$/, '') + '/user/' + glbl.profileId + '/playlists', - shared_by_me: glbl.site.url.replace(/\/$/, '') + '/user/' + glbl.profileId + '/shared_by_me', - shared_with_me: glbl.site.url.replace(/\/$/, '') + '/user/' + glbl.profileId + '/shared_with_me', - }, - user: { - liked: glbl.url.likedMedia, - history: glbl.url.history, - addMedia: glbl.url.addMedia, - editChannel: glbl.url.editChannel, - editProfile: glbl.url.editProfile, - }, - archive: { - tags: glbl.url.tags, - categories: glbl.url.categories, - }, - manage: { - media: !glbl.user.is.anonymous ? glbl.url.manageMedia : '', - users: !glbl.user.is.anonymous ? glbl.url.manageUsers : '', - comments: !glbl.user.is.anonymous ? glbl.url.manageComments : '', - }, - }); - - site.init(glbl.site); - contents.init(glbl.contents); - api.init(glbl.site.api, glbl.api); - sidebar.init(glbl.features.sideBar); - taxonomies.init(glbl.site.taxonomies); - member.init(glbl.user, glbl.features); - theme.init(glbl.site.theme, glbl.site.logo); - optionsEmbedded.init(glbl.features.embeddedVideo); - media.init(glbl.features.mediaItem, glbl.features.media.shareOptions); - playlists.init(glbl.features.playlists); - - notifications.init(glbl.contents.notifications); - - DATA = { - site: site.settings(), - theme: theme.settings(), - member: member.settings(), - media: media.settings(), - playlists: playlists.settings(), - url: url.pages(), - api: api.endpoints(), - sidebar: sidebar.settings(), - contents: contents.settings(), - options: { - pages: optionsPages.settings(), - embedded: optionsEmbedded.settings(), - }, - enabled: { - pages: pages.settings(), - taxonomies: taxonomies.settings(), - }, - notifications: notifications.settings(), - }; - - return DATA; -} diff --git a/frontend/src/static/js/utils/settings/config.ts b/frontend/src/static/js/utils/settings/config.ts new file mode 100644 index 00000000..4fa1733a --- /dev/null +++ b/frontend/src/static/js/utils/settings/config.ts @@ -0,0 +1,50 @@ +import { GlobalMediaCMS, MediaCMSConfig } from '../../types'; +import { apiConfig } from './api'; +import { contentsConfig } from './contents'; +import { mediaConfig } from './media'; +import { memberConfig } from './member'; +import { notificationsConfig } from './notifications'; +import { optionsEmbeddedConfig } from './optionsEmbedded'; +import { optionsPagesConfig } from './optionsPages'; +import { pagesConfig } from './pages'; +import { playlistsConfig } from './playlists'; +import { sidebarConfig } from './sidebar'; +import { siteConfig } from './site'; +import { taxonomiesConfig } from './taxonomies'; +import { themeConfig } from './theme'; +import { urlConfig } from './url'; + +let DATA: MediaCMSConfig | null = null; + +export function config(globalObj: GlobalMediaCMS) { + if (DATA) { + return DATA; + } + + const { api, contents, features, pages, site, user } = globalObj; + + const enabledPages = pagesConfig({ ...site.pages, ...site.userPages }); + + DATA = { + api: apiConfig(site.api, api), + contents: contentsConfig(contents), + enabled: { + pages: enabledPages, + taxonomies: taxonomiesConfig(site.taxonomies), + }, + media: mediaConfig(features.mediaItem, features.media.shareOptions), + member: memberConfig(user, features), + notifications: notificationsConfig(contents.notifications), + options: { + pages: optionsPagesConfig(pages.home, pages.search, pages.media, pages.profile, enabledPages), + embedded: optionsEmbeddedConfig(features.embeddedVideo), + }, + playlists: playlistsConfig(features.playlists), + sidebar: sidebarConfig(features.sideBar), + site: siteConfig(site), + theme: themeConfig(site.theme, site.logo), + url: urlConfig(globalObj), + }; + + return DATA; +} diff --git a/frontend/src/static/js/utils/settings/contents.js b/frontend/src/static/js/utils/settings/contents.js deleted file mode 100755 index 3080b023..00000000 --- a/frontend/src/static/js/utils/settings/contents.js +++ /dev/null @@ -1,121 +0,0 @@ -let CONTENTS = null; - -function headerContents(contents) { - const ret = { - right: '', - onLogoRight: '', - }; - - if (void 0 !== contents) { - if ('string' === typeof contents.right) { - ret.right = contents.right.trim(); - } - - if ('string' === typeof contents.onLogoRight) { - ret.onLogoRight = contents.onLogoRight.trim(); - } - } - - return ret; -} - -function sidebarContents(contents) { - const ret = { - navMenu: { - items: [], - }, - mainMenuExtra: { - items: [], - }, - belowNavMenu: '', - belowThemeSwitcher: '', - footer: '', - }; - - if (undefined !== contents) { - if (undefined !== contents.mainMenuExtraItems) { - let i = 0; - while (i < contents.mainMenuExtraItems.length) { - if ( - 'string' === typeof contents.mainMenuExtraItems[i].text && - 'string' === typeof contents.mainMenuExtraItems[i].link && - 'string' === typeof contents.mainMenuExtraItems[i].icon - ) { - ret.mainMenuExtra.items.push({ - text: contents.mainMenuExtraItems[i].text, - link: contents.mainMenuExtraItems[i].link, - icon: contents.mainMenuExtraItems[i].icon, - className: contents.mainMenuExtraItems[i].className, - }); - } - - i += 1; - } - } - - if (undefined !== contents.navMenuItems) { - let i = 0; - while (i < contents.navMenuItems.length) { - if ( - 'string' === typeof contents.navMenuItems[i].text && - 'string' === typeof contents.navMenuItems[i].link && - 'string' === typeof contents.navMenuItems[i].icon - ) { - ret.navMenu.items.push({ - text: contents.navMenuItems[i].text, - link: contents.navMenuItems[i].link, - icon: contents.navMenuItems[i].icon, - className: contents.navMenuItems[i].className, - }); - } - - i += 1; - } - } - - if ('string' === typeof contents.belowNavMenu) { - ret.belowNavMenu = contents.belowNavMenu.trim(); - } - - if ('string' === typeof contents.belowThemeSwitcher) { - ret.belowThemeSwitcher = contents.belowThemeSwitcher.trim(); - } - - if ('string' === typeof contents.footer) { - ret.footer = contents.footer.trim(); - } - } - - return ret; -} - -function uploaderContents(contents) { - const ret = { - belowUploadArea: '', - postUploadMessage: '', - }; - - if (void 0 !== contents) { - if ('string' === typeof contents.belowUploadArea) { - ret.belowUploadArea = contents.belowUploadArea.trim(); - } - - if ('string' === typeof contents.postUploadMessage) { - ret.postUploadMessage = contents.postUploadMessage.trim(); - } - } - - return ret; -} - -export function init(contents) { - CONTENTS = { - header: headerContents(contents.header), - sidebar: sidebarContents(contents.sidebar), - uploader: uploaderContents(contents.uploader), - }; -} - -export function settings() { - return CONTENTS; -} diff --git a/frontend/src/static/js/utils/settings/contents.ts b/frontend/src/static/js/utils/settings/contents.ts new file mode 100755 index 00000000..6344771c --- /dev/null +++ b/frontend/src/static/js/utils/settings/contents.ts @@ -0,0 +1,67 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +const headerContents = (settings?: DeepPartial) => ({ + right: settings?.right !== undefined ? settings.right.trim() : '', + onLogoRight: settings?.onLogoRight !== undefined ? settings.onLogoRight.trim() : '', +}); + +function sidebarContents(settings?: DeepPartial) { + const sidebar: MediaCMSConfig['contents']['sidebar'] = { + belowNavMenu: settings?.belowNavMenu ? settings.belowNavMenu.trim() : '', + belowThemeSwitcher: settings?.belowThemeSwitcher ? settings.belowThemeSwitcher.trim() : '', + footer: settings?.footer ? settings.footer.trim() : '', + mainMenuExtra: { items: [] }, + navMenu: { items: [] }, + }; + + if (settings?.mainMenuExtraItems) { + for (const item of settings.mainMenuExtraItems) { + if (!item) { + continue; + } + + const text = item.text ? item.text.trim() : ''; + const link = item.link ? item.link.trim() : ''; + const icon = item.icon ? item.icon.trim() : ''; + + const className = item.className ? item.className.trim() : ''; + + if (text && link && icon) { + sidebar.mainMenuExtra.items.push({ text, link, icon, className }); + } + } + } + + if (settings?.navMenuItems) { + for (const item of settings.navMenuItems) { + if (!item) { + continue; + } + + const text = item.text ? item.text.trim() : ''; + const link = item.link ? item.link.trim() : ''; + const icon = item.icon ? item.icon.trim() : ''; + + const className = item.className ? item.className.trim() : ''; + + if (text && link && icon) { + sidebar.navMenu.items.push({ text, link, icon, className }); + } + } + } + + return sidebar; +} + +const uploaderContents = (settings?: DeepPartial) => ({ + belowUploadArea: settings?.belowUploadArea ? settings?.belowUploadArea.trim() : '', + postUploadMessage: settings?.postUploadMessage ? settings?.postUploadMessage.trim() : '', +}); + +export const contentsConfig = ( + settings?: DeepPartial> +): MediaCMSConfig['contents'] => ({ + header: headerContents(settings?.header), + sidebar: sidebarContents(settings?.sidebar), + uploader: uploaderContents(settings?.uploader), +}); diff --git a/frontend/src/static/js/utils/settings/media.js b/frontend/src/static/js/utils/settings/media.js deleted file mode 100755 index fee51cfb..00000000 --- a/frontend/src/static/js/utils/settings/media.js +++ /dev/null @@ -1,49 +0,0 @@ -let MEDIA = null; - -export function init(item, shareOptions) { - MEDIA = { - item: { - displayAuthor: true, - displayViews: true, - displayPublishDate: true, - }, - share: { - options: [], - }, - }; - - if (void 0 !== item) { - - if (true === item.hideAuthor) { - MEDIA.item.displayAuthor = false; - } - - if (true === item.hideViews) { - MEDIA.item.displayViews = false; - } - - if (true === item.hideDate) { - MEDIA.item.displayPublishDate = false; - } - } - - if (void 0 !== shareOptions) { - const validShareOptions = [ - 'embed', - 'email', - ]; - - let i = 0; - while (i < shareOptions.length) { - if (-1 < validShareOptions.indexOf(shareOptions[i])) { - MEDIA.share.options.push(shareOptions[i]); - } - - i += 1; - } - } -} - -export function settings() { - return MEDIA; -} diff --git a/frontend/src/static/js/utils/settings/media.ts b/frontend/src/static/js/utils/settings/media.ts new file mode 100755 index 00000000..b025c68a --- /dev/null +++ b/frontend/src/static/js/utils/settings/media.ts @@ -0,0 +1,33 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function mediaConfig( + item?: DeepPartial, + shareOptions?: DeepPartial +) { + const ret: MediaCMSConfig['media'] = { + item: { + displayAuthor: item?.hideAuthor === true ? false : true, + displayViews: item?.hideViews === true ? false : true, + displayPublishDate: item?.hideDate === true ? false : true, + }, + share: { options: [] }, + }; + + if (shareOptions) { + const validShareOptions = ['embed', 'email']; // @todo: Check this + + for (const option of shareOptions) { + if (!option) { + continue; + } + + const opt = option.trim(); + + if (validShareOptions.includes(opt)) { + ret.share.options.push(opt); + } + } + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/member.js b/frontend/src/static/js/utils/settings/member.js deleted file mode 100755 index 834aad83..00000000 --- a/frontend/src/static/js/utils/settings/member.js +++ /dev/null @@ -1,135 +0,0 @@ -let MEMBER = null; - -export function init(user, features) { - MEMBER = { - name: null, - username: null, - thumbnail: null, - is: { - admin: false, - anonymous: true, - }, - can: { - login: true, - register: true, - addMedia: false, - editProfile: false, - canSeeMembersPage: true, - usersNeedsToBeApproved: true, - changePassword: true, - deleteProfile: false, - readComment: true, - addComment: false, - mentionComment: false, - deleteComment: false, - editMedia: false, - deleteMedia: false, - editSubtitle: false, - manageMedia: false, - manageUsers: false, - manageComments: false, - reportMedia: false, - downloadMedia: false, - saveMedia: false, - likeMedia: true, - dislikeMedia: true, - shareMedia: true, - contactUser: false, - }, - pages: { - home: null, - about: null, - media: null, - playlists: null, - }, - }; - - if (void 0 !== user) { - MEMBER.is.anonymous = true === user.is.anonymous ? true : false; - - if (!MEMBER.is.anonymous) { - MEMBER.is.admin = true === user.is.admin; - - MEMBER.name = 'string' === typeof user.name ? user.name.trim() : ''; - MEMBER.name = '' === MEMBER.name ? null : MEMBER.name; - - MEMBER.username = 'string' === typeof user.username ? user.username.trim() : ''; - MEMBER.username = '' === MEMBER.username ? null : MEMBER.username; - - MEMBER.thumbnail = 'string' === typeof user.thumbnail ? user.thumbnail.trim() : ''; - MEMBER.thumbnail = '' === MEMBER.thumbnail ? null : MEMBER.thumbnail; - - MEMBER.can.changePassword = false === user.can.changePassword ? false : MEMBER.can.changePassword; - - MEMBER.can.deleteProfile = true === user.can.deleteProfile; - MEMBER.can.addComment = true === user.can.addComment; - MEMBER.can.mentionComment = true === user.can.mentionComment; - MEMBER.can.deleteComment = true === user.can.deleteComment; - MEMBER.can.editMedia = true === user.can.editMedia; - MEMBER.can.deleteMedia = true === user.can.deleteMedia; - MEMBER.can.editSubtitle = true === user.can.editSubtitle; - MEMBER.can.manageMedia = true === user.can.manageMedia; - MEMBER.can.manageUsers = true === user.can.manageUsers; - MEMBER.can.manageComments = true === user.can.manageComments; - - MEMBER.can.contactUser = true === user.can.contactUser; - - if (void 0 !== user.pages) { - - if ('string' === typeof user.pages.about) { - MEMBER.pages.about = user.pages.about.trim(); - MEMBER.pages.about = '' === MEMBER.pages.about ? null : MEMBER.pages.about; - } - - if ('string' === typeof user.pages.media) { - MEMBER.pages.media = user.pages.media.trim(); - MEMBER.pages.media = '' === MEMBER.pages.media ? null : MEMBER.pages.media; - } - - if ('string' === typeof user.pages.playlists) { - MEMBER.pages.playlists = user.pages.playlists.trim(); - MEMBER.pages.playlists = '' === MEMBER.pages.playlists ? null : MEMBER.pages.playlists; - } - } - } - - MEMBER.can.canSeeMembersPage = true === user.can.canSeeMembersPage; - MEMBER.can.usersNeedsToBeApproved = true === user.can.usersNeedsToBeApproved; - MEMBER.can.addMedia = true === user.can.addMedia; - MEMBER.can.editProfile = true === user.can.editProfile; - MEMBER.can.readComment = false === user.can.readComment ? false : true; - } - - if (void 0 !== features) { - if (void 0 !== features.media) { - if (void 0 !== features.media.actions) { - const mediaActions = features.media.actions; - - MEMBER.can.addComment = MEMBER.can.addComment && true === mediaActions.comment; - MEMBER.can.mentionComment = MEMBER.can.mentionComment && true === mediaActions.comment_mention; - - MEMBER.can.likeMedia = false === mediaActions.like ? false : true; - MEMBER.can.dislikeMedia = false === mediaActions.dislike ? false : true; - MEMBER.can.reportMedia = false === mediaActions.report ? false : true; - - MEMBER.can.downloadMedia = true === mediaActions.download; - MEMBER.can.saveMedia = true === mediaActions.save; - MEMBER.can.shareMedia = true === mediaActions.share; - } - } - - if (void 0 !== features.headerBar) { - if (true === features.headerBar.hideLogin) { - MEMBER.can.login = false; - } - - if (true === features.headerBar.hideRegister) { - MEMBER.can.register = false; - } - } - } -} - -export function settings() { - return MEMBER; -} diff --git a/frontend/src/static/js/utils/settings/member.ts b/frontend/src/static/js/utils/settings/member.ts new file mode 100755 index 00000000..3fed890e --- /dev/null +++ b/frontend/src/static/js/utils/settings/member.ts @@ -0,0 +1,98 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function memberConfig( + user?: DeepPartial, + features?: { + headerBar?: DeepPartial; + media?: { actions?: DeepPartial }; + } +) { + const ret: MediaCMSConfig['member'] = { + name: null, + username: null, + thumbnail: null, + is: { admin: false, anonymous: true }, + can: { + login: true, + register: true, + addMedia: false, + editProfile: false, + canSeeMembersPage: true, + usersNeedsToBeApproved: true, + changePassword: true, + deleteProfile: false, + readComment: true, + addComment: false, + mentionComment: false, + deleteComment: false, + editMedia: false, + deleteMedia: false, + editSubtitle: false, + manageMedia: false, + manageUsers: false, + manageComments: false, + reportMedia: false, + downloadMedia: false, + saveMedia: false, + likeMedia: true, + dislikeMedia: true, + shareMedia: true, + contactUser: false, + }, + pages: { home: null, about: null, media: null, playlists: null }, + }; + + if (user) { + ret.is.anonymous = user.is?.anonymous === false ? false : true; + + if (!ret.is.anonymous) { + ret.is.admin = user.is?.admin === true; + + ret.name = (user.name ? user.name.trim() : null) || null; + ret.username = (user.username ? user.username.trim() : null) || null; + ret.thumbnail = (user.thumbnail ? user.thumbnail.trim() : null) || null; + ret.can.changePassword = user.can?.changePassword === false ? false : true; + + ret.can.deleteProfile = user.can?.deleteProfile === true; + ret.can.addComment = user.can?.addComment === true; + ret.can.mentionComment = user.can?.mentionComment === true; + ret.can.deleteComment = user.can?.deleteComment === true; + ret.can.editMedia = user.can?.editMedia === true; + ret.can.deleteMedia = user.can?.deleteMedia === true; + ret.can.editSubtitle = user.can?.editSubtitle === true; + ret.can.manageMedia = user.can?.manageMedia === true; + ret.can.manageUsers = user.can?.manageUsers === true; + ret.can.manageComments = user.can?.manageComments === true; + ret.can.contactUser = user.can?.contactUser === true; + + ret.pages.about = (user.pages?.about ? user.pages.about.trim() : null) || null; + ret.pages.media = (user.pages?.media ? user.pages.media.trim() : null) || null; + ret.pages.playlists = (user.pages?.playlists ? user.pages.playlists.trim() : null) || null; + } + + ret.can.canSeeMembersPage = user.can?.canSeeMembersPage === false ? false : true; + ret.can.usersNeedsToBeApproved = user.can?.usersNeedsToBeApproved === false ? false : true; + ret.can.addMedia = user.can?.addMedia === true; + ret.can.editProfile = user.can?.editProfile === true; + ret.can.readComment = user.can?.readComment === false ? false : true; + } + + const mediaActions = features?.media?.actions; + if (mediaActions !== undefined) { + ret.can.addComment = ret.can.addComment && mediaActions?.comment === true; + ret.can.mentionComment = ret.can.mentionComment && mediaActions?.comment_mention === true; + + ret.can.likeMedia = mediaActions?.like === false ? false : true; + ret.can.dislikeMedia = mediaActions?.dislike === false ? false : true; + ret.can.reportMedia = mediaActions?.report === false ? false : true; + + ret.can.downloadMedia = mediaActions?.download === true; + ret.can.saveMedia = mediaActions?.save === true; + ret.can.shareMedia = mediaActions?.share === true; + } + + ret.can.login = features?.headerBar?.hideLogin === true ? false : true; + ret.can.register = features?.headerBar?.hideRegister === true ? false : true; + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/notifications.js b/frontend/src/static/js/utils/settings/notifications.js deleted file mode 100644 index 2447c356..00000000 --- a/frontend/src/static/js/utils/settings/notifications.js +++ /dev/null @@ -1,32 +0,0 @@ -let NOTIFICATIONS = null; - -export function init(settings) { - NOTIFICATIONS = { - messages: { - addToLiked: 'Added to liked media', - removeFromLiked: 'Removed from liked media', - addToDisliked: 'Added to disliked media', - removeFromDisliked: 'Removed from disliked media', - }, - }; - - let k, g; - - if (void 0 !== settings) { - for (k in NOTIFICATIONS) { - if (void 0 !== settings[k]) { - if ('messages' === k) { - for (g in NOTIFICATIONS[k]) { - if ('string' === typeof settings[k][g]) { - NOTIFICATIONS[k][g] = settings[k][g]; - } - } - } - } - } - } -} - -export function settings() { - return NOTIFICATIONS; -} diff --git a/frontend/src/static/js/utils/settings/notifications.ts b/frontend/src/static/js/utils/settings/notifications.ts new file mode 100644 index 00000000..f3fbcebd --- /dev/null +++ b/frontend/src/static/js/utils/settings/notifications.ts @@ -0,0 +1,27 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function notificationsConfig(settings?: DeepPartial) { + const ret: MediaCMSConfig['notifications'] = { + messages: { + addToLiked: 'Added to liked media', + removeFromLiked: 'Removed from liked media', + addToDisliked: 'Added to disliked media', + removeFromDisliked: 'Removed from disliked media', + }, + }; + + if (!settings?.messages) { + return ret; + } + + const entries = Object.entries(settings.messages) as [keyof typeof settings.messages, string][]; + + for (const [key, value] of entries) { + const message = value?.trim(); + if (message) { + ret.messages[key] = message; + } + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/optionsEmbedded.js b/frontend/src/static/js/utils/settings/optionsEmbedded.js deleted file mode 100755 index d5d201a3..00000000 --- a/frontend/src/static/js/utils/settings/optionsEmbedded.js +++ /dev/null @@ -1,42 +0,0 @@ -let EMBEDDED = null; - -export function init(embeddedVideo) { - EMBEDDED = { - video: { - dimensions: { - width: 560, - widthUnit: 'px', // Valid values: 'px', 'percent' - height: 315, - heightUnit: 'px', // Valid values: 'px', 'percent' - }, - }, - }; - - if (void 0 !== embeddedVideo) { - if (void 0 !== embeddedVideo.initialDimensions) { - if (!isNaN(embeddedVideo.initialDimensions.width)) { - EMBEDDED.video.dimensions.width = embeddedVideo.initialDimensions.width; - } - - if ('string' === typeof embeddedVideo.initialDimensions.widthUnit) { - if ('percent' === embeddedVideo.initialDimensions.widthUnit) { - embeddedVideo.initialDimensions.widthUnit = 'percent'; - } - } - - if (!isNaN(embeddedVideo.initialDimensions.height)) { - EMBEDDED.video.dimensions.height = embeddedVideo.initialDimensions.height; - } - - if ('string' === typeof embeddedVideo.initialDimensions.heightUnit) { - if ('percent' === embeddedVideo.initialDimensions.heightUnit) { - embeddedVideo.initialDimensions.heightUnit = 'percent'; - } - } - } - } -} - -export function settings() { - return EMBEDDED; -} diff --git a/frontend/src/static/js/utils/settings/optionsEmbedded.ts b/frontend/src/static/js/utils/settings/optionsEmbedded.ts new file mode 100755 index 00000000..12f78856 --- /dev/null +++ b/frontend/src/static/js/utils/settings/optionsEmbedded.ts @@ -0,0 +1,45 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function optionsEmbeddedConfig(settings?: DeepPartial) { + const ret: MediaCMSConfig['options']['embedded'] = { + video: { + dimensions: { + width: 560, + widthUnit: 'px', + height: 315, + heightUnit: 'px', + }, + }, + }; + + if (!settings?.initialDimensions) { + return ret; + } + + const { + height, + width, + // heightUnit, // @note: It doesn't used + // widthUnit // @note: It doesn't used + } = settings.initialDimensions; + + if ('number' === typeof width && !Number.isNaN(width)) { + ret.video.dimensions.width = width; + } + + if ('number' === typeof height && !Number.isNaN(height)) { + ret.video.dimensions.height = height; + } + + // @note: It doesn't used + // if (widthUnit?.trim() === 'percent') { + // settings.initialDimensions.widthUnit = 'percent'; + // } + + // @note: It doesn't used + // if (heightUnit?.trim() === 'percent') { + // settings.initialDimensions.heightUnit = 'percent'; + // } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/optionsPages.js b/frontend/src/static/js/utils/settings/optionsPages.js deleted file mode 100755 index a668bf9c..00000000 --- a/frontend/src/static/js/utils/settings/optionsPages.js +++ /dev/null @@ -1,108 +0,0 @@ -let PAGES = null; - -export function init(home, search, media, profile, VALID_PAGES) { - PAGES = { - home: { - sections: { - latest: { - title: '', - }, - featured: { - title: '', - }, - recommended: { - title: '', - }, - }, - }, - search: { - advancedFilters: false, - }, - media: { - categoriesWithTitle: false, - htmlInDescription: false, - displayViews: true, - related: { - initialSize: 10, - }, - }, - profile: { - htmlInDescription: false, - includeHistory: false, - includeLikedMedia: false, - }, - }; - - if (void 0 !== home) { - if (void 0 !== home.sections) { - if (void 0 !== home.sections.latest) { - if ('string' === typeof home.sections.latest.title) { - PAGES.home.sections.latest.title = home.sections.latest.title.trim(); - } - } - - if (void 0 !== home.sections.featured) { - if ('string' === typeof home.sections.featured.title) { - PAGES.home.sections.featured.title = home.sections.featured.title.trim(); - } - } - - if (void 0 !== home.sections.recommended) { - if ('string' === typeof home.sections.recommended.title) { - PAGES.home.sections.recommended.title = home.sections.recommended.title.trim(); - } - } - } - } - - if (void 0 !== search) { - if (true === search.advancedFilters) { - PAGES.search.advancedFilters = search.advancedFilters; - } - } - - if ('' === PAGES.home.sections.latest.title) { - PAGES.home.sections.latest.title = void 0 !== VALID_PAGES.latest ? VALID_PAGES.latest.title : 'Latest'; - } - - if ('' === PAGES.home.sections.featured.title) { - PAGES.home.sections.featured.title = void 0 !== VALID_PAGES.featured ? VALID_PAGES.featured.title : 'Featured'; - } - - if ('' === PAGES.home.sections.recommended.title) { - PAGES.home.sections.recommended.title = - void 0 !== VALID_PAGES.recommended ? VALID_PAGES.recommended.title : 'Recommended'; - } - - if (void 0 !== media) { - if (true === media.categoriesWithTitle) { - PAGES.media.categoriesWithTitle = media.categoriesWithTitle; - } - - if (true === media.hideViews) { - PAGES.media.displayViews = false; - } - - if (true === media.htmlInDescription) { - PAGES.media.htmlInDescription = media.htmlInDescription; - } - } - - if (void 0 !== profile) { - if (true === profile.htmlInDescription) { - PAGES.profile.htmlInDescription = profile.htmlInDescription; - } - - if (true === profile.includeHistory) { - PAGES.profile.includeHistory = profile.includeHistory; - } - - if (true === profile.includeLikedMedia) { - PAGES.profile.includeLikedMedia = profile.includeLikedMedia; - } - } -} - -export function settings() { - return PAGES; -} diff --git a/frontend/src/static/js/utils/settings/optionsPages.ts b/frontend/src/static/js/utils/settings/optionsPages.ts new file mode 100755 index 00000000..fb9fdc31 --- /dev/null +++ b/frontend/src/static/js/utils/settings/optionsPages.ts @@ -0,0 +1,63 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function optionsPagesConfig( + home?: DeepPartial, + search?: DeepPartial, + media?: DeepPartial, + profile?: DeepPartial, + VALID_PAGES?: MediaCMSConfig['enabled']['pages'] +) { + const ret: MediaCMSConfig['options']['pages'] = { + home: { + sections: { + latest: { title: VALID_PAGES?.latest?.title || 'Latest' }, + featured: { title: VALID_PAGES?.featured?.title || 'Featured' }, + recommended: { title: VALID_PAGES?.recommended?.title || 'Recommended' }, + }, + }, + search: { + advancedFilters: search?.advancedFilters === true, + }, + media: { + categoriesWithTitle: media?.categoriesWithTitle === true, + htmlInDescription: media?.htmlInDescription === true, + displayViews: media?.hideViews === true ? false : true, + related: { + initialSize: + 'number' === typeof media?.related?.initialSize && !Number.isNaN(media.related.initialSize) + ? media.related.initialSize + : 10, + }, + }, + profile: { + htmlInDescription: profile?.htmlInDescription === true, + includeHistory: profile?.includeHistory === true, + includeLikedMedia: profile?.includeLikedMedia === true, + }, + }; + + if (home?.sections) { + if (typeof home.sections.latest?.title === 'string') { + const latestTitle = home.sections.latest.title.trim(); + if (latestTitle !== '') { + ret.home.sections.latest.title = latestTitle; + } + } + + if (typeof home.sections.featured?.title === 'string') { + const featuredTitle = home.sections.featured.title.trim(); + if (featuredTitle !== '') { + ret.home.sections.featured.title = featuredTitle; + } + } + + if (typeof home.sections.recommended?.title === 'string') { + const recommendedTitle = home.sections.recommended.title.trim(); + if (recommendedTitle !== '') { + ret.home.sections.recommended.title = recommendedTitle; + } + } + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/pages.js b/frontend/src/static/js/utils/settings/pages.js deleted file mode 100755 index 8b723e89..00000000 --- a/frontend/src/static/js/utils/settings/pages.js +++ /dev/null @@ -1,50 +0,0 @@ -let PAGES = null; - -export function init(settings) { - PAGES = { - latest: { - enabled: false, - title: 'Recent uploads', - }, - featured: { - enabled: false, - title: 'Featured', - }, - recommended: { - enabled: false, - title: 'Recommended', - }, - members: { - enabled: false, - title: 'Members', - }, - liked: { - enabled: false, - title: 'Liked media', - }, - history: { - enabled: false, - title: 'History', - }, - }; - - if (void 0 !== settings) { - for (let k in PAGES) { - if (void 0 !== settings[k]) { - PAGES[k].enabled = true; - - if (void 0 !== settings[k].enabled && false === settings[k].enabled) { - PAGES[k].enabled = false; - } - - if ('string' === typeof settings[k].title) { - PAGES[k].title = settings[k].title.trim(); - } - } - } - } -} - -export function settings() { - return PAGES; -} diff --git a/frontend/src/static/js/utils/settings/pages.ts b/frontend/src/static/js/utils/settings/pages.ts new file mode 100755 index 00000000..85bb3eea --- /dev/null +++ b/frontend/src/static/js/utils/settings/pages.ts @@ -0,0 +1,30 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function pagesConfig( + settings?: DeepPartial & DeepPartial +) { + const ret: MediaCMSConfig['enabled']['pages'] = { + latest: { enabled: false, title: 'Recent uploads' }, + featured: { enabled: false, title: 'Featured' }, + recommended: { enabled: false, title: 'Recommended' }, + members: { enabled: false, title: 'Members' }, + liked: { enabled: false, title: 'Liked media' }, + history: { enabled: false, title: 'History' }, + }; + + for (let sk in settings) { + const key = sk as keyof typeof settings; + + if (!ret[key]) { + continue; + } + + ret[key].enabled = settings[key]?.enabled === false ? false : true; + + if (settings[key]?.title !== undefined) { + ret[key].title = settings[key].title.trim(); + } + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/playlists.js b/frontend/src/static/js/utils/settings/playlists.js deleted file mode 100755 index 7f383c92..00000000 --- a/frontend/src/static/js/utils/settings/playlists.js +++ /dev/null @@ -1,35 +0,0 @@ -let PLAYLISTS = null; - -export function init(plists) { - PLAYLISTS = { - mediaTypes: [], - }; - - if (void 0 !== plists) { - if (void 0 !== plists.mediaTypes) { - if (plists.mediaTypes.length) { - PLAYLISTS.mediaTypes = []; - - let i = 0; - while (i < plists.mediaTypes.length) { - switch (plists.mediaTypes[i]) { - case 'audio': - case 'video': - PLAYLISTS.mediaTypes.push(plists.mediaTypes[i]); - break; - } - - i += 1; - } - } - } - } - - if (!PLAYLISTS.mediaTypes.length) { - PLAYLISTS.mediaTypes = ['audio', 'video']; - } -} - -export function settings() { - return PLAYLISTS; -} diff --git a/frontend/src/static/js/utils/settings/playlists.ts b/frontend/src/static/js/utils/settings/playlists.ts new file mode 100755 index 00000000..8656969a --- /dev/null +++ b/frontend/src/static/js/utils/settings/playlists.ts @@ -0,0 +1,19 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function playlistsConfig(settings?: DeepPartial) { + const ret: MediaCMSConfig['playlists'] = { mediaTypes: [] }; + + if (Array.isArray(settings?.mediaTypes)) { + for (const mtype of settings.mediaTypes) { + if (mtype === 'audio' || mtype === 'video') { + ret.mediaTypes.push(mtype); + } + } + } + + if (ret.mediaTypes.length === 0) { + ret.mediaTypes = ['audio', 'video']; + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/sidebar.js b/frontend/src/static/js/utils/settings/sidebar.js deleted file mode 100644 index 784103ad..00000000 --- a/frontend/src/static/js/utils/settings/sidebar.js +++ /dev/null @@ -1,27 +0,0 @@ -let SIDEBAR = null; - -export function init(settings) { - SIDEBAR = { - hideHomeLink: false, - hideTagsLink: false, - hideCategoriesLink: false, - }; - - if (void 0 !== settings) { - if ('boolean' === typeof settings.hideHomeLink) { - SIDEBAR.hideHomeLink = settings.hideHomeLink; - } - - if ('boolean' === typeof settings.hideTagsLink) { - SIDEBAR.hideTagsLink = settings.hideTagsLink; - } - - if ('boolean' === typeof settings.hideCategoriesLink) { - SIDEBAR.hideCategoriesLink = settings.hideCategoriesLink; - } - } -} - -export function settings() { - return SIDEBAR; -} diff --git a/frontend/src/static/js/utils/settings/sidebar.ts b/frontend/src/static/js/utils/settings/sidebar.ts new file mode 100644 index 00000000..611add4c --- /dev/null +++ b/frontend/src/static/js/utils/settings/sidebar.ts @@ -0,0 +1,9 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export const sidebarConfig = ( + settings?: DeepPartial +): MediaCMSConfig['sidebar'] => ({ + hideHomeLink: settings?.hideHomeLink === true, + hideTagsLink: settings?.hideTagsLink === true, + hideCategoriesLink: settings?.hideCategoriesLink === true, +}); diff --git a/frontend/src/static/js/utils/settings/site.js b/frontend/src/static/js/utils/settings/site.js deleted file mode 100755 index b6a3844a..00000000 --- a/frontend/src/static/js/utils/settings/site.js +++ /dev/null @@ -1,42 +0,0 @@ -let SITE = null; - -export function init(settings) { - SITE = { - id: 'media-cms', - url: '', - api: '', - title: '', - useRoundedCorners: true, - version: '1.0.0', - }; - - if (void 0 !== settings) { - if ('string' === typeof settings.id) { - SITE.id = settings.id.trim(); - } - - if ('string' === typeof settings.url) { - SITE.url = settings.url.trim(); - } - - if ('string' === typeof settings.api) { - SITE.api = settings.api.trim(); - } - - if ('string' === typeof settings.title) { - SITE.title = settings.title.trim(); - } - - if ('boolean' === typeof settings.useRoundedCorners) { - SITE.useRoundedCorners = settings.useRoundedCorners; - } - - if ('string' === typeof settings.version) { - SITE.version = settings.version.trim(); - } - } -} - -export function settings() { - return SITE; -} diff --git a/frontend/src/static/js/utils/settings/site.ts b/frontend/src/static/js/utils/settings/site.ts new file mode 100755 index 00000000..cfd06209 --- /dev/null +++ b/frontend/src/static/js/utils/settings/site.ts @@ -0,0 +1,10 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export const siteConfig = (settings?: DeepPartial): MediaCMSConfig['site'] => ({ + id: settings?.id?.trim() ?? 'media-cms', + url: settings?.url?.trim() ?? '', + api: settings?.api?.trim() ?? '', + title: settings?.title?.trim() ?? '', + useRoundedCorners: settings?.useRoundedCorners === false ? false : true, + version: settings?.version?.trim() ?? '1.0.0', // @todo: Validate version format +}); diff --git a/frontend/src/static/js/utils/settings/taxonomies.js b/frontend/src/static/js/utils/settings/taxonomies.js deleted file mode 100755 index d685e225..00000000 --- a/frontend/src/static/js/utils/settings/taxonomies.js +++ /dev/null @@ -1,34 +0,0 @@ -let TAXONOMIES = null; - -export function init(settings) { - TAXONOMIES = { - tags: { - enabled: false, - title: 'Tags', - }, - categories: { - enabled: false, - title: 'Categories', - }, - }; - - if (void 0 !== settings) { - for (let k in TAXONOMIES) { - if (void 0 !== settings[k]) { - TAXONOMIES[k].enabled = true; - - if (void 0 !== settings[k].enabled && false === settings[k].enabled) { - TAXONOMIES[k].enabled = false; - } - - if ('string' === typeof settings[k].title) { - TAXONOMIES[k].title = settings[k].title.trim(); - } - } - } - } -} - -export function settings() { - return TAXONOMIES; -} diff --git a/frontend/src/static/js/utils/settings/taxonomies.ts b/frontend/src/static/js/utils/settings/taxonomies.ts new file mode 100755 index 00000000..be8332ca --- /dev/null +++ b/frontend/src/static/js/utils/settings/taxonomies.ts @@ -0,0 +1,25 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function taxonomiesConfig(settings?: DeepPartial) { + const ret: MediaCMSConfig['enabled']['taxonomies'] = { + tags: { enabled: false, title: 'Tags' }, + categories: { enabled: false, title: 'Categories' }, + }; + + // @todo: Similar code in 'pages.ts' + for (let sk in settings) { + const key = sk as keyof typeof settings; + + if (!ret[key]) { + continue; + } + + ret[key].enabled = settings[key]?.enabled === false ? false : true; // @todo: Check this again + + if (settings[key]?.title !== undefined) { + ret[key].title = settings[key].title.trim(); + } + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/theme.js b/frontend/src/static/js/utils/settings/theme.js deleted file mode 100755 index 177e88f3..00000000 --- a/frontend/src/static/js/utils/settings/theme.js +++ /dev/null @@ -1,65 +0,0 @@ -let THEME = null; - -export function init(theme, logo) { - THEME = { - mode: 'light', // Valid options: 'light', 'dark'. - switch: { - enabled: true, - position: 'header', // Valid options: 'header', 'sidebar'. - }, - logo: { - lightMode: { - img: '', - svg: '', - }, - darkMode: { - img: '', - svg: '', - }, - }, - }; - - if (void 0 !== theme) { - if ('string' === typeof theme.mode) { - THEME.mode = theme.mode.trim(); - THEME.mode = 'dark' === THEME.mode ? 'dark' : 'light'; - } - - if (void 0 !== theme.switch) { - if (false === theme.switch.enabled) { - THEME.switch.enabled = theme.switch.enabled; - } - - if ('string' === typeof theme.switch.position) { - THEME.switch.position = theme.switch.position.trim(); - THEME.switch.position = 'sidebar' === theme.switch.position ? 'sidebar' : 'header'; - } - } - } - - if (void 0 !== logo) { - if (void 0 !== logo.lightMode) { - if ('string' === typeof logo.lightMode.img) { - THEME.logo.lightMode.img = logo.lightMode.img.trim(); - } - - if ('string' === typeof logo.lightMode.svg) { - THEME.logo.lightMode.svg = logo.lightMode.svg.trim(); - } - } - - if (void 0 !== logo.darkMode) { - if ('string' === typeof logo.darkMode.img) { - THEME.logo.darkMode.img = logo.darkMode.img.trim(); - } - - if ('string' === typeof logo.darkMode.svg) { - THEME.logo.darkMode.svg = logo.darkMode.svg.trim(); - } - } - } -} - -export function settings() { - return THEME; -} diff --git a/frontend/src/static/js/utils/settings/theme.ts b/frontend/src/static/js/utils/settings/theme.ts new file mode 100755 index 00000000..f346336c --- /dev/null +++ b/frontend/src/static/js/utils/settings/theme.ts @@ -0,0 +1,51 @@ +import { DeepPartial, GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export function themeConfig( + theme?: DeepPartial, + logo?: DeepPartial +) { + const ret: MediaCMSConfig['theme'] = { + mode: 'light', + switch: { enabled: true, position: 'header' }, + logo: { lightMode: { img: '', svg: '' }, darkMode: { img: '', svg: '' } }, + }; + + if (theme) { + if (theme.mode?.trim() === 'dark') { + ret.mode = 'dark'; + } + + if (theme.switch) { + if (theme.switch.enabled === false) { + ret.switch.enabled = false; + } + if (theme.switch.position?.trim() === 'sidebar') { + ret.switch.position = 'sidebar'; + } + } + } + + if (logo) { + if (logo.lightMode) { + if (logo.lightMode.img) { + ret.logo.lightMode.img = logo.lightMode.img.trim(); + } + + if (logo.lightMode.svg) { + ret.logo.lightMode.svg = logo.lightMode.svg.trim(); + } + } + + if (logo.darkMode) { + if (logo.darkMode?.img) { + ret.logo.darkMode.img = logo.darkMode.img.trim(); + } + + if (logo.darkMode?.svg) { + ret.logo.darkMode.svg = logo.darkMode.svg.trim(); + } + } + } + + return ret; +} diff --git a/frontend/src/static/js/utils/settings/url.js b/frontend/src/static/js/utils/settings/url.js deleted file mode 100755 index f7bf9ec5..00000000 --- a/frontend/src/static/js/utils/settings/url.js +++ /dev/null @@ -1,13 +0,0 @@ -let PAGES = null; - -export function init(pages_url) { - PAGES = {}; - - for (let k in pages_url) { - PAGES[k] = pages_url[k]; - } -} - -export function pages() { - return PAGES; -} diff --git a/frontend/src/static/js/utils/settings/url.ts b/frontend/src/static/js/utils/settings/url.ts new file mode 100755 index 00000000..bbfe7288 --- /dev/null +++ b/frontend/src/static/js/utils/settings/url.ts @@ -0,0 +1,59 @@ +import { GlobalMediaCMS, MediaCMSConfig } from '../../types'; + +export const urlConfig = ({ + profileId, + site, + url, + user, +}: Pick): MediaCMSConfig['url'] => ({ + home: url.home, + admin: !user.is.anonymous && user.is.admin ? url.admin : '', + error404: url.error404, + embed: site.url.replace(/\/+$/, '') + '/embed?m=', + latest: url.latestMedia, + featured: url.featuredMedia, + recommended: url.recommendedMedia, + signin: url.signin, + signout: !user.is.anonymous ? url.signout : '', + register: url.register, + changePassword: !user.is.anonymous ? url.changePassword : '', + members: url.members, + search: { + base: url.search, + query: url.search + '?q=', + tag: url.search + '?t=', + category: url.search + '?c=', + }, + profile: + site.devEnv === true + ? { + media: user.pages.media, + about: user.pages.about, + playlists: user.pages.playlists, + shared_by_me: user.pages.media + '/shared_by_me', + shared_with_me: user.pages.media + '/shared_with_me', + } + : { + media: site.url.replace(/\/$/, '') + '/user/' + profileId, + about: site.url.replace(/\/$/, '') + '/user/' + profileId + '/about', + playlists: site.url.replace(/\/$/, '') + '/user/' + profileId + '/playlists', + shared_by_me: site.url.replace(/\/$/, '') + '/user/' + profileId + '/shared_by_me', + shared_with_me: site.url.replace(/\/$/, '') + '/user/' + profileId + '/shared_with_me', + }, + user: { + liked: url.likedMedia, + history: url.history, + addMedia: url.addMedia, + editChannel: url.editChannel, + editProfile: url.editProfile, + }, + archive: { + tags: url.tags, + categories: url.categories, + }, + manage: { + media: !user.is.anonymous ? url.manageMedia : '', + users: !user.is.anonymous ? url.manageUsers : '', + comments: !user.is.anonymous ? url.manageComments : '', + }, +}); diff --git a/frontend/tests/utils/settings/api.test.ts b/frontend/tests/utils/settings/api.test.ts index c94f08da..2c79d071 100644 --- a/frontend/tests/utils/settings/api.test.ts +++ b/frontend/tests/utils/settings/api.test.ts @@ -1,46 +1,40 @@ -import { init, endpoints } from '../../../src/static/js/utils/settings/api'; +import { apiConfig } from '../../../src/static/js/utils/settings/api'; -const apiConfig = (url: any, ep: any) => { - init(url, ep); - return endpoints(); -}; +const sampleGlobal = { + site: { api: 'https://example.com/api///' }, + // endpoints below intentionally contain leading slashes to ensure they are stripped + api: { + media: '/v1/media/', + playlists: '/v1/playlists', + members: '/v1/users', + liked: '/v1/user/liked', + history: '/v1/user/history', + tags: '/v1/tags', + categories: '/v1/categories', + manage_media: '/v1/manage/media', + manage_users: '/v1/manage/users', + manage_comments: '/v1/manage/comments', + search: '/v1/search', + }, +} as const; describe('utils/settings', () => { describe('api', () => { - const sampleGlobal = { - site: { api: 'https://example.com/api/v1///' }, - // The endpoints below intentionally contain leading slashes to ensure they are stripped - api: { - media: '/media/', - members: '/users//', - playlists: '/playlists', - liked: '/user/liked', - history: '/user/history', - tags: '/tags', - categories: '/categories', - manage_media: '/manage/media', - manage_users: '/manage/users', - manage_comments: '/manage/comments', - search: '/search', - }, - } as const; - - test('Trims trailing slashes on base and ensures single slash joins', () => { - const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api); - // @todo: Check again the cases of trailing slashes + test('trims trailing slashes on base and ensures single slash joins', () => { + const cfg = apiConfig(sampleGlobal.site.api as any, sampleGlobal.api as any); expect(cfg.media).toBe('https://example.com/api/v1/media/'); - expect(cfg.users).toBe('https://example.com/api/v1/users//'); + // base should not end with a slash and endpoint leading slash stripped + expect(cfg.users).toBe('https://example.com/api/v1/users'); }); - test('Adds featured/recommended query to media variants', () => { - const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api); + test('adds featured/recommended query to media variants', () => { + const cfg = apiConfig(sampleGlobal.site.api as any, sampleGlobal.api as any); expect(cfg.featured).toBe('https://example.com/api/v1/media/?show=featured'); expect(cfg.recommended).toBe('https://example.com/api/v1/media/?show=recommended'); }); - test('Builds nested user, archive, manage maps', () => { - const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api); - + test('builds nested user, archive, manage maps', () => { + const cfg = apiConfig(sampleGlobal.site.api as any, sampleGlobal.api as any); expect(cfg.user.liked).toBe('https://example.com/api/v1/user/liked'); expect(cfg.user.history).toBe('https://example.com/api/v1/user/history'); expect(cfg.user.playlists).toBe('https://example.com/api/v1/playlists?author='); @@ -53,22 +47,30 @@ describe('utils/settings', () => { expect(cfg.manage.comments).toBe('https://example.com/api/v1/manage/comments'); }); - test('Builds search endpoints with expected query fragments', () => { - const cfg = apiConfig(sampleGlobal.site.api, sampleGlobal.api); + test('builds search endpoints with expected query fragments', () => { + const cfg = apiConfig(sampleGlobal.site.api as any, sampleGlobal.api as any); expect(cfg.search.query).toBe('https://example.com/api/v1/search?q='); expect(cfg.search.titles).toBe('https://example.com/api/v1/search?show=titles&q='); expect(cfg.search.tag).toBe('https://example.com/api/v1/search?t='); expect(cfg.search.category).toBe('https://example.com/api/v1/search?c='); }); - test('Handles base url with path and endpoint with existing query', () => { - const cfg = apiConfig('https://example.com/base/', { + test('handles base url with path and endpoint with existing query', () => { + const base = 'https://example.com/base/'; + const endpoints = { media: 'items?x=1', playlists: '/pls/', + members: 'users', liked: 'me/liked', + history: 'me/history', + tags: 't', categories: '/c', + manage_media: 'm/media', + manage_users: 'm/users', + manage_comments: 'm/comments', search: '/s', - }); + } as any; + const cfg = apiConfig(base as any, endpoints); expect(cfg.media).toBe('https://example.com/base/items?x=1'); expect(cfg.playlists).toBe('https://example.com/base/pls/'); expect(cfg.user.liked).toBe('https://example.com/base/me/liked'); diff --git a/frontend/tests/utils/settings/config.test.ts b/frontend/tests/utils/settings/config.test.ts index 43d4f859..e3935172 100644 --- a/frontend/tests/utils/settings/config.test.ts +++ b/frontend/tests/utils/settings/config.test.ts @@ -1,140 +1,144 @@ import { config } from '../../../src/static/js/utils/settings/config'; +// The aggregator pulls all sub-configs together. We validate composition and memoization. +// Behaviors to test: +// 1) Builds enabled pages by merging site.pages and site.userPages and passes to optionsPages +// 2) Produces api endpoints derived from site.api + api endpoints +// 3) Honors user/feature flags within member capabilities and url manage links visibility +// 4) Applies theme and logo mapping and site defaults when missing +// 5) Memoizes result and returns same object instance on subsequent calls + +// @todo: Replace 'baseGlobal' with 'sampleGlobalMediaCMS' and add missing properties value checks. +const baseGlobal = { + profileId: 'john', + site: { + id: 'my-site', + url: 'https://example.com/', + api: 'https://example.com/api/', + title: 'Example', + theme: { mode: 'dark', switch: { enabled: true, position: 'sidebar' } }, + logo: { + lightMode: { img: '/img/light.png', svg: '/img/light.svg' }, + darkMode: { img: '/img/dark.png', svg: '/img/dark.svg' }, + }, + devEnv: false, + useRoundedCorners: true, + version: '2.0.0', + taxonomies: { + tags: { enabled: true, title: 'Topic Tags' }, + categories: { enabled: false, title: 'Kinds' }, + }, + pages: { + latest: { enabled: true, title: 'Recent uploads' }, + featured: { enabled: true, title: 'Featured picks' }, + recommended: { enabled: false, title: 'You may like' }, + }, + userPages: { + members: { enabled: true, title: 'People' }, + liked: { enabled: true, title: 'Favorites' }, + history: { enabled: true, title: 'Watched' }, + }, + }, + url: { + home: '/', + admin: '/admin', + error404: '/404', + latestMedia: '/latest', + featuredMedia: '/featured', + recommendedMedia: '/recommended', + signin: '/signin', + signout: '/signout', + register: '/register', + changePassword: '/password', + members: '/members', + search: '/search', + likedMedia: '/liked', + history: '/history', + addMedia: '/add', + editChannel: '/edit/channel', + editProfile: '/edit/profile', + tags: '/tags', + categories: '/categories', + manageMedia: '/manage/media', + manageUsers: '/manage/users', + manageComments: '/manage/comments', + }, + api: { + media: 'v1/media/', + playlists: 'v1/playlists', + members: 'v1/users', + liked: 'v1/user/liked', + history: 'v1/user/history', + tags: 'v1/tags', + categories: 'v1/categories', + manage_media: 'v1/manage/media', + manage_users: 'v1/manage/users', + manage_comments: 'v1/manage/comments', + search: 'v1/search', + }, + contents: { + notifications: { + messages: { addToLiked: 'Yay', removeFromLiked: 'Oops', addToDisliked: 'nay', removeFromDisliked: 'ok' }, + }, + }, + pages: { + home: { sections: { latest: { title: 'Latest T' } } }, + search: { advancedFilters: true }, + media: { categoriesWithTitle: true, hideViews: true, related: { initialSize: 5 } }, + profile: { htmlInDescription: true, includeHistory: true, includeLikedMedia: true }, + }, + features: { + mediaItem: { hideAuthor: true, hideViews: false, hideDate: true }, + media: { + actions: { + like: true, + dislike: true, + report: true, + comment: true, + comment_mention: true, + download: true, + save: true, + share: true, + }, + shareOptions: ['embed', 'email', 'invalid'], + }, + playlists: { mediaTypes: ['audio'] }, + sideBar: { hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false }, + embeddedVideo: { initialDimensions: { width: 640, height: 360 } }, + headerBar: { hideLogin: false, hideRegister: true }, + }, + user: { + is: { anonymous: false, admin: true }, + name: ' John ', + username: ' john ', + thumbnail: ' /img/j.png ', + can: { + changePassword: true, + deleteProfile: true, + addComment: true, + mentionComment: true, + deleteComment: true, + editMedia: true, + deleteMedia: true, + editSubtitle: true, + manageMedia: true, + manageUsers: true, + manageComments: true, + contactUser: true, + canSeeMembersPage: true, + usersNeedsToBeApproved: false, + addMedia: true, + editProfile: true, + readComment: true, + }, + pages: { about: '/u/john/about ', media: '/u/john ', playlists: '/u/john/playlists ' }, + }, +} as const; + describe('utils/settings', () => { describe('config', () => { - const baseGlobal = { - profileId: 'john', - site: { - id: 'my-site', - url: 'https://example.com/', - api: 'https://example.com/api/', - title: 'Example', - theme: { mode: 'dark', switch: { enabled: true, position: 'sidebar' } }, - logo: { - lightMode: { img: '/img/light.png', svg: '/img/light.svg' }, - darkMode: { img: '/img/dark.png', svg: '/img/dark.svg' }, - }, - devEnv: false, - useRoundedCorners: true, - version: '2.0.0', - taxonomies: { - tags: { enabled: true, title: 'Topic Tags' }, - categories: { enabled: false, title: 'Kinds' }, - }, - pages: { - latest: { enabled: true, title: 'Recent uploads' }, - featured: { enabled: true, title: 'Featured picks' }, - recommended: { enabled: false, title: 'You may like' }, - }, - userPages: { - members: { enabled: true, title: 'People' }, - liked: { enabled: true, title: 'Favorites' }, - history: { enabled: true, title: 'Watched' }, - }, - }, - url: { - home: '/', - admin: '/admin', - error404: '/404', - latestMedia: '/latest', - featuredMedia: '/featured', - recommendedMedia: '/recommended', - signin: '/signin', - signout: '/signout', - register: '/register', - changePassword: '/password', - members: '/members', - search: '/search', - likedMedia: '/liked', - history: '/history', - addMedia: '/add', - editChannel: '/edit/channel', - editProfile: '/edit/profile', - tags: '/tags', - categories: '/categories', - manageMedia: '/manage/media', - manageUsers: '/manage/users', - manageComments: '/manage/comments', - }, - api: { - media: 'v1/media/', - playlists: 'v1/playlists', - members: 'v1/users', - liked: 'v1/user/liked', - history: 'v1/user/history', - tags: 'v1/tags', - categories: 'v1/categories', - manage_media: 'v1/manage/media', - manage_users: 'v1/manage/users', - manage_comments: 'v1/manage/comments', - search: 'v1/search', - }, - contents: { - notifications: { - messages: { - addToLiked: 'Yay', - removeFromLiked: 'Oops', - addToDisliked: 'nay', - removeFromDisliked: 'ok', - }, - }, - }, - pages: { - home: { sections: { latest: { title: 'Latest T' } } }, - search: { advancedFilters: true }, - media: { categoriesWithTitle: true, hideViews: true, related: { initialSize: 5 } }, - profile: { htmlInDescription: true, includeHistory: true, includeLikedMedia: true }, - }, - features: { - mediaItem: { hideAuthor: true, hideViews: false, hideDate: true }, - media: { - actions: { - like: true, - dislike: true, - report: true, - comment: true, - comment_mention: true, - download: true, - save: true, - share: true, - }, - shareOptions: ['embed', 'email', 'invalid'], - }, - playlists: { mediaTypes: ['audio'] }, - sideBar: { hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false }, - embeddedVideo: { initialDimensions: { width: 640, height: 360 } }, - headerBar: { hideLogin: false, hideRegister: true }, - }, - user: { - is: { anonymous: false, admin: true }, - name: ' John ', - username: ' john ', - thumbnail: ' /img/j.png ', - can: { - changePassword: true, - deleteProfile: true, - addComment: true, - mentionComment: true, - deleteComment: true, - editMedia: true, - deleteMedia: true, - editSubtitle: true, - manageMedia: true, - manageUsers: true, - manageComments: true, - contactUser: true, - canSeeMembersPage: true, - usersNeedsToBeApproved: false, - addMedia: true, - editProfile: true, - readComment: true, - }, - pages: { about: '/u/john/about ', media: '/u/john ', playlists: '/u/john/playlists ' }, - }, - } as const; - test('merges enabled pages and passes titles into options.pages.home sections', () => { - const cfg = config(baseGlobal); + const cfg = config(baseGlobal as any); expect(cfg.enabled.pages.latest).toStrictEqual({ enabled: true, title: 'Recent uploads' }); expect(cfg.enabled.pages.featured).toStrictEqual({ enabled: true, title: 'Featured picks' }); expect(cfg.enabled.pages.recommended).toStrictEqual({ enabled: false, title: 'You may like' }); @@ -144,14 +148,14 @@ describe('utils/settings', () => { }); test('produces api endpoints based on site.api and api endpoints', () => { - const cfg = config(baseGlobal); + const cfg = config(baseGlobal as any); expect(cfg.api.media).toBe('https://example.com/api/v1/media/'); expect(cfg.api.user.liked).toBe('https://example.com/api/v1/user/liked'); expect(cfg.api.search.query).toBe('https://example.com/api/v1/search?q='); }); test('member and url manage links reflect user and feature flags', () => { - const cfg = config(baseGlobal); + const cfg = config(baseGlobal as any); expect(cfg.member.is).toStrictEqual({ admin: true, anonymous: false }); expect(cfg.member.can).toMatchObject({ manageMedia: true, @@ -166,7 +170,7 @@ describe('utils/settings', () => { }); test('theme and site defaults propagate correctly', () => { - const cfg = config(baseGlobal); + const cfg = config(baseGlobal as any); expect(cfg.theme.mode).toBe('dark'); expect(cfg.theme.switch.position).toBe('sidebar'); expect(cfg.theme.logo.darkMode.img).toBe('/img/dark.png'); @@ -175,13 +179,13 @@ describe('utils/settings', () => { }); test('memoizes and returns the same object instance on repeated calls', () => { - const first = config(baseGlobal); - const second = config(baseGlobal); + const first = config(baseGlobal as any); + const second = config(baseGlobal as any); expect(second).toBe(first); }); test('url profile paths use site.url when not in dev env', () => { - const cfg = config(baseGlobal); + const cfg = config(baseGlobal as any); expect(cfg.url.profile.media).toBe('https://example.com/user/john'); expect(cfg.url.embed).toBe('https://example.com/embed?m='); }); diff --git a/frontend/tests/utils/settings/contents.test.ts b/frontend/tests/utils/settings/contents.test.ts index e486ac78..120778f3 100644 --- a/frontend/tests/utils/settings/contents.test.ts +++ b/frontend/tests/utils/settings/contents.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/contents'; - -const contentsConfig = (obj: any) => { - init(obj); - return settings(); -}; +import { contentsConfig } from '../../../src/static/js/utils/settings/contents'; describe('utils/settings', () => { describe('contents', () => { @@ -53,8 +48,7 @@ describe('utils/settings', () => { }); }); - // @todo: Revisit this behavior - test('Sidebar menu items require text, link, icon and NOT get trimmed', () => { + test('Sidebar menu items require text, link, icon and get trimmed', () => { const cfg = contentsConfig({ sidebar: { mainMenuExtraItems: [ @@ -62,22 +56,63 @@ describe('utils/settings', () => { { text: 'no-link', icon: 'i' }, { link: '/missing-text', icon: 'i' }, { text: 'no-icon', link: '/x' }, + null as any, + undefined, ], navMenuItems: [ { text: ' B ', link: ' /b ', icon: ' i-b ' }, { text: ' ', link: '/bad', icon: 'i' }, + null as any, + undefined, ], }, }); - expect(cfg.sidebar.mainMenuExtra.items).toStrictEqual([ - { text: ' A ', link: ' /a ', icon: ' i-a ', className: ' cls ' }, - ]); + expect(cfg.sidebar.mainMenuExtra.items).toEqual([{ text: 'A', link: '/a', icon: 'i-a', className: 'cls' }]); - expect(cfg.sidebar.navMenu.items).toStrictEqual([ - { text: ' B ', link: ' /b ', icon: ' i-b ', className: undefined }, - { text: ' ', link: '/bad', icon: 'i', className: undefined }, - ]); + expect(cfg.sidebar.navMenu.items).toEqual([{ text: 'B', link: '/b', icon: 'i-b', className: '' }]); + }); + + test('sidebar strings are trimmed or default to empty', () => { + const cfg = contentsConfig({ + sidebar: { + belowNavMenu: ' X ', + belowThemeSwitcher: ' Y ', + footer: ' Z ', + }, + } as any); + + expect(cfg.sidebar.belowNavMenu).toBe('X'); + expect(cfg.sidebar.belowThemeSwitcher).toBe('Y'); + expect(cfg.sidebar.footer).toBe('Z'); + + const cfg2 = contentsConfig({ sidebar: {} } as any); + expect(cfg2.sidebar.belowNavMenu).toBe(''); + expect(cfg2.sidebar.belowThemeSwitcher).toBe(''); + expect(cfg2.sidebar.footer).toBe(''); + }); + + test('uploader strings are trimmed or default to empty', () => { + const cfg = contentsConfig({ + uploader: { belowUploadArea: ' U1 ', postUploadMessage: ' U2 ' }, + } as any); + + expect(cfg.uploader.belowUploadArea).toBe('U1'); + expect(cfg.uploader.postUploadMessage).toBe('U2'); + + const cfg2 = contentsConfig({ uploader: {} } as any); + expect(cfg2.uploader.belowUploadArea).toBe(''); + expect(cfg2.uploader.postUploadMessage).toBe(''); + }); + + test('handles completely missing settings by returning defaults', () => { + const cfg = contentsConfig(undefined as any); + expect(cfg.header.right).toBe(''); + expect(cfg.header.onLogoRight).toBe(''); + expect(cfg.sidebar.mainMenuExtra.items).toEqual([]); + expect(cfg.sidebar.navMenu.items).toEqual([]); + expect(cfg.sidebar.footer).toBe(''); + expect(cfg.uploader.postUploadMessage).toBe(''); }); }); }); diff --git a/frontend/tests/utils/settings/media.test.ts b/frontend/tests/utils/settings/media.test.ts index c521a25d..952cb0b6 100644 --- a/frontend/tests/utils/settings/media.test.ts +++ b/frontend/tests/utils/settings/media.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/media'; - -const mediaConfig = (item?: any, shareOptions?: any) => { - init(item, shareOptions); - return settings(); -}; +import { mediaConfig } from '../../../src/static/js/utils/settings/media'; describe('utils/settings', () => { describe('media', () => { @@ -27,14 +22,17 @@ describe('utils/settings', () => { expect(cfg.share.options).toEqual([]); }); - // @todo: Revisit this behavior test('Filters share options to valid ones and trims whitespace', () => { - const cfg = mediaConfig(undefined, [' embed ', 'email', ' email ']); - expect(cfg.share.options).toEqual(['email']); + const cfg = mediaConfig(undefined, [' embed ', 'email', ' email '] as unknown as Array< + 'embed' | 'email' | undefined + >); + expect(cfg.share.options).toEqual(['embed', 'email', 'email']); // @todo: Revisit this. }); test('Ignores falsy and invalid share options', () => { - const cfg = mediaConfig(undefined, [undefined, '', ' ', 'invalid', 'share', 'EMBED']); + const cfg = mediaConfig(undefined, [undefined, '', ' ', 'invalid', 'share', 'EMBED'] as unknown as Array< + 'embed' | 'email' | undefined + >); expect(cfg.share.options).toEqual([]); }); }); diff --git a/frontend/tests/utils/settings/member.test.ts b/frontend/tests/utils/settings/member.test.ts index 541910e1..262cef24 100644 --- a/frontend/tests/utils/settings/member.test.ts +++ b/frontend/tests/utils/settings/member.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/member'; - -const memberConfig = (user?: any, features?: any) => { - init(user, features); - return settings(); -}; +import { memberConfig } from '../../../src/static/js/utils/settings/member'; describe('utils/settings', () => { describe('member', () => { @@ -56,8 +51,8 @@ describe('utils/settings', () => { can: { changePassword: true, deleteProfile: true, - addComment: true, - mentionComment: true, + addComment: false, + mentionComment: false, deleteComment: true, editMedia: true, deleteMedia: true, @@ -90,8 +85,8 @@ describe('utils/settings', () => { changePassword: true, deleteProfile: true, readComment: true, - addComment: true, - mentionComment: true, + addComment: false, + mentionComment: false, deleteComment: true, editMedia: true, deleteMedia: true, @@ -127,6 +122,12 @@ describe('utils/settings', () => { expect(cfg2.can.mentionComment).toBe(true); }); + test('Preserves comment capabilities from user.can when media.actions is missing', () => { + const cfg = memberConfig({ is: { anonymous: false }, can: { addComment: true, mentionComment: true } }); + expect(cfg.can.addComment).toBe(true); + expect(cfg.can.mentionComment).toBe(true); + }); + test('Header login/register reflect headerBar feature flags', () => { expect(memberConfig(undefined, { headerBar: { hideLogin: true } }).can.login).toBe(false); expect(memberConfig(undefined, { headerBar: { hideRegister: true } }).can.register).toBe(false); @@ -149,6 +150,16 @@ describe('utils/settings', () => { expect(cfg1.can.saveMedia).toBe(true); }); + test('Applies legacy defaults when media.actions exists but fields are missing', () => { + const cfg = memberConfig(undefined, { media: { actions: {} } }); + expect(cfg.can.likeMedia).toBe(true); + expect(cfg.can.dislikeMedia).toBe(true); + expect(cfg.can.reportMedia).toBe(true); + expect(cfg.can.downloadMedia).toBe(false); + expect(cfg.can.saveMedia).toBe(false); + expect(cfg.can.shareMedia).toBe(false); + }); + test('User flags canSeeMembersPage/usersNeedsToBeApproved/readComment default handling', () => { const cfg1 = memberConfig({ is: { anonymous: false }, diff --git a/frontend/tests/utils/settings/notifications.test.ts b/frontend/tests/utils/settings/notifications.test.ts index c5fdc438..3acbce01 100644 --- a/frontend/tests/utils/settings/notifications.test.ts +++ b/frontend/tests/utils/settings/notifications.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/notifications'; - -const notificationsConfig = (sett?: any) => { - init(sett); - return settings(); -}; +import { notificationsConfig } from '../../../src/static/js/utils/settings/notifications'; describe('utils/settings', () => { describe('notifications', () => { @@ -19,8 +14,7 @@ describe('utils/settings', () => { }); }); - // @todo: Revisit this behavior - test('Keep incoming message values ​​without processing', () => { + test('Trims incoming message values and applies only when non-empty', () => { const cfg = notificationsConfig({ messages: { addToLiked: ' Yay ', @@ -29,18 +23,19 @@ describe('utils/settings', () => { removeFromDisliked: '\t OK\t', }, }); - expect(cfg.messages.addToLiked).toBe(' Yay '); - expect(cfg.messages.removeFromLiked).toBe(' '); - expect(cfg.messages.addToDisliked).toBe('\nNope'); - expect(cfg.messages.removeFromDisliked).toBe('\t OK\t'); + expect(cfg.messages.addToLiked).toBe('Yay'); + // empty after trim -> keep default + expect(cfg.messages.removeFromLiked).toBe('Removed from liked media'); + expect(cfg.messages.addToDisliked).toBe('Nope'); + expect(cfg.messages.removeFromDisliked).toBe('OK'); }); - test('Ignores undefined, keeping defaults', () => { + test('Ignores undefined or empty-string overrides, keeping defaults', () => { const cfg = notificationsConfig({ messages: { addToLiked: undefined, - removeFromLiked: undefined, - addToDisliked: undefined, + removeFromLiked: '', + addToDisliked: ' ', removeFromDisliked: undefined, }, }); @@ -59,9 +54,18 @@ describe('utils/settings', () => { }); test('Handles extraneous keys by passing them through while keeping known defaults intact', () => { - const cfg = notificationsConfig({ messages: { addToLiked: 'A', notARealKey: 'x' } }); - expect(cfg.messages.notARealKey).toBeUndefined(); + const cfg = notificationsConfig({ + messages: { + addToLiked: 'A', + // Inject an unknown key; current implementation passes unknown keys through + ...{ notARealKey: 'x' }, + }, + }); + expect(cfg.messages.addToLiked).toBe('A'); + // extraneous key currently copied over + expect((cfg.messages as any).notARealKey).toBe('x'); + // sanity check known defaults remain for untouched keys expect(cfg.messages.removeFromLiked).toBe('Removed from liked media'); expect(cfg.messages.addToDisliked).toBe('Added to disliked media'); expect(cfg.messages.removeFromDisliked).toBe('Removed from disliked media'); diff --git a/frontend/tests/utils/settings/optionsEmbedded.test.ts b/frontend/tests/utils/settings/optionsEmbedded.test.ts index cda7578b..55d27467 100644 --- a/frontend/tests/utils/settings/optionsEmbedded.test.ts +++ b/frontend/tests/utils/settings/optionsEmbedded.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/optionsEmbedded'; - -const optionsEmbeddedConfig = (embeddedVideo?: any) => { - init(embeddedVideo); - return settings(); -}; +import { optionsEmbeddedConfig } from '../../../src/static/js/utils/settings/optionsEmbedded'; describe('utils/settings', () => { describe('optionsEmbedded', () => { @@ -22,32 +17,30 @@ describe('utils/settings', () => { expect(cfg.video.dimensions).toStrictEqual({ width: 640, widthUnit: 'px', height: 360, heightUnit: 'px' }); }); - // @todo: Revisit this behavior test('Ignores NaN and non-numeric width/height and keeps defaults', () => { - const cfg1 = optionsEmbeddedConfig({ initialDimensions: { width: NaN, height: NaN } }); + const cfg1 = optionsEmbeddedConfig({ initialDimensions: { width: NaN, height: NaN } } as any); expect(cfg1.video.dimensions).toStrictEqual({ width: 560, widthUnit: 'px', height: 315, heightUnit: 'px' }); - const cfg2 = optionsEmbeddedConfig({ initialDimensions: { width: '640', height: '360' } }); - expect(cfg2.video.dimensions).toStrictEqual({ - width: '640', - widthUnit: 'px', - height: '360', - heightUnit: 'px', - }); + const cfg2 = optionsEmbeddedConfig({ initialDimensions: { width: '640', height: '360' } } as any); + expect(cfg2.video.dimensions).toStrictEqual({ width: 560, widthUnit: 'px', height: 315, heightUnit: 'px' }); }); - // @todo: Revisit this behavior test('Ignores provided widthUnit/heightUnit as they are not used', () => { const cfg = optionsEmbeddedConfig({ - initialDimensions: { width: 800, height: 450, widthUnit: 'percent', heightUnit: 'percent' }, - }); + initialDimensions: { + width: 800, + height: 450, + widthUnit: 'percent', + heightUnit: 'percent', + }, + } as any); + // units should remain default 'px' expect(cfg.video.dimensions.width).toBe(800); expect(cfg.video.dimensions.height).toBe(450); expect(cfg.video.dimensions.widthUnit).toBe('px'); expect(cfg.video.dimensions.heightUnit).toBe('px'); }); - // @todo: Revisit this behavior test('Does not mutate the provided settings object', () => { const input = { initialDimensions: { width: 700, height: 400, widthUnit: 'percent', heightUnit: 'percent' }, diff --git a/frontend/tests/utils/settings/optionsPages.test.ts b/frontend/tests/utils/settings/optionsPages.test.ts index 2eddfb6b..0c26861e 100644 --- a/frontend/tests/utils/settings/optionsPages.test.ts +++ b/frontend/tests/utils/settings/optionsPages.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/optionsPages'; - -const optionsPagesConfig = (home?: any, search?: any, media?: any, profile?: any, VALID_PAGES?: any) => { - init(home, search, media, profile, VALID_PAGES); - return settings(); -}; +import { optionsPagesConfig } from '../../../src/static/js/utils/settings/optionsPages'; describe('utils/settings', () => { describe('optionsPages', () => { @@ -12,7 +7,8 @@ describe('utils/settings', () => { latest: { title: 'Recent' }, featured: { title: 'Spotlight' }, recommended: { title: 'You may like' }, - }); + } as any); + expect(cfg.home.sections.latest.title).toBe('Recent'); expect(cfg.home.sections.featured.title).toBe('Spotlight'); expect(cfg.home.sections.recommended.title).toBe('You may like'); @@ -26,86 +22,97 @@ describe('utils/settings', () => { recommended: { title: ' Recommended' }, }, }); + expect(cfg.home.sections.latest.title).toBe('LATEST'); expect(cfg.home.sections.featured.title).toBe('Featured'); expect(cfg.home.sections.recommended.title).toBe('Recommended'); }); + test('Falls back to VALID_PAGES titles when custom home section titles are whitespace-only', () => { + const cfg = optionsPagesConfig( + { + sections: { + latest: { title: ' ' }, + featured: { title: '\n\t' }, + recommended: { title: ' ' }, + }, + }, + undefined, + undefined, + undefined, + { + latest: { title: 'Recent' }, + featured: { title: 'Spotlight' }, + recommended: { title: 'You may like' }, + } as any + ); + + expect(cfg.home.sections.latest.title).toBe('Recent'); + expect(cfg.home.sections.featured.title).toBe('Spotlight'); + expect(cfg.home.sections.recommended.title).toBe('You may like'); + }); + test('Sets search.advancedFilters true only when explicitly true', () => { - const def = optionsPagesConfig(undefined, undefined, undefined, undefined, {}); + const def = optionsPagesConfig(); expect(def.search.advancedFilters).toBe(false); - const falsy = optionsPagesConfig(undefined, { advancedFilters: false }, undefined, undefined, {}); + const falsy = optionsPagesConfig(undefined, { advancedFilters: false } as any); expect(falsy.search.advancedFilters).toBe(false); - const truthy = optionsPagesConfig(undefined, { advancedFilters: true }, undefined, undefined, {}); + const truthy = optionsPagesConfig(undefined, { advancedFilters: true } as any); expect(truthy.search.advancedFilters).toBe(true); }); test('Configures media options with correct defaults and overrides', () => { - const def = optionsPagesConfig(undefined, undefined, undefined, undefined, {}); - + const def = optionsPagesConfig(); expect(def.media.categoriesWithTitle).toBe(false); expect(def.media.htmlInDescription).toBe(false); expect(def.media.displayViews).toBe(true); expect(def.media.related.initialSize).toBe(10); - const override = optionsPagesConfig( - undefined, - undefined, - { - categoriesWithTitle: true, - htmlInDescription: true, - hideViews: true, - related: { initialSize: 25 }, - }, - undefined, - {} - ); + const override = optionsPagesConfig(undefined, undefined, { + categoriesWithTitle: true, + htmlInDescription: true, + hideViews: true, + related: { initialSize: 25 }, + }); expect(override.media.categoriesWithTitle).toBe(true); expect(override.media.htmlInDescription).toBe(true); expect(override.media.displayViews).toBe(false); - expect(override.media.related.initialSize).toBe(10); // @todo: Fix this! It should return 25. + expect(override.media.related.initialSize).toBe(25); }); test('Ignores NaN and non-numeric media.related.initialSize and keeps default 10', () => { - const cfg1 = optionsPagesConfig(undefined, undefined, { related: { initialSize: NaN } }, undefined, {}); + const cfg1 = optionsPagesConfig(undefined, undefined, { related: { initialSize: NaN } } as any); expect(cfg1.media.related.initialSize).toBe(10); - const cfg2 = optionsPagesConfig(undefined, undefined, { related: { initialSize: '12' } }, undefined, {}); + const cfg2 = optionsPagesConfig(undefined, undefined, { related: { initialSize: '12' as any } } as any); expect(cfg2.media.related.initialSize).toBe(10); }); test('Profile settings true only when explicitly true', () => { - const def = optionsPagesConfig(undefined, undefined, undefined, undefined, {}); + const def = optionsPagesConfig(); expect(def.profile.htmlInDescription).toBe(false); expect(def.profile.includeHistory).toBe(false); expect(def.profile.includeLikedMedia).toBe(false); - const truthy = optionsPagesConfig( - undefined, - undefined, - undefined, - { - htmlInDescription: true, - includeHistory: true, - includeLikedMedia: true, - }, - {} - ); + const truthy = optionsPagesConfig(undefined, undefined, undefined, { + htmlInDescription: true, + includeHistory: true, + includeLikedMedia: true, + }); expect(truthy.profile.htmlInDescription).toBe(true); expect(truthy.profile.includeHistory).toBe(true); expect(truthy.profile.includeLikedMedia).toBe(true); }); - // @todo: Revisit this behavior test('Does not mutate provided input objects', () => { const home = { sections: { latest: { title: ' A ' } } }; const search = { advancedFilters: true }; const media = { hideViews: true, related: { initialSize: 5 } }; const profile = { includeHistory: true }; - const validPages = { latest: { title: 'L' }, featured: { title: 'F' }, recommended: { title: 'R' } }; + const validPages: any = { latest: { title: 'L' }, featured: { title: 'F' }, recommended: { title: 'R' } }; const homeCopy = JSON.parse(JSON.stringify(home)); const searchCopy = JSON.parse(JSON.stringify(search)); diff --git a/frontend/tests/utils/settings/pages.test.ts b/frontend/tests/utils/settings/pages.test.ts index afc00a98..13d1ca4a 100644 --- a/frontend/tests/utils/settings/pages.test.ts +++ b/frontend/tests/utils/settings/pages.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/pages'; - -const pagesConfig = (sett?: any) => { - init(sett); - return settings(); -}; +import { pagesConfig } from '../../../src/static/js/utils/settings/pages'; describe('utils/settings', () => { describe('pages', () => { @@ -25,9 +20,10 @@ describe('utils/settings', () => { featured: { enabled: true }, recommended: { enabled: false }, members: { enabled: undefined }, - liked: { enabled: null }, - history: { enabled: 0 }, + liked: { enabled: null as any }, + history: { enabled: 0 as any }, }); + expect(cfg.latest.enabled).toBe(true); expect(cfg.featured.enabled).toBe(true); expect(cfg.recommended.enabled).toBe(false); @@ -42,19 +38,28 @@ describe('utils/settings', () => { featured: { title: '\nFeatured' }, recommended: {}, }); + expect(cfg.latest.title).toBe('Latest'); expect(cfg.featured.title).toBe('Featured'); expect(cfg.recommended.title).toBe('Recommended'); }); test('Ignores unknown keys in settings', () => { - const cfg = pagesConfig({ unknownKey: { enabled: true, title: 'X' }, latest: { enabled: true } }); + const cfg = pagesConfig({ + // @ts-ignore + unknownKey: { enabled: true, title: 'X' }, + latest: { enabled: true }, + }); + expect(cfg.latest.enabled).toBe(true); - expect(cfg.unknownKey).toBeUndefined(); + expect((cfg as any).unknownKey).toBeUndefined(); }); test('Does not mutate the input settings object', () => { - const input = { latest: { enabled: false, title: ' A ' }, featured: { enabled: true, title: ' B ' } }; + const input = { + latest: { enabled: false, title: ' A ' }, + featured: { enabled: true, title: ' B ' }, + }; const snapshot = JSON.parse(JSON.stringify(input)); pagesConfig(input); expect(input).toStrictEqual(snapshot); diff --git a/frontend/tests/utils/settings/playlists.test.ts b/frontend/tests/utils/settings/playlists.test.ts index ec06e74d..d6a0c25a 100644 --- a/frontend/tests/utils/settings/playlists.test.ts +++ b/frontend/tests/utils/settings/playlists.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/playlists'; - -const playlistsConfig = (plists?: any) => { - init(plists); - return settings(); -}; +import { playlistsConfig } from '../../../src/static/js/utils/settings/playlists'; describe('utils/settings', () => { describe('playlists', () => { @@ -18,32 +13,33 @@ describe('utils/settings', () => { }); test('Includes only valid media types when both valid and invalid are provided', () => { - const cfg = playlistsConfig({ mediaTypes: ['audio', 'invalid', 'video', 'something'] }); + const cfg = playlistsConfig({ mediaTypes: ['audio', 'invalid', 'video', 'something'] as any }); expect(cfg.mediaTypes).toEqual(['audio', 'video']); }); test('Returns default when provided mediaTypes is non-array or undefined/null', () => { - expect(playlistsConfig({}).mediaTypes).toEqual(['audio', 'video']); - expect(playlistsConfig({ mediaTypes: undefined }).mediaTypes).toEqual(['audio', 'video']); - // expect(playlistsConfig({ mediaTypes: null }).mediaTypes).toEqual(['audio', 'video']); // @todo: Revisit this behavior - expect(playlistsConfig({ mediaTypes: 'audio' }).mediaTypes).toEqual(['audio', 'video']); - expect(playlistsConfig({ mediaTypes: 123 }).mediaTypes).toEqual(['audio', 'video']); + expect(playlistsConfig({} as any).mediaTypes).toEqual(['audio', 'video']); + expect(playlistsConfig({ mediaTypes: undefined } as any).mediaTypes).toEqual(['audio', 'video']); + expect(playlistsConfig({ mediaTypes: null as any }).mediaTypes).toEqual(['audio', 'video']); + expect(playlistsConfig({ mediaTypes: 'audio' as any }).mediaTypes).toEqual(['audio', 'video']); + expect(playlistsConfig({ mediaTypes: 123 as any }).mediaTypes).toEqual(['audio', 'video']); }); - // @todo: Revisit this behavior test('Handles duplicates and preserves order among valid items', () => { - const cfg = playlistsConfig({ mediaTypes: ['video', 'audio', 'video', 'audio', 'invalid'] }); + const cfg = playlistsConfig({ mediaTypes: ['video', 'audio', 'video', 'audio', 'invalid'] as any }); + // Implementation preserves order and includes duplicates; however, it later enforces default if empty only. + // Since duplicates are allowed by implementation, expect duplicates to be preserved. expect(cfg.mediaTypes).toEqual(['video', 'audio', 'video', 'audio']); }); - // @todo: Revisit this behavior test('Rejects non-exact case values (e.g., \"Audio\")', () => { - const cfg = playlistsConfig({ mediaTypes: ['Audio', 'Video'] }); + const cfg = playlistsConfig({ mediaTypes: ['Audio', 'Video'] as any }); + // None match exactly, so default should apply. expect(cfg.mediaTypes).toEqual(['audio', 'video']); }); test('does not mutate the input object', () => { - const input = { mediaTypes: ['audio', 'video', 'invalid'] }; + const input: any = { mediaTypes: ['audio', 'video', 'invalid'] }; const copy = JSON.parse(JSON.stringify(input)); playlistsConfig(input); expect(input).toEqual(copy); diff --git a/frontend/tests/utils/settings/sidebar.test.ts b/frontend/tests/utils/settings/sidebar.test.ts index 5330ba6d..e7e30595 100644 --- a/frontend/tests/utils/settings/sidebar.test.ts +++ b/frontend/tests/utils/settings/sidebar.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/sidebar'; - -const sidebarConfig = (sett?: any) => { - init(sett); - return settings(); -}; +import { sidebarConfig } from '../../../src/static/js/utils/settings/sidebar'; describe('utils/settings', () => { describe('sidebar', () => { @@ -32,14 +27,14 @@ describe('utils/settings', () => { // undefined expect(sidebarConfig({}).hideHomeLink).toBe(false); // null - expect(sidebarConfig({ hideTagsLink: null }).hideTagsLink).toBe(false); + expect(sidebarConfig({ hideTagsLink: null as any }).hideTagsLink).toBe(false); // other types - expect(sidebarConfig({ hideCategoriesLink: 'yes' }).hideCategoriesLink).toBe(false); - expect(sidebarConfig({ hideCategoriesLink: 1 }).hideCategoriesLink).toBe(false); + expect(sidebarConfig({ hideCategoriesLink: 'yes' as any }).hideCategoriesLink).toBe(false); + expect(sidebarConfig({ hideCategoriesLink: 1 as any }).hideCategoriesLink).toBe(false); }); test('Is resilient to partial inputs and ignores extra properties', () => { - const cfg = sidebarConfig({ hideTagsLink: true, extra: 'prop' }); + const cfg = sidebarConfig({ hideTagsLink: true, extra: 'prop' } as any); expect(cfg).toStrictEqual({ hideHomeLink: false, hideTagsLink: true, hideCategoriesLink: false }); }); diff --git a/frontend/tests/utils/settings/site.test.ts b/frontend/tests/utils/settings/site.test.ts index 664038ce..3e6b4f03 100644 --- a/frontend/tests/utils/settings/site.test.ts +++ b/frontend/tests/utils/settings/site.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/site'; - -const siteConfig = (sett?: any) => { - init(sett); - return settings(); -}; +import { siteConfig } from '../../../src/static/js/utils/settings/site'; describe('utils/settings', () => { describe('site', () => { @@ -27,6 +22,7 @@ describe('utils/settings', () => { title: ' Media CMS ', version: ' 2.3.4 ', }); + expect(cfg).toStrictEqual({ id: 'my-site', url: 'https://example.com/', @@ -42,22 +38,24 @@ describe('utils/settings', () => { expect(siteConfig({ useRoundedCorners: true }).useRoundedCorners).toBe(true); expect(siteConfig({ useRoundedCorners: false }).useRoundedCorners).toBe(false); // non-boolean should still evaluate to default true because only === false toggles it off - expect(siteConfig({ useRoundedCorners: 'no' }).useRoundedCorners).toBe(true); - expect(siteConfig({ useRoundedCorners: 0 }).useRoundedCorners).toBe(true); - expect(siteConfig({ useRoundedCorners: null }).useRoundedCorners).toBe(true); + expect(siteConfig({ useRoundedCorners: 'no' as any }).useRoundedCorners).toBe(true); + expect(siteConfig({ useRoundedCorners: 0 as any }).useRoundedCorners).toBe(true); + expect(siteConfig({ useRoundedCorners: null as any }).useRoundedCorners).toBe(true); }); test('Is resilient to partial inputs and ignores extra properties', () => { - const cfg = siteConfig({ id: ' x ', extra: 'y' }); + const cfg = siteConfig({ id: ' x ', extra: 'y' } as any); expect(cfg).toMatchObject({ id: 'x' }); - expect(Object.keys(cfg).sort()).toEqual(['api', 'id', 'title', 'url', 'useRoundedCorners', 'version']); + expect(Object.keys(cfg).sort()).toStrictEqual( + ['api', 'id', 'title', 'url', 'useRoundedCorners', 'version'].sort() + ); }); test('Does not mutate input object', () => { const input = { id: ' my-id ', useRoundedCorners: false }; const copy = JSON.parse(JSON.stringify(input)); siteConfig(input); - expect(input).toEqual(copy); + expect(input).toStrictEqual(copy); }); }); }); diff --git a/frontend/tests/utils/settings/taxonomies.test.ts b/frontend/tests/utils/settings/taxonomies.test.ts index 82f92d59..18f6d219 100644 --- a/frontend/tests/utils/settings/taxonomies.test.ts +++ b/frontend/tests/utils/settings/taxonomies.test.ts @@ -1,53 +1,51 @@ -import { init, settings } from '../../../src/static/js/utils/settings/taxonomies'; +import { taxonomiesConfig } from '../../../src/static/js/utils/settings/taxonomies'; -const taxonomiesConfig = (sett?: any) => { - init(sett); - return settings(); -}; - -describe('utils-settings/taxonomies', () => { - test('Should return defaults when settings is undefined', () => { - const res = taxonomiesConfig(); - expect(res).toStrictEqual({ - tags: { enabled: false, title: 'Tags' }, - categories: { enabled: false, title: 'Categories' }, +describe('utils/settings', () => { + describe('taxonomies', () => { + test('Should return defaults when settings is undefined', () => { + const res = taxonomiesConfig(); + expect(res).toStrictEqual({ + tags: { enabled: false, title: 'Tags' }, + categories: { enabled: false, title: 'Categories' }, + }); }); - }); - test('Should enable a taxonomy when enabled is true', () => { - const res = taxonomiesConfig({ tags: { enabled: true } }); - expect(res.tags).toStrictEqual({ enabled: true, title: 'Tags' }); - }); - - test('Should keep taxonomy disabled when enabled is true', () => { - const res = taxonomiesConfig({ categories: { enabled: true } }); - expect(res.categories).toStrictEqual({ enabled: true, title: 'Categories' }); - }); - - test('Should default to enabled=true when enabled is omitted but key exists', () => { - const res = taxonomiesConfig({ tags: {} }); - expect(res.tags).toStrictEqual({ enabled: true, title: 'Tags' }); - }); - - test('Should trim title when provided', () => { - const res = taxonomiesConfig({ tags: { title: ' My Tags ' } }); - expect(res.tags).toStrictEqual({ enabled: true, title: 'My Tags' }); - }); - - test('Should ignore unknown taxonomy keys', () => { - const input = { - unknownKey: { enabled: true, title: 'X' }, - tags: { enabled: true, title: 'Tagz' }, - }; - const res = taxonomiesConfig(input); - expect(res).toStrictEqual({ - tags: { enabled: true, title: 'Tagz' }, - categories: { enabled: false, title: 'Categories' }, + test('Should enable a taxonomy when enabled is true', () => { + const res = taxonomiesConfig({ tags: { enabled: true } }); + expect(res.tags).toStrictEqual({ enabled: true, title: 'Tags' }); }); - }); - test('Should not change title when title is undefined', () => { - const res = taxonomiesConfig({ categories: { enabled: true, title: undefined } }); - expect(res.categories).toStrictEqual({ enabled: true, title: 'Categories' }); + test('Should keep taxonomy disabled when enabled is explicitly false', () => { + const res = taxonomiesConfig({ categories: { enabled: false } }); + expect(res.categories).toStrictEqual({ enabled: false, title: 'Categories' }); + }); + + test('Should default to enabled=true when enabled is omitted but key exists', () => { + const res = taxonomiesConfig({ tags: {} }); + expect(res.tags).toStrictEqual({ enabled: true, title: 'Tags' }); + }); + + test('Should trim title when provided', () => { + const res = taxonomiesConfig({ tags: { title: ' My Tags ' } }); + expect(res.tags).toStrictEqual({ enabled: true, title: 'My Tags' }); + }); + + test('Should ignore unknown taxonomy keys', () => { + const input = { + unknownKey: { enabled: true, title: 'X' }, + tags: { enabled: true, title: 'Tagz' }, + }; + const res = taxonomiesConfig(input); + + expect(res).toStrictEqual({ + tags: { enabled: true, title: 'Tagz' }, + categories: { enabled: false, title: 'Categories' }, + }); + }); + + test('Should not change title when title is undefined', () => { + const res = taxonomiesConfig({ categories: { enabled: true, title: undefined } }); + expect(res.categories).toStrictEqual({ enabled: true, title: 'Categories' }); + }); }); }); diff --git a/frontend/tests/utils/settings/theme.test.ts b/frontend/tests/utils/settings/theme.test.ts index bd1a2b2d..02b7942a 100644 --- a/frontend/tests/utils/settings/theme.test.ts +++ b/frontend/tests/utils/settings/theme.test.ts @@ -1,9 +1,4 @@ -import { init, settings } from '../../../src/static/js/utils/settings/theme'; - -const themeConfig = (theme?: any, logo?: any) => { - init(theme, logo); - return settings(); -}; +import { themeConfig } from '../../../src/static/js/utils/settings/theme'; describe('utils/settings', () => { describe('theme', () => { @@ -18,10 +13,10 @@ describe('utils/settings', () => { test("Sets dark mode only when theme.mode is exactly 'dark' after trim", () => { expect(themeConfig({ mode: 'dark' }).mode).toBe('dark'); - expect(themeConfig({ mode: ' dark ' }).mode).toBe('dark'); - expect(themeConfig({ mode: 'Dark' }).mode).toBe('light'); + expect(themeConfig({ mode: ' dark ' } as any).mode).toBe('dark'); + expect(themeConfig({ mode: 'Dark' } as any).mode).toBe('light'); expect(themeConfig({ mode: 'light' }).mode).toBe('light'); - expect(themeConfig({ mode: ' ' }).mode).toBe('light'); + expect(themeConfig({ mode: ' ' } as any).mode).toBe('light'); }); test('Switch config: enabled only toggles off when explicitly false; position set to sidebar only when exactly sidebar after trim', () => { @@ -30,9 +25,9 @@ describe('utils/settings', () => { expect(themeConfig({ switch: { enabled: undefined } }).switch.enabled).toBe(true); expect(themeConfig({ switch: { position: 'sidebar' } }).switch.position).toBe('sidebar'); - expect(themeConfig({ switch: { position: ' sidebar ' } }).switch.position).toBe('header'); // @todo: Fix this. It should be 'sidebar' + expect(themeConfig({ switch: { position: ' sidebar ' } } as any).switch.position).toBe('sidebar'); expect(themeConfig({ switch: { position: 'header' } }).switch.position).toBe('header'); - expect(themeConfig({ switch: { position: 'foot' } }).switch.position).toBe('header'); + expect(themeConfig({ switch: { position: 'foot' } } as any).switch.position).toBe('header'); }); test('Trims and maps logo URLs for both light and dark modes; ignores missing fields', () => { @@ -63,7 +58,7 @@ describe('utils/settings', () => { }); test('Does not mutate input objects', () => { - const themeIn = { mode: ' dark ', switch: { enabled: false, position: ' sidebar ' } }; + const themeIn: any = { mode: ' dark ', switch: { enabled: false, position: ' sidebar ' } }; const logoIn = { lightMode: { img: ' x ', svg: ' y ' }, darkMode: { img: ' z ', svg: ' w ' } }; const themeCopy = JSON.parse(JSON.stringify(themeIn)); const logoCopy = JSON.parse(JSON.stringify(logoIn)); diff --git a/frontend/tests/utils/settings/url.test.ts b/frontend/tests/utils/settings/url.test.ts index 3357d228..7eb5786a 100644 --- a/frontend/tests/utils/settings/url.test.ts +++ b/frontend/tests/utils/settings/url.test.ts @@ -1,13 +1,8 @@ -import { init, pages } from '../../../src/static/js/utils/settings/url'; - -const urlConfig = (pages_url?: any) => { - init(pages_url); - return pages(); -}; +import { urlConfig } from '../../../src/static/js/utils/settings/url'; describe('utils/settings', () => { describe('url', () => { - const baseGlobal = { + const base = { profileId: 'john', site: { url: 'https://example.com/', @@ -43,57 +38,81 @@ describe('utils/settings', () => { }, } as const; - test('Authenticated non-admin user', () => { - const cfg = urlConfig(baseGlobal); - - expect(cfg).toStrictEqual({ - profileId: 'john', - site: { url: 'https://example.com/', devEnv: false }, - url: { - home: '/', - admin: '/admin', - error404: '/404', - latestMedia: '/latest', - featuredMedia: '/featured', - recommendedMedia: '/recommended', - signin: '/signin', - signout: '/signout', - register: '/register', - changePassword: '/password', - members: '/members', - search: '/search', - likedMedia: '/liked', - history: '/history', - addMedia: '/add', - editChannel: '/edit/channel', - editProfile: '/edit/profile', - tags: '/tags', - categories: '/categories', - manageMedia: '/manage/media', - manageUsers: '/manage/users', - manageComments: '/manage/comments', - }, - user: { - is: { anonymous: false, admin: false }, - pages: { media: '/u/john', about: '/u/john/about', playlists: '/u/john/playlists' }, - }, - }); + test('non-admin authenticated user: admin hidden, signout/changePassword visible, manage visible', () => { + const cfg = urlConfig(base as any); + expect(cfg.admin).toBe(''); + expect(cfg.signout).toBe('/signout'); + expect(cfg.changePassword).toBe('/password'); + expect(cfg.manage.media).toBe('/manage/media'); + expect(cfg.manage.users).toBe('/manage/users'); + expect(cfg.manage.comments).toBe('/manage/comments'); }); - test('Admin user', () => { + test('anonymous user: admin, signout, changePassword, manage all hidden', () => { const cfg = urlConfig({ - ...baseGlobal, - user: { ...baseGlobal.user, is: { anonymous: false, admin: true } }, - }); - expect(cfg.user.is).toStrictEqual({ anonymous: false, admin: true }); + ...base, + user: { ...base.user, is: { anonymous: true, admin: false } }, + } as any); + expect(cfg.admin).toBe(''); + expect(cfg.signout).toBe(''); + expect(cfg.changePassword).toBe(''); + expect(cfg.manage.media).toBe(''); + expect(cfg.manage.users).toBe(''); + expect(cfg.manage.comments).toBe(''); }); - test('Anonymous user', () => { + test('admin user: admin visible', () => { const cfg = urlConfig({ - ...baseGlobal, - user: { ...baseGlobal.user, is: { anonymous: true, admin: true } }, + ...base, + user: { ...base.user, is: { anonymous: false, admin: true } }, + } as any); + expect(cfg.admin).toBe('/admin'); + }); + + test('embed URL strips trailing slashes from site.url', () => { + const cfg1 = urlConfig(base as any); + expect(cfg1.embed).toBe('https://example.com/embed?m='); + + const cfg2 = urlConfig({ ...base, site: { ...base.site, url: 'https://example.com////' } } as any); + expect(cfg2.embed).toBe('https://example.com/embed?m='); + }); + + test('search URLs are composed correctly', () => { + const cfg = urlConfig(base as any); + expect(cfg.search.base).toBe('/search'); + expect(cfg.search.query).toBe('/search?q='); + expect(cfg.search.tag).toBe('/search?t='); + expect(cfg.search.category).toBe('/search?c='); + }); + + test('profile URLs: devEnv=false use site.url + profileId', () => { + const cfg = urlConfig(base as any); + expect(cfg.profile.media).toBe('https://example.com/user/john'); + expect(cfg.profile.about).toBe('https://example.com/user/john/about'); + expect(cfg.profile.playlists).toBe('https://example.com/user/john/playlists'); + expect(cfg.profile.shared_by_me).toBe('https://example.com/user/john/shared_by_me'); + expect(cfg.profile.shared_with_me).toBe('https://example.com/user/john/shared_with_me'); + }); + + test('profile URLs: devEnv=true use user.pages and append shared paths', () => { + const cfg = urlConfig({ ...base, site: { ...base.site, devEnv: true } } as any); + expect(cfg.profile.media).toBe('/u/john'); + expect(cfg.profile.about).toBe('/u/john/about'); + expect(cfg.profile.playlists).toBe('/u/john/playlists'); + expect(cfg.profile.shared_by_me).toBe('/u/john/shared_by_me'); + expect(cfg.profile.shared_with_me).toBe('/u/john/shared_with_me'); + }); + + test('passes through archive and user URLs', () => { + const cfg = urlConfig(base as any); + expect(cfg.user).toStrictEqual({ + liked: '/liked', + history: '/history', + addMedia: '/add', + editChannel: '/edit/channel', + editProfile: '/edit/profile', }); - expect(cfg.user.is).toStrictEqual({ anonymous: true, admin: true }); + expect(cfg.archive).toStrictEqual({ tags: '/tags', categories: '/categories' }); }); }); });