Merge pull request #16791 from drodil/shortcuts_external_links

feat: allow adding external links to shortcuts
This commit is contained in:
Patrik Oldsberg
2023-03-21 11:10:51 +01:00
committed by GitHub
10 changed files with 98 additions and 10 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-shortcuts': patch
---
Allow external links to be added as shortcuts
+1 -1
View File
@@ -167,7 +167,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarItem icon={Score} to="score-board" text="Score board" />
</SidebarScrollWrapper>
<SidebarDivider />
<Shortcuts />
<Shortcuts allowExternalLinks />
</SidebarGroup>
<SidebarSpace />
<SidebarDivider />
+2
View File
@@ -44,6 +44,8 @@ export const SidebarComponent = () => (
);
```
To allow external links to be added as shortcut, you can add `allowExternalLinks` property to the `<Shortcuts />` component.
The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each user's shortcuts.
To overwrite the default implementation add it to the App's `apis.ts`:
+2
View File
@@ -58,6 +58,8 @@ export const shortcutsPlugin: BackstagePlugin<{}, {}, {}>;
// @public
export interface ShortcutsProps {
// (undocumented)
allowExternalLinks?: boolean;
// (undocumented)
icon?: IconComponent;
}
+9 -1
View File
@@ -45,9 +45,16 @@ type Props = {
onClose: () => void;
anchorEl?: Element;
api: ShortcutApi;
allowExternalLinks?: boolean;
};
export const AddShortcut = ({ onClose, anchorEl, api }: Props) => {
export const AddShortcut = ({
onClose,
anchorEl,
api,
allowExternalLinks,
}: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const { pathname } = useLocation();
@@ -114,6 +121,7 @@ export const AddShortcut = ({ onClose, anchorEl, api }: Props) => {
onClose={handleClose}
onSave={handleSave}
formValues={formValues}
allowExternalLinks={allowExternalLinks}
/>
</Card>
</Popover>
+9 -1
View File
@@ -46,9 +46,16 @@ type Props = {
onClose: () => void;
anchorEl?: Element;
api: ShortcutApi;
allowExternalLinks?: boolean;
};
export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => {
export const EditShortcut = ({
shortcut,
onClose,
anchorEl,
api,
allowExternalLinks,
}: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const open = Boolean(anchorEl);
@@ -127,6 +134,7 @@ export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => {
formValues={{ url: shortcut.url, title: shortcut.title }}
onClose={handleClose}
onSave={handleSave}
allowExternalLinks={allowExternalLinks}
/>
</Card>
</Popover>
@@ -43,6 +43,50 @@ describe('ShortcutForm', () => {
});
});
it('allows external links', async () => {
await renderInTestApp(<ShortcutForm allowExternalLinks {...props} />);
const urlInput = screen.getByPlaceholderText('Enter a URL');
const titleInput = screen.getByPlaceholderText('Enter a display name');
fireEvent.change(urlInput, {
target: { value: 'https://www.backstage.io' },
});
fireEvent.change(titleInput, { target: { value: 'Backstage' } });
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(props.onSave).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Backstage',
url: 'https://www.backstage.io',
}),
expect.anything(),
);
});
});
it('allows relative links when external links are enabled', async () => {
await renderInTestApp(<ShortcutForm allowExternalLinks {...props} />);
const urlInput = screen.getByPlaceholderText('Enter a URL');
const titleInput = screen.getByPlaceholderText('Enter a display name');
fireEvent.change(urlInput, {
target: { value: '/catalog' },
});
fireEvent.change(titleInput, { target: { value: 'Catalog' } });
fireEvent.click(screen.getByText('Save'));
await waitFor(() => {
expect(props.onSave).toHaveBeenCalledWith(
expect.objectContaining({
title: 'Catalog',
url: '/catalog',
}),
expect.anything(),
);
});
});
it('calls the save handler', async () => {
await renderInTestApp(
<ShortcutForm
+20 -6
View File
@@ -40,11 +40,16 @@ type Props = {
formValues?: FormValues;
onSave: SubmitHandler<FormValues>;
onClose: () => void;
allowExternalLinks?: boolean;
};
export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => {
export const ShortcutForm = ({
formValues,
onSave,
onClose,
allowExternalLinks,
}: Props) => {
const classes = useStyles();
const {
handleSubmit,
reset,
@@ -70,10 +75,19 @@ export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => {
control={control}
rules={{
required: true,
pattern: {
value: /^\//,
message: 'Must be a relative URL (starts with a /)',
},
...(allowExternalLinks
? {
pattern: {
value: /^(https?:\/\/)|\//,
message: 'Must start with http(s):// or /',
},
}
: {
pattern: {
value: /^\//,
message: 'Must be a relative URL (starts with a /)',
},
}),
}}
render={({ field }) => (
<TextField
+3 -1
View File
@@ -58,9 +58,10 @@ const getIconText = (title: string) =>
type Props = {
shortcut: Shortcut;
api: ShortcutApi;
allowExternalLinks?: boolean;
};
export const ShortcutItem = ({ shortcut, api }: Props) => {
export const ShortcutItem = ({ shortcut, api, allowExternalLinks }: Props) => {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState<Element | undefined>();
@@ -100,6 +101,7 @@ export const ShortcutItem = ({ shortcut, api }: Props) => {
anchorEl={anchorEl}
api={api}
shortcut={shortcut}
allowExternalLinks={allowExternalLinks}
/>
</>
);
+3
View File
@@ -35,6 +35,7 @@ import { IconComponent, useApi } from '@backstage/core-plugin-api';
*/
export interface ShortcutsProps {
icon?: IconComponent;
allowExternalLinks?: boolean;
}
export const Shortcuts = (props: ShortcutsProps) => {
@@ -62,6 +63,7 @@ export const Shortcuts = (props: ShortcutsProps) => {
onClose={handleClose}
anchorEl={anchorEl}
api={shortcutApi}
allowExternalLinks={props.allowExternalLinks}
/>
{loading ? (
<Progress />
@@ -71,6 +73,7 @@ export const Shortcuts = (props: ShortcutsProps) => {
key={shortcut.id}
shortcut={shortcut}
api={shortcutApi}
allowExternalLinks={props.allowExternalLinks}
/>
))
)}