feat: add [homarr_base] replacement for external urls (#2024)

This commit is contained in:
Jelte Lagendijk
2024-05-07 19:47:19 +02:00
committed by GitHub
parent 477d249da8
commit 5fbb444d5f
8 changed files with 117 additions and 37 deletions

View File

@@ -106,6 +106,7 @@
"rss-parser": "^3.12.0", "rss-parser": "^3.12.0",
"sabnzbd-api": "^1.5.0", "sabnzbd-api": "^1.5.0",
"swagger-ui-react": "^5.11.0", "swagger-ui-react": "^5.11.0",
"tldts": "^6.1.18",
"trpc-openapi": "^1.2.0", "trpc-openapi": "^1.2.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",

View File

@@ -31,7 +31,8 @@
}, },
"externalAddress": { "externalAddress": {
"label": "External address", "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:<br><br><b>[homarr_base]</b> : full address excluding port and path. <i>(Example: 'https://subdomain.homarr.dev')</i><br><b>[homarr_hostname]</b> : full base url including it's current subdomain. <i>(Example: 'subdomain.homarr.dev')</i><br><b>[homarr_domain]</b> : domain with subdomain filtered out. <i>(Example: `homarr.dev')</i><br><b>[homarr_protocol]</b> : <i>http/https</i><br><br>These variables all depend on the current url."
} }
}, },
"behaviour": { "behaviour": {
@@ -39,7 +40,7 @@
"label": "Open in new tab", "label": "Open in new tab",
"description": "Open the app in a new tab instead of the current one." "description": "Open the app in a new tab instead of the current one."
}, },
"tooltipDescription":{ "tooltipDescription": {
"label": "Application Description", "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." "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" "text": "This may take a few seconds"
} }
}, },
"appNameFontSize":{ "appNameFontSize": {
"label":"App Name Font Size", "label": "App Name Font Size",
"description":"Set the font size for when the app name is shown on the tile." "description": "Set the font size for when the app name is shown on the tile."
}, },
"appNameStatus":{ "appNameStatus": {
"label":"App Name Status", "label": "App Name Status",
"description":"Choose where you want the title to show up, if at all.", "description": "Choose where you want the title to show up, if at all.",
"dropdown": { "dropdown": {
"normal":"Show title on tile only", "normal": "Show title on tile only",
"hover":"Show title on tooltip hover only", "hover": "Show title on tooltip hover only",
"hidden":"Don't show at all" "hidden": "Don't show at all"
} }
}, },
"positionAppName":{ "positionAppName": {
"label":"App Name Position", "label": "App Name Position",
"description":"Position of the app's name relative to the icon.", "description": "Position of the app's name relative to the icon.",
"dropdown": { "dropdown": {
"top":"Top", "top": "Top",
"right":"Right", "right": "Right",
"bottom":"Bottom", "bottom": "Bottom",
"left":"Left" "left": "Left"
} }
}, },
"lineClampAppName":{ "lineClampAppName": {
"label":"App Name Line Clamp", "label": "App Name Line Clamp",
"description":"Defines on how many lines your title should fit at it's maximum. Set 0 for unlimited." "description": "Defines on how many lines your title should fit at it's maximum. Set 0 for unlimited."
} }
}, },
"integration": { "integration": {

View File

@@ -75,7 +75,11 @@ export const EditAppModal = ({
return t('validation.noExternalUri'); return t('validation.noExternalUri');
} }
if (!url.match(appUrlWithAnyProtocolRegex)) { if (
!url.match(appUrlWithAnyProtocolRegex) &&
!url.startsWith('[homarr_base]') &&
!url.startsWith('[homarr_protocol]://')
) {
return t('validation.invalidExternalUri'); return t('validation.invalidExternalUri');
} }
@@ -110,7 +114,7 @@ export const EditAppModal = ({
// also close the parent modal // also close the parent modal
context.closeAll(); context.closeAll();
umami.track('Add app', { name: values.name }); umami.track('Add app', { name: values.name });
}; };
const [activeTab, setActiveTab] = useState<EditAppModalTab>('general'); const [activeTab, setActiveTab] = useState<EditAppModalTab>('general');

View File

@@ -3,6 +3,7 @@ import { UseFormReturnType } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react'; import { IconClick, IconCursorText, IconLink } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { InfoCard } from '~/components/InfoCard/InfoCard';
import { AppType } from '~/types/app'; import { AppType } from '~/types/app';
import { EditAppModalTab } from '../type'; import { EditAppModalTab } from '../type';
@@ -50,14 +51,21 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
form.setFieldValue('url', e.target.value); form.setFieldValue('url', e.target.value);
}} }}
/> />
<TextInput <Stack style={{ gap: 0 }}>
icon={<IconClick size={16} />} <Group style={{ gap: '0.25rem' }}>
label={t('general.externalAddress.label')} <Text size="0.875rem" weight={500}>
description={t('general.externalAddress.description')} {t('general.externalAddress.label')}
placeholder="https://homarr.mywebsite.com/" </Text>
variant="default" <InfoCard message={t('general.externalAddress.tooltip')} />
{...form.getInputProps('behaviour.externalUrl')} </Group>
/> <TextInput
icon={<IconClick size={16} />}
description={t('general.externalAddress.description')}
placeholder="https://homarr.mywebsite.com/"
variant="default"
{...form.getInputProps('behaviour.externalUrl')}
/>
</Stack>
<Collapse in={opened}> <Collapse in={opened}>
<Card withBorder> <Card withBorder>
@@ -81,7 +89,9 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
</Collapse> </Collapse>
{!form.values.behaviour.externalUrl.startsWith('https://') && {!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]://') && (
<Text color="red" mt="sm" size="sm"> <Text color="red" mt="sm" size="sm">
{t('behaviour.customProtocolWarning')} {t('behaviour.customProtocolWarning')}
</Text> </Text>

View File

@@ -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 { createStyles, useMantineTheme } from '@mantine/styles';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import Link from 'next/link'; import { useExternalUrl } from '~/hooks/useExternalUrl';
import { AppType } from '~/types/app'; import { AppType } from '~/types/app';
import { useEditModeStore } from '../../Views/useEditModeStore'; import { useEditModeStore } from '../../Views/useEditModeStore';
@@ -26,6 +26,7 @@ export const AppTile = ({ className, app }: AppTileProps) => {
.join(': '); .join(': ');
const isRow = app.appearance.positionAppName.includes('row'); const isRow = app.appearance.positionAppName.includes('row');
const href = useExternalUrl(app);
function Inner() { function Inner() {
return ( return (
@@ -88,7 +89,7 @@ export const AppTile = ({ className, app }: AppTileProps) => {
<UnstyledButton <UnstyledButton
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }} style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
component="a" component="a"
href={app.behaviour.externalUrl.length > 0 ? app.behaviour.externalUrl : app.url} href={href}
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'} target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
className={`${classes.button} ${classes.base}`} className={`${classes.button} ${classes.base}`}
> >

View File

@@ -14,6 +14,7 @@ import { modals } from '@mantine/modals';
import { IconDotsVertical, IconShare3 } from '@tabler/icons-react'; import { IconDotsVertical, IconShare3 } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider'; import { useConfigContext } from '~/config/provider';
import { useGetExternalUrl } from '~/hooks/useExternalUrl';
import { CategoryType } from '~/types/category'; import { CategoryType } from '~/types/category';
import { useCardStyles } from '../../../layout/Common/useCardStyles'; import { useCardStyles } from '../../../layout/Common/useCardStyles';
@@ -33,6 +34,7 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
const { classes: cardClasses, cx } = useCardStyles(true); const { classes: cardClasses, cx } = useCardStyles(true);
const { classes } = useStyles(); const { classes } = useStyles();
const { t } = useTranslation(['layout/common', 'common']); const { t } = useTranslation(['layout/common', 'common']);
const getAppUrl = useGetExternalUrl();
const categoryList = config?.categories.map((x) => x.name) ?? []; const categoryList = config?.categories.map((x) => x.name) ?? [];
const [toggledCategories, setToggledCategories] = useLocalStorage({ const [toggledCategories, setToggledCategories] = useLocalStorage({
@@ -44,7 +46,8 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
const handleMenuClick = () => { const handleMenuClick = () => {
for (let i = 0; i < apps.length; i += 1) { for (let i = 0; i < apps.length; i += 1) {
const app = apps[i]; 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) { if (popUp === null) {
modals.openConfirmModal({ modals.openConfirmModal({
@@ -114,7 +117,9 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
</Menu.Item> </Menu.Item>
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>
) : <CategoryEditMenu category={category} />} ) : (
<CategoryEditMenu category={category} />
)}
</Box> </Box>
<Accordion.Panel> <Accordion.Panel>
<div <div

View File

@@ -0,0 +1,39 @@
import { useCallback, useMemo } from 'react';
import * as tldts from 'tldts';
import { AppType } from '~/types/app';
export const useGetExternalUrl = () => {
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;
};

View File

@@ -7495,6 +7495,7 @@ __metadata:
sabnzbd-api: ^1.5.0 sabnzbd-api: ^1.5.0
sass: ^1.56.1 sass: ^1.56.1
swagger-ui-react: ^5.11.0 swagger-ui-react: ^5.11.0
tldts: ^6.1.18
trpc-openapi: ^1.2.0 trpc-openapi: ^1.2.0
ts-node: latest ts-node: latest
turbo: ^1.10.12 turbo: ^1.10.12
@@ -11885,6 +11886,24 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "tmp@npm:^0.0.33":
version: 0.0.33 version: 0.0.33
resolution: "tmp@npm:0.0.33" resolution: "tmp@npm:0.0.33"