From 5fbb444d5f37c3279eb7fff9033f7061ad1b1b7b Mon Sep 17 00:00:00 2001 From: Jelte Lagendijk Date: Tue, 7 May 2024 19:47:19 +0200 Subject: [PATCH] feat: add `[homarr_base]` replacement for external urls (#2024) --- package.json | 1 + public/locales/en/layout/modals/add-app.json | 43 ++++++++++--------- .../Modals/EditAppModal/EditAppModal.tsx | 8 +++- .../Tabs/GeneralTab/GeneralTab.tsx | 28 ++++++++---- .../Dashboard/Tiles/Apps/AppTile.tsx | 7 +-- .../Dashboard/Wrappers/Category/Category.tsx | 9 +++- src/hooks/useExternalUrl.ts | 39 +++++++++++++++++ yarn.lock | 19 ++++++++ 8 files changed, 117 insertions(+), 37 deletions(-) create mode 100644 src/hooks/useExternalUrl.ts diff --git a/package.json b/package.json index ee7ea990e49..d8b5b82447a 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "rss-parser": "^3.12.0", "sabnzbd-api": "^1.5.0", "swagger-ui-react": "^5.11.0", + "tldts": "^6.1.18", "trpc-openapi": "^1.2.0", "uuid": "^9.0.0", "xml-js": "^1.6.11", diff --git a/public/locales/en/layout/modals/add-app.json b/public/locales/en/layout/modals/add-app.json index ce8d7f37c6f..149519b93fd 100644 --- a/public/locales/en/layout/modals/add-app.json +++ b/public/locales/en/layout/modals/add-app.json @@ -31,7 +31,8 @@ }, "externalAddress": { "label": "External address", - "description": "URL that will be opened when clicking on the app." + "description": "URL that will be opened when clicking on the app.", + "tooltip": "You can use a few variables to create dynamic addresses:

[homarr_base] : full address excluding port and path. (Example: 'https://subdomain.homarr.dev')
[homarr_hostname] : full base url including it's current subdomain. (Example: 'subdomain.homarr.dev')
[homarr_domain] : domain with subdomain filtered out. (Example: `homarr.dev')
[homarr_protocol] : http/https

These variables all depend on the current url." } }, "behaviour": { @@ -39,7 +40,7 @@ "label": "Open in new tab", "description": "Open the app in a new tab instead of the current one." }, - "tooltipDescription":{ + "tooltipDescription": { "label": "Application Description", "description": "The text you enter will appear when hovering over your app.\r\nUse this to give users more details about your app or leave empty to have nothing." }, @@ -68,32 +69,32 @@ "text": "This may take a few seconds" } }, - "appNameFontSize":{ - "label":"App Name Font Size", - "description":"Set the font size for when the app name is shown on the tile." + "appNameFontSize": { + "label": "App Name Font Size", + "description": "Set the font size for when the app name is shown on the tile." }, - "appNameStatus":{ - "label":"App Name Status", - "description":"Choose where you want the title to show up, if at all.", + "appNameStatus": { + "label": "App Name Status", + "description": "Choose where you want the title to show up, if at all.", "dropdown": { - "normal":"Show title on tile only", - "hover":"Show title on tooltip hover only", - "hidden":"Don't show at all" + "normal": "Show title on tile only", + "hover": "Show title on tooltip hover only", + "hidden": "Don't show at all" } }, - "positionAppName":{ - "label":"App Name Position", - "description":"Position of the app's name relative to the icon.", + "positionAppName": { + "label": "App Name Position", + "description": "Position of the app's name relative to the icon.", "dropdown": { - "top":"Top", - "right":"Right", - "bottom":"Bottom", - "left":"Left" + "top": "Top", + "right": "Right", + "bottom": "Bottom", + "left": "Left" } }, - "lineClampAppName":{ - "label":"App Name Line Clamp", - "description":"Defines on how many lines your title should fit at it's maximum. Set 0 for unlimited." + "lineClampAppName": { + "label": "App Name Line Clamp", + "description": "Defines on how many lines your title should fit at it's maximum. Set 0 for unlimited." } }, "integration": { diff --git a/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx b/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx index 7b50106043a..f9ba29e04bd 100644 --- a/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx @@ -75,7 +75,11 @@ export const EditAppModal = ({ return t('validation.noExternalUri'); } - if (!url.match(appUrlWithAnyProtocolRegex)) { + if ( + !url.match(appUrlWithAnyProtocolRegex) && + !url.startsWith('[homarr_base]') && + !url.startsWith('[homarr_protocol]://') + ) { return t('validation.invalidExternalUri'); } @@ -110,7 +114,7 @@ export const EditAppModal = ({ // also close the parent modal context.closeAll(); - umami.track('Add app', { name: values.name }); + umami.track('Add app', { name: values.name }); }; const [activeTab, setActiveTab] = useState('general'); diff --git a/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx b/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx index 3e7806159b3..a1a83319790 100644 --- a/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx +++ b/src/components/Dashboard/Modals/EditAppModal/Tabs/GeneralTab/GeneralTab.tsx @@ -3,6 +3,7 @@ import { UseFormReturnType } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; +import { InfoCard } from '~/components/InfoCard/InfoCard'; import { AppType } from '~/types/app'; import { EditAppModalTab } from '../type'; @@ -50,14 +51,21 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { form.setFieldValue('url', e.target.value); }} /> - } - label={t('general.externalAddress.label')} - description={t('general.externalAddress.description')} - placeholder="https://homarr.mywebsite.com/" - variant="default" - {...form.getInputProps('behaviour.externalUrl')} - /> + + + + {t('general.externalAddress.label')} + + + + } + description={t('general.externalAddress.description')} + placeholder="https://homarr.mywebsite.com/" + variant="default" + {...form.getInputProps('behaviour.externalUrl')} + /> + @@ -81,7 +89,9 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => { {!form.values.behaviour.externalUrl.startsWith('https://') && - !form.values.behaviour.externalUrl.startsWith('http://') && ( + !form.values.behaviour.externalUrl.startsWith('http://') && + !form.values.behaviour.externalUrl.startsWith('[homarr_base]') && + !form.values.behaviour.externalUrl.startsWith('[homarr_protocol]://') && ( {t('behaviour.customProtocolWarning')} diff --git a/src/components/Dashboard/Tiles/Apps/AppTile.tsx b/src/components/Dashboard/Tiles/Apps/AppTile.tsx index da4ee649e4b..e9fa7dc01e1 100644 --- a/src/components/Dashboard/Tiles/Apps/AppTile.tsx +++ b/src/components/Dashboard/Tiles/Apps/AppTile.tsx @@ -1,7 +1,7 @@ -import { Affix, Box, Text, Tooltip, UnstyledButton } from '@mantine/core'; +import { Box, Text, Tooltip, UnstyledButton } from '@mantine/core'; import { createStyles, useMantineTheme } from '@mantine/styles'; import { motion } from 'framer-motion'; -import Link from 'next/link'; +import { useExternalUrl } from '~/hooks/useExternalUrl'; import { AppType } from '~/types/app'; import { useEditModeStore } from '../../Views/useEditModeStore'; @@ -26,6 +26,7 @@ export const AppTile = ({ className, app }: AppTileProps) => { .join(': '); const isRow = app.appearance.positionAppName.includes('row'); + const href = useExternalUrl(app); function Inner() { return ( @@ -88,7 +89,7 @@ export const AppTile = ({ className, app }: AppTileProps) => { 0 ? app.behaviour.externalUrl : app.url} + href={href} target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'} className={`${classes.button} ${classes.base}`} > diff --git a/src/components/Dashboard/Wrappers/Category/Category.tsx b/src/components/Dashboard/Wrappers/Category/Category.tsx index 0aa51ec2d00..e7115d4a5db 100644 --- a/src/components/Dashboard/Wrappers/Category/Category.tsx +++ b/src/components/Dashboard/Wrappers/Category/Category.tsx @@ -14,6 +14,7 @@ import { modals } from '@mantine/modals'; import { IconDotsVertical, IconShare3 } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { useConfigContext } from '~/config/provider'; +import { useGetExternalUrl } from '~/hooks/useExternalUrl'; import { CategoryType } from '~/types/category'; import { useCardStyles } from '../../../layout/Common/useCardStyles'; @@ -33,6 +34,7 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => { const { classes: cardClasses, cx } = useCardStyles(true); const { classes } = useStyles(); const { t } = useTranslation(['layout/common', 'common']); + const getAppUrl = useGetExternalUrl(); const categoryList = config?.categories.map((x) => x.name) ?? []; const [toggledCategories, setToggledCategories] = useLocalStorage({ @@ -44,7 +46,8 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => { const handleMenuClick = () => { for (let i = 0; i < apps.length; i += 1) { const app = apps[i]; - const popUp = window.open(app.url, app.id); + const appUrl = getAppUrl(app); + const popUp = window.open(appUrl, app.id); if (popUp === null) { modals.openConfirmModal({ @@ -114,7 +117,9 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => { - ) : } + ) : ( + + )}
{ + const parsedUrl = useMemo(() => { + try { + return tldts.parse(window.location.toString()); + } catch { + return null; + } + }, [window.location]); + + const getHref = useCallback( + (appType: AppType) => { + if (appType.behaviour.externalUrl.length > 0) { + return appType.behaviour.externalUrl + .replace('[homarr_base]', `${window.location.protocol}//${window.location.hostname}`) + .replace('[homarr_hostname]', parsedUrl?.hostname ?? '') + .replace('[homarr_domain]', parsedUrl?.domain ?? '') + .replace('[homarr_protocol]', window.location.protocol.replace(':', '')); + } + return appType.url; + }, + [parsedUrl] + ); + + return getHref; +}; + +export const useExternalUrl = (app: AppType) => { + const getHref = useGetExternalUrl(); + + const href = useMemo(() => { + return getHref(app); + }, [app, getHref]); + + return href; +}; diff --git a/yarn.lock b/yarn.lock index 26f58fa16d5..c9d89b889f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7495,6 +7495,7 @@ __metadata: sabnzbd-api: ^1.5.0 sass: ^1.56.1 swagger-ui-react: ^5.11.0 + tldts: ^6.1.18 trpc-openapi: ^1.2.0 ts-node: latest turbo: ^1.10.12 @@ -11885,6 +11886,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^6.1.18": + version: 6.1.18 + resolution: "tldts-core@npm:6.1.18" + checksum: 392d490c04aca5b40f16666fb6cbc9d7ef6df42884b1ac196736ffdeeb378019962dbc04ee00a2a85ce61944a0692c1f1cd8b64896277949906991146830314f + languageName: node + linkType: hard + +"tldts@npm:^6.1.18": + version: 6.1.18 + resolution: "tldts@npm:6.1.18" + dependencies: + tldts-core: ^6.1.18 + bin: + tldts: bin/cli.js + checksum: 04ec7d6a5ad42ddedd9dd250d9cde37608b09bf28eecb94bad8c49d65225d861e0bddc6e1cea3253baf8fc96722e0d2fc1e926eb73b7a35396c77346efaf70f0 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33"