Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add [homarr_base] replacement for external urls #2024

Merged
merged 19 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
43 changes: 22 additions & 21 deletions public/locales/en/layout/modals/add-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@
},
"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:<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": {
"isOpeningNewTab": {
"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."
},
Expand Down Expand Up @@ -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": {
Expand Down
8 changes: 6 additions & 2 deletions src/components/Dashboard/Modals/EditAppModal/EditAppModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

Expand Down Expand Up @@ -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<EditAppModalTab>('general');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,14 +51,21 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
form.setFieldValue('url', e.target.value);
}}
/>
<TextInput
icon={<IconClick size={16} />}
label={t('general.externalAddress.label')}
description={t('general.externalAddress.description')}
placeholder="https://homarr.mywebsite.com/"
variant="default"
{...form.getInputProps('behaviour.externalUrl')}
/>
<Stack style={{ gap: 0 }}>
<Group style={{ gap: '0.25rem' }}>
<Text size="0.875rem" weight={500}>
{t('general.externalAddress.label')}
</Text>
<InfoCard message={t('general.externalAddress.tooltip')} />
</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}>
<Card withBorder>
Expand All @@ -81,7 +89,9 @@ export const GeneralTab = ({ form, openTab }: GeneralTabProps) => {
</Collapse>

{!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">
{t('behaviour.customProtocolWarning')}
</Text>
Expand Down
7 changes: 4 additions & 3 deletions src/components/Dashboard/Tiles/Apps/AppTile.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
Expand Down Expand Up @@ -88,7 +89,7 @@ export const AppTile = ({ className, app }: AppTileProps) => {
<UnstyledButton
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
component="a"
href={app.behaviour.externalUrl.length > 0 ? app.behaviour.externalUrl : app.url}
href={href}
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
className={`${classes.button} ${classes.base}`}
>
Expand Down
9 changes: 7 additions & 2 deletions src/components/Dashboard/Wrappers/Category/Category.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -114,7 +117,9 @@ export const DashboardCategory = ({ category }: DashboardCategoryProps) => {
</Menu.Item>
</Menu.Dropdown>
</Menu>
) : <CategoryEditMenu category={category} />}
) : (
<CategoryEditMenu category={category} />
)}
</Box>
<Accordion.Panel>
<div
Expand Down
39 changes: 39 additions & 0 deletions src/hooks/useExternalUrl.ts
Original file line number Diff line number Diff line change
@@ -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;
};
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading