Skip to content
Open
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
5 changes: 5 additions & 0 deletions workspaces/shortcuts/.changeset/many-poems-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage-community/plugin-shortcuts': minor
---

The shortcuts plugin is now fully migrated to BUI with production-ready styling that automatically adapts to light and dark system themes, improved positioning, and modern Backstage UI components. All ESLint checks pass, and the plugin maintains backward compatibility with existing functionality.
28 changes: 28 additions & 0 deletions workspaces/shortcuts/app-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
app:
title: Backstage Shortcuts
baseUrl: http://localhost:3000

organization:
name: My Company

backend:
baseUrl: http://localhost:7007
listen:
port: 7007
csp:
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
database:
client: better-sqlite3
connection: ':memory:'

catalog:
locations:
- type: file
target: ../../catalog-info.yaml

auth:
providers: {}
8 changes: 8 additions & 0 deletions workspaces/shortcuts/plugins/shortcuts/dev/app-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
app:
title: Backstage Shortcuts
baseUrl: http://localhost:3000

backend:
baseUrl: http://localhost:7007
listen:
port: 7007
4 changes: 2 additions & 2 deletions workspaces/shortcuts/plugins/shortcuts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"@backstage/core-plugin-api": "backstage:^",
"@backstage/theme": "backstage:^",
"@backstage/types": "backstage:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@backstage/ui": "backstage:^",
"@remixicon/react": "^4.0.0",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-hook-form": "^7.12.2",
"react-use": "^17.2.4",
Expand Down
36 changes: 36 additions & 0 deletions workspaces/shortcuts/plugins/shortcuts/src/AddShortcut.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@layer components {
.card {
max-width: 400px;
padding: var(--bui-space-4);
border-radius: var(--bui-radius-2);
border: 1px solid var(--bui-border-1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: var(--bui-white);
color: var(--bui-black);
}

@media (prefers-color-scheme: dark) {
.card {
background-color: var(--bui-gray-8) !important;
color: var(--bui-white);
}
}

/* Support for data-theme attribute */
[data-theme="dark"] .card {
background-color: var(--bui-gray-8) !important;
color: var(--bui-white);
}

.header {
margin-bottom: var(--bui-space-2);
display: flex;
justify-content: space-between;
align-items: center;
}

.button {
margin-top: var(--bui-space-2);
color: var(--bui-bg-solid) !important;
}
}
131 changes: 83 additions & 48 deletions workspaces/shortcuts/plugins/shortcuts/src/AddShortcut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,15 @@
* limitations under the License.
*/

import { useState } from 'react';
import { useState, useRef, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { SubmitHandler } from 'react-hook-form';
import Button from '@material-ui/core/Button';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import Popover from '@material-ui/core/Popover';
import { makeStyles } from '@material-ui/core/styles';
import { Button, Text } from '@backstage/ui';
import { ShortcutForm } from './ShortcutForm';
import { FormValues, Shortcut } from './types';
import { ShortcutApi } from './api';
import { alertApiRef, useApi, useAnalytics } from '@backstage/core-plugin-api';

const useStyles = makeStyles(theme => ({
card: {
maxWidth: 400,
},
header: {
marginBottom: theme.spacing(1),
},
button: {
marginTop: theme.spacing(1),
},
}));
import styles from './AddShortcut.module.css';

type Props = {
onClose: () => void;
Expand All @@ -53,11 +38,11 @@ export const AddShortcut = ({
api,
allowExternalLinks,
}: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const { pathname, search } = useLocation();
const [formValues, setFormValues] = useState<FormValues>();
const open = Boolean(anchorEl);
const popoverRef = useRef<HTMLDivElement>(null);
const analytics = useAnalytics();

const handleSave: SubmitHandler<FormValues> = async ({ url, title }) => {
Expand All @@ -79,52 +64,102 @@ export const AddShortcut = ({
}

onClose();
return;
};

const handlePaste = () => {
setFormValues({ url: `${pathname}${search}`, title: document.title });
};

const handleClose = () => {
const handleClose = useCallback(() => {
setFormValues(undefined);
onClose();
};
}, [onClose]);

// Handle click outside to close popover
/**
* Positioning Strategy:
*
* The AddShortcut popover uses a simple fixed-position approach with `getBoundingClientRect()`
* for anchor positioning. This replaces the MUI Popover component from the previous implementation.
*
* Design Rationale:
* - The anchor button is in the Backstage sidebar (a stable, non-scrolling container)
* - User interactions are confined to the sidebar area with limited scroll/resize scenarios
* - The form closes when clicked outside, mitigating off-screen visibility concerns
* - Keeping the implementation lightweight and dependency-free improves bundle size
* - The current approach provides acceptable UX for this specific use case
*
* Future Improvements (if needed):
* - For complex scroll/viewport collision scenarios, consider @floating-ui/react
* - For panel flipping/shifting behavior, integrate Popper or Floating UI library
* - Monitor user feedback on positioning accuracy during scrolling/resizing
*
* Trade-offs Accepted:
* ✓ Simpler, lighter implementation
* ✓ Fewer external dependencies
* ✗ No automatic viewport collision detection
* ✗ No automatic repositioning on scroll/resize (but sidebar is typically fixed)
* ✗ Manual position updates if needed (currently not required)
*/
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
anchorEl &&
!anchorEl.contains(event.target as Node)
) {
handleClose();
}
};

if (open) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
return undefined;
}, [open, anchorEl, handleClose]);

if (!open) return null;

return (
<Popover
open={open}
anchorEl={anchorEl}
TransitionProps={{ onExit: handleClose }}
onClose={onClose}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
<div
ref={popoverRef}
style={{
position: 'fixed',
zIndex: 1300,
top: anchorEl ? (anchorEl as any).getBoundingClientRect().top : 0,
left: anchorEl
? (anchorEl as any).getBoundingClientRect().right + 10
: 0,
}}
>
<Card className={classes.card}>
<CardHeader
className={classes.header}
title="Add Shortcut"
titleTypographyProps={{ variant: 'subtitle2' }}
action={
<Button
className={classes.button}
variant="text"
size="small"
color="primary"
onClick={handlePaste}
>
Use current page
</Button>
}
/>
<div
className={styles.card}
style={{
backgroundColor: 'light-dark(#ffffff, #424242)',
}}
>
<div className={styles.header}>
<Text variant="body-medium">Add Shortcut</Text>
<Button
className={styles.button}
variant="secondary"
onClick={handlePaste}
>
Use current page
</Button>
</div>
<ShortcutForm
onClose={handleClose}
onSave={handleSave}
formValues={formValues}
allowExternalLinks={allowExternalLinks}
/>
</Card>
</Popover>
</div>
</div>
);
};
35 changes: 35 additions & 0 deletions workspaces/shortcuts/plugins/shortcuts/src/EditShortcut.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@layer components {
.card {
width: 400px;
padding: var(--bui-space-4);
border-radius: var(--bui-radius-2);
border: 1px solid var(--bui-border-1);
background-color: var(--bui-white);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
color: var(--bui-black);
}

@media (prefers-color-scheme: dark) {
.card {
background-color: var(--bui-gray-8) !important;
color: var(--bui-white);
}
}

/* Support for data-theme attribute */
[data-theme="dark"] .card {
background-color: var(--bui-gray-8) !important;
color: var(--bui-white);
}

.header {
margin-bottom: var(--bui-space-2);
display: flex;
justify-content: space-between;
align-items: center;
}

.button {
margin-top: var(--bui-space-2);
}
}
Loading
Loading