Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
382e3ae
feat(KeyboardShortcuts): initial commit
maxpatiiuk Jul 13, 2024
1b4d5d6
feat(KeyboardShortcuts): complete base features
maxpatiiuk Jul 13, 2024
9309bd6
refactor(KeyboardShortcuts): move code into separate folder
maxpatiiuk Jul 13, 2024
979975a
refactor(KeyboardShortcuts): use key codes over key names
maxpatiiuk Jul 13, 2024
be5b560
refactor(KeyboardShortcuts): rename from "Keyboard"
maxpatiiuk Jul 13, 2024
98269a5
fix(KeyboardShortcuts): polish UI
maxpatiiuk Jul 14, 2024
09f8a31
feat(KeyboardShortcuts): begin integrating with UI
maxpatiiuk Jul 14, 2024
ec31b19
fix(KeyboardShortcuts): correctly localize shortcuts on windows
maxpatiiuk Jul 14, 2024
83fb8a5
feat(TreeView): improve layout
maxpatiiuk Jul 21, 2024
9e41157
feat(KeyboardShortcuts): Add shortcuts for forms, tree and queries
maxpatiiuk Jul 21, 2024
7f08bdd
fix(KeyboardShortcuts): prevent adding duplicate shortcuts
maxpatiiuk Dec 25, 2024
3b2afc1
fix(KeyboardShortcuts): don't permit binding to Shift+Tab
maxpatiiuk Dec 25, 2024
139d255
feat(KeyboardShortcuts): add custom URL shortcuts
maxpatiiuk Dec 26, 2024
580584e
fix(KeyboardShortcuts): respond to review feedback
maxpatiiuk Jul 26, 2025
ea892c2
refactor(QueryBuilder): self-review code
maxpatiiuk Jul 26, 2025
db1a62f
docs(KeyboardShortcuts): add instructions on adding new shortcuts
maxpatiiuk Jul 26, 2025
408db75
build(KeyboardShortcuts): avoid cyclical import
maxpatiiuk Jul 26, 2025
186cc9f
tests(Preferences): fix localization tests
maxpatiiuk Jul 26, 2025
29e1cfd
feat(keyboard-shortcuts): merge dormant shortcut branch
grantfitzsimmons Sep 14, 2026
e093dc0
fix(keyboard-shortcuts): resolve merge conflicts
grantfitzsimmons Sep 14, 2026
49b3f3e
fix(keyboard-shortcuts): import link localization
grantfitzsimmons Sep 14, 2026
8ab203c
fix(keyboard-shortcuts): restore preference paths
grantfitzsimmons Sep 14, 2026
a4d07d6
fix(keyboard-shortcuts): restore query builder shortcuts
grantfitzsimmons Sep 14, 2026
87ade76
fix(keyboard-shortcuts): restore tree shortcuts
grantfitzsimmons Sep 14, 2026
d911d75
feat(keyboard-shortcuts): expose user tool shortcuts
grantfitzsimmons Sep 14, 2026
aa8c127
feat(keyboard-shortcuts): show user tools in preferences
grantfitzsimmons Sep 14, 2026
3bc8c77
fix(preferences): balance navigation and content columns
grantfitzsimmons Sep 14, 2026
57bbbbe
fix(keyboard-shortcuts): separate user tools from custom pages
grantfitzsimmons Sep 14, 2026
bf417ee
feat(dialogs): add close and save shortcuts
grantfitzsimmons Sep 14, 2026
b067b8b
feat(keyboard-shortcuts): configure dialog shortcuts
grantfitzsimmons Sep 14, 2026
8aa43d4
fix(keyboard-shortcuts): activate user tool URLs
grantfitzsimmons Sep 21, 2026
e5d1f65
feat(keyboard-shortcuts): add form action shortcuts
grantfitzsimmons Sep 21, 2026
bc661cc
feat(keyboard-shortcuts): include navigation menu items
grantfitzsimmons Sep 21, 2026
4489e8e
fix(keyboard-shortcuts): preserve menu navigation guards
grantfitzsimmons Sep 21, 2026
9f27c23
fix(keyboard-shortcuts): configure attachment sliders
grantfitzsimmons Sep 21, 2026
76750d7
fix(keyboard-shortcuts): preserve overlay navigation
grantfitzsimmons Sep 22, 2026
d15b9b1
fix(keyboard-shortcuts): route internal user tools in app
grantfitzsimmons Sep 22, 2026
f2c391b
test(keyboard-shortcuts): update shortcut snapshots
grantfitzsimmons Sep 22, 2026
816e411
Merge branch 'main' into issue-1746
grantfitzsimmons Sep 22, 2026
0c7c74a
fix(preferences): label keyboard shortcuts page
grantfitzsimmons Sep 22, 2026
c2c8b6b
refactor(preferences): merge keyboard shortcuts into user preferences
grantfitzsimmons Sep 22, 2026
eefade0
refactor(preferences): group shortcuts in submenu
grantfitzsimmons Sep 22, 2026
a1f40ab
Lint code with ESLint and Prettier
grantfitzsimmons Sep 22, 2026
bb855b3
Merge branch 'main' into issue-1746
grantfitzsimmons Sep 22, 2026
56438f0
feat(preferences): clarify shortcut sections
grantfitzsimmons Sep 22, 2026
0723f74
fix(preferences): separate URL shortcuts under user tools
grantfitzsimmons Sep 22, 2026
b626b50
feat(preferences): separate URL shortcut section
grantfitzsimmons Sep 22, 2026
b80080b
Lint code with ESLint and Prettier
grantfitzsimmons Sep 22, 2026
6ced604
fix(keyboard-shortcuts): respect disabled action states
grantfitzsimmons Oct 1, 2026
e6bd4b7
fix(keyboard-shortcuts): clean platform-specific bindings
grantfitzsimmons Oct 1, 2026
c46b781
fix(keyboard-shortcuts): route URLs by path prefix
grantfitzsimmons Oct 1, 2026
9370e3f
fix(preferences): omit empty shortcut categories
grantfitzsimmons Oct 1, 2026
48e7dd0
fix(record-navigation): disable unavailable controls
grantfitzsimmons Oct 1, 2026
828552b
fix(keyboard-shortcuts): ignore selects while editing
grantfitzsimmons Oct 1, 2026
602edef
fix(keyboard-shortcuts): require related record link
grantfitzsimmons Oct 1, 2026
856146c
fix(keyboard-shortcuts): avoid duplicate COJO bindings
grantfitzsimmons Oct 1, 2026
5a33e95
fix(record-navigation): guard invalid boundaries
grantfitzsimmons Oct 1, 2026
ea4c60b
feat(keyboard-shortcuts): add hold-to-view cheat sheet
grantfitzsimmons Oct 1, 2026
ee9cf8e
fix(keyboard-shortcuts): allow cheat sheet scrolling
grantfitzsimmons Oct 1, 2026
4563ad8
fix(keyboard-shortcuts): refine cheat sheet appearance
grantfitzsimmons Oct 1, 2026
d012b2d
fix(keyboard-shortcuts): label user tool shortcuts
grantfitzsimmons Oct 1, 2026
ad53fd0
chore(keyboard-shortcuts): show cheat sheet faster
grantfitzsimmons Oct 1, 2026
993c528
fix(keyboard-shortcuts): resolve tool URLs
grantfitzsimmons Oct 1, 2026
450f5c3
feat(keyboard-shortcuts): show relevant shortcuts on top
grantfitzsimmons Oct 1, 2026
1233f91
Exclude main shortcut tools from URL dialog
grantfitzsimmons Oct 1, 2026
4bbee58
fix(keyboard-shortcuts): hide strange routes
grantfitzsimmons Oct 1, 2026
a274358
feat(keyboard-shortcuts): add simple toggle
grantfitzsimmons Oct 1, 2026
e568cc8
Add express search to URL shortcuts
grantfitzsimmons Oct 1, 2026
72e808c
fix(keyboard-shortcuts): better match user tools
grantfitzsimmons Oct 1, 2026
615416e
fix(keyboard-shortcuts): hide a couple more routes
grantfitzsimmons Oct 1, 2026
1474d59
fix(keyboard-shortcuts): secure tree query tabs
grantfitzsimmons Oct 1, 2026
2cb4029
fix(keyboard-shortcuts): require modifier to close dialogs
grantfitzsimmons Oct 1, 2026
bad20de
fix(keyboard-shortcuts): avoid duplicate express search menu
grantfitzsimmons Oct 1, 2026
07a9186
fix(keyboard-shortcuts): move toggle into shortcut preferences
grantfitzsimmons Oct 1, 2026
7b477ae
fix(keyboard-shortcuts): place toggle in sidebar section
grantfitzsimmons Oct 1, 2026
4cfd0aa
fix(keyboard-shortcuts): put toggle above action sections
grantfitzsimmons Oct 1, 2026
94c243f
fix(keyboard-shortcuts): resolve toggle preference source
grantfitzsimmons Oct 1, 2026
10f2156
fix(keyboard-shortcuts): improve description
grantfitzsimmons Oct 1, 2026
edb83c9
fix(keyboard-shortcuts): remove default user preferences shortcut
grantfitzsimmons Oct 1, 2026
356032a
fix(keyboard-shortcuts): label express search shortcut
grantfitzsimmons Oct 1, 2026
3b67e12
fix(keyboard-shortcuts): ignore multi-modifier for cheat sheet
grantfitzsimmons Oct 1, 2026
7aceb79
fix(keyboard-shortcuts): disable cheat sheet listeners
grantfitzsimmons Oct 1, 2026
36ccd38
fix(keyboard-shortcuts): avoid copy shortcut for dialog close
grantfitzsimmons Oct 1, 2026
61ac641
Merge origin/main into issue-1746
grantfitzsimmons Oct 1, 2026
8e4be9d
fix(query-builder): schedule embedded shortcut queries
grantfitzsimmons Oct 1, 2026
861b16c
fix(keyboard-shortcuts): handle modifier chord order
grantfitzsimmons Oct 1, 2026
c764a7a
fix(keyboard-shortcuts): fix preferences
grantfitzsimmons Oct 21, 2026
c5bdd0e
feat(keyboard-shortcuts): show URL shortcuts in preferences
grantfitzsimmons Oct 21, 2026
5162996
chore: merge main into issue-1746
grantfitzsimmons Oct 21, 2026
c6231e1
fix(keyboard-shortcuts): identify shortcut groups
grantfitzsimmons Oct 21, 2026
4027a3d
feat(keyboard-shortcuts): automatically save shortcut on release
grantfitzsimmons Oct 21, 2026
b601f60
feat(keyboard-shortcuts): warn about duplicates
grantfitzsimmons Oct 21, 2026
0ab2a01
fix(keyboard-shortcuts): improve custom URLs
grantfitzsimmons Oct 21, 2026
beeaadd
fix(keyboard-shortcuts): fix choose collection behavior
grantfitzsimmons Oct 21, 2026
8835370
fix(keyboard-shortcuts): remove spurious unsaved changes dialog
grantfitzsimmons Oct 21, 2026
60a5b02
feat(keyboard-shortcuts): configure dialog escape shortcut
grantfitzsimmons Oct 21, 2026
5ba7240
fix(keyboard-shortcuts): make shortcuts not conflict
grantfitzsimmons Oct 21, 2026
37b8f51
Lint code with ESLint and Prettier
grantfitzsimmons Oct 21, 2026
3b011b2
Merge branch 'main' into issue-1746
grantfitzsimmons Oct 21, 2026
79e6bdb
Merge branch 'main' into issue-1746
grantfitzsimmons Oct 24, 2026
1027bc3
fix(forms): use available slider translations
grantfitzsimmons Oct 24, 2026
7c9ffe9
fix(workbench): type disambiguation lookup via primary key
grantfitzsimmons Oct 24, 2026
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
4 changes: 2 additions & 2 deletions specifyweb/backend/workbench/upload/upload_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def get_django_predicates(
model = self.django_model

if self.disambiguation is not None:
if model.objects.filter(id=self.disambiguation).exists():
if model.objects.filter(pk=self.disambiguation).exists():
return DjangoPredicates(filters={"id": self.disambiguation})

# here's why the line below matters: If some records were added during record epansion, we cannot _filter_ or _eclude_ on them.
Expand Down Expand Up @@ -554,7 +554,7 @@ def _resolve_reference_attributes(self, model, reference_record) -> dict[str, An
def _handle_row(self, skip_match: bool, allow_null: bool) -> UploadResult:
model = self.django_model
if self.disambiguation is not None:
if model.objects.filter(id=self.disambiguation).exists():
if model.objects.filter(pk=self.disambiguation).exists():
return UploadResult(
Matched(
id=self.disambiguation, info=ReportInfo(self.name, [], None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('CreateAppResource', () => {

// This is a lot more cleaner than the inner HTML
expect(getByRole('dialog').textContent).toMatchInlineSnapshot(
`"Select Resource TypeTypeDocumentationLabelDocumentationReportDocumentationDefault User PreferencesDocumentationLeaflet LayersDocumentationRSS Export FeedDocumentationExpress Search ConfigDocumentationType SearchesDocumentationWeb LinksDocumentationField FormattersDocumentationRecord FormattersDocumentationData Entry TablesDocumentationInteractions TablesDocumentationOther XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"`
`"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"`
);
});

Expand Down
70 changes: 55 additions & 15 deletions specifyweb/frontend/js_src/lib/components/Atoms/DataEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import type { LocalizedString } from 'typesafe-i18n';
import { commonText } from '../../localization/common';
import { formsText } from '../../localization/forms';
import type { RA } from '../../utils/types';
import { localized } from '../../utils/types';
import type { AnySchema } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import type { ViewDescription } from '../FormParse';
import type { cellAlign, cellVerticalAlign } from '../FormParse/cells';
import { userPreferences } from '../Preferences/userPreferences';
import { Button } from './Button';
import { className } from './className';
import type { icons } from './Icons';
Expand All @@ -19,23 +21,24 @@ const dataEntryButton = (
className: string,
title: LocalizedString,
icon: keyof typeof icons
) =>
function (
) => {
const component = (
props: Omit<TagProps<'button'>, 'children' | 'type'> & {
readonly onClick:
| ((event: React.MouseEvent<HTMLButtonElement>) => void)
| undefined;
}
): JSX.Element {
return (
<Button.Icon
className={`${className} ${props.className ?? ''}`}
icon={icon}
title={title}
{...props}
/>
);
};
): JSX.Element => (
<Button.Icon
className={`${className} ${props.className ?? ''}`}
icon={icon}
title={title}
{...props}
/>
);
Object.defineProperty(component, 'name', { value: icon });
return component;
};

export const columnDefinitionsToCss = (
columns: RA<number | undefined>,
Expand All @@ -54,6 +57,12 @@ export const columnDefinitionsToCss = (
* This is called DataEntry instead of Form because "Form" is already taken
*/

const DataEntryAdd = dataEntryButton(
className.dataEntryAdd,
commonText.add(),
'plus'
);

export const DataEntry = {
Grid: wrap<
'div',
Expand Down Expand Up @@ -145,7 +154,29 @@ export const DataEntry = {
})
),
SubFormTitle: wrap('DataEntry.SubFormTitle', 'h3', className.formTitle),
Add: dataEntryButton(className.dataEntryAdd, commonText.add(), 'plus'),
Add({
enableShortcut,
onClick: handleClick,
title = commonText.add(),
...rest
}: Omit<Parameters<typeof DataEntryAdd>[0], 'onClick'> & {
readonly onClick: (() => void) | undefined;
readonly enableShortcut: boolean;
}): JSX.Element {
const addButtonShortcut = userPreferences.useKeyboardShortcut(
'form',
'recordSet',
'addResource',
enableShortcut && rest.disabled !== true ? handleClick : undefined
);
return (
<DataEntryAdd
title={`${title}${addButtonShortcut}`}
onClick={handleClick}
{...rest}
/>
);
},
View: dataEntryButton(className.dataEntryView, commonText.view(), 'eye'),
Edit: dataEntryButton(className.dataEntryEdit, commonText.edit(), 'pencil'),
Clone: dataEntryButton(
Expand All @@ -170,14 +201,23 @@ export const DataEntry = {
readonly className?: string;
readonly resource: SpecifyResource<AnySchema> | undefined;
}): JSX.Element | null {
const ref = React.useRef<HTMLAnchorElement>(null);
const keyboardShortcut = userPreferences.useKeyboardShortcut(
'form',
'dialogs',
'openRelatedRecordInNewTab',
typeof resource === 'object' && !resource.isNew()
? (): void => ref.current?.click()
: undefined
);
return typeof resource === 'object' && !resource.isNew() ? (
<Link.NewTab
aria-label={commonText.openInNewTab()}
className={`${className.dataEntryVisit} ${localClassName}`}
forwardRef={ref}
href={resource.viewUrl()}
title={commonText.openInNewTab()}
title={localized(`${commonText.openInNewTab()}${keyboardShortcut}`)}
/>
) : null;
},
};
/* eslint-enable @typescript-eslint/naming-convention */
10 changes: 4 additions & 6 deletions specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const icons = {
collection: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" /></svg>,
cube: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11 17a1 1 0 001.447.894l4-2A1 1 0 0017 15V9.236a1 1 0 00-1.447-.894l-4 2a1 1 0 00-.553.894V17zM15.211 6.276a1 1 0 000-1.788l-4.764-2.382a1 1 0 00-.894 0L4.789 4.488a1 1 0 000 1.788l4.764 2.382a1 1 0 00.894 0l4.764-2.382zM4.447 8.342A1 1 0 003 9.236V15a1 1 0 00.553.894l4 2A1 1 0 009 17v-5.764a1 1 0 00-.553-.894l-4-2z" /></svg>,
cubeTransparent: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M9.504 1.132a1 1 0 01.992 0l1.75 1a1 1 0 11-.992 1.736L10 3.152l-1.254.716a1 1 0 11-.992-1.736l1.75-1zM5.618 4.504a1 1 0 01-.372 1.364L5.016 6l.23.132a1 1 0 11-.992 1.736L4 7.723V8a1 1 0 01-2 0V6a.996.996 0 01.52-.878l1.734-.99a1 1 0 011.364.372zm8.764 0a1 1 0 011.364-.372l1.733.99A1.002 1.002 0 0118 6v2a1 1 0 11-2 0v-.277l-.254.145a1 1 0 11-.992-1.736l.23-.132-.23-.132a1 1 0 01-.372-1.364zm-7 4a1 1 0 011.364-.372L10 8.848l1.254-.716a1 1 0 11.992 1.736L11 10.58V12a1 1 0 11-2 0v-1.42l-1.246-.712a1 1 0 01-.372-1.364zM3 11a1 1 0 011 1v1.42l1.246.712a1 1 0 11-.992 1.736l-1.75-1A1 1 0 012 14v-2a1 1 0 011-1zm14 0a1 1 0 011 1v2a1 1 0 01-.504.868l-1.75 1a1 1 0 11-.992-1.736L16 13.42V12a1 1 0 011-1zm-9.618 5.504a1 1 0 011.364-.372l.254.145V16a1 1 0 112 0v.277l.254-.145a1 1 0 11.992 1.736l-1.735.992a.995.995 0 01-1.022 0l-1.735-.992a1 1 0 01-.372-1.364z" fillRule="evenodd" /></svg>,
cursorClick: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M6.672 1.911a1 1 0 10-1.932.518l.259.966a1 1 0 001.932-.518l-.26-.966zM2.429 4.74a1 1 0 10-.517 1.932l.966.259a1 1 0 00.517-1.932l-.966-.26zm8.814-.569a1 1 0 00-1.415-1.414l-.707.707a1 1 0 101.415 1.415l.707-.708zm-7.071 7.072l.707-.707A1 1 0 003.465 9.12l-.708.707a1 1 0 001.415 1.415zm3.2-5.171a1 1 0 00-1.3 1.3l4 10a1 1 0 001.823.075l1.38-2.759 3.018 3.02a1 1 0 001.414-1.415l-3.019-3.02 2.76-1.379a1 1 0 00-.076-1.822l-10-4z" fillRule="evenodd" /></svg>,
database: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M3 12v3c0 1.657 3.134 3 7 3s7-1.343 7-3v-3c0 1.657-3.134 3-7 3s-7-1.343-7-3z" /><path d="M3 7v3c0 1.657 3.134 3 7 3s7-1.343 7-3V7c0 1.657-3.134 3-7 3S3 8.657 3 7z" /><path d="M17 5c0 1.657-3.134 3-7 3S3 6.657 3 5s3.134-3 7-3 7 1.343 7 3z" /></svg>,
document: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" fillRule="evenodd" /></svg>,
documentReport: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm2 10a1 1 0 10-2 0v3a1 1 0 102 0v-3zm2-3a1 1 0 011 1v5a1 1 0 11-2 0v-5a1 1 0 011-1zm4-1a1 1 0 10-2 0v7a1 1 0 102 0V8z" fillRule="evenodd" /></svg>,
Expand All @@ -86,13 +87,10 @@ export const icons = {
exclamationCircle: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" fillRule="evenodd" /></svg>,
externalLink: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" /><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" /></svg>,
eye: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /><path clipRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" fillRule="evenodd" /></svg>,
filter: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z" fillRule="evenodd" /></svg>,
fingerPrint: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M6.625 2.655A9 9 0 0119 11a1 1 0 11-2 0 7 7 0 00-9.625-6.492 1 1 0 11-.75-1.853zM4.662 4.959A1 1 0 014.75 6.37 6.97 6.97 0 003 11a1 1 0 11-2 0 8.97 8.97 0 012.25-5.953 1 1 0 011.412-.088z" fillRule="evenodd" /><path clipRule="evenodd" d="M5 11a5 5 0 1110 0 1 1 0 11-2 0 3 3 0 10-6 0c0 1.677-.345 3.276-.968 4.729a1 1 0 11-1.838-.789A9.964 9.964 0 005 11zm8.921 2.012a1 1 0 01.831 1.145 19.86 19.86 0 01-.545 2.436 1 1 0 11-1.92-.558c.207-.713.371-1.445.49-2.192a1 1 0 011.144-.83z" fillRule="evenodd" /><path clipRule="evenodd" d="M10 10a1 1 0 011 1c0 2.236-.46 4.368-1.29 6.304a1 1 0 01-1.838-.789A13.952 13.952 0 009 11a1 1 0 011-1z" fillRule="evenodd" /></svg>,
gallery:<svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clipRule="evenodd" d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" fillRule="evenodd" />
</svg>,
globe: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path clipRule="evenodd" d="M4.083 9h1.946c.089-1.546.383-2.97.837-4.118A6.004 6.004 0 004.083 9zM10 2a8 8 0 100 16 8 8 0 000-16zm0 2c-.076 0-.232.032-.465.262-.238.234-.497.623-.737 1.182-.389.907-.673 2.142-.766 3.556h3.936c-.093-1.414-.377-2.649-.766-3.556-.24-.56-.5-.948-.737-1.182C10.232 4.032 10.076 4 10 4zm3.971 5c-.089-1.546-.383-2.97-.837-4.118A6.004 6.004 0 0115.917 9h-1.946zm-2.003 2H8.032c.093 1.414.377 2.649.766 3.556.24.56.5.948.737 1.182.233.23.389.262.465.262.076 0 .232-.032.465-.262.238-.234.498-.623.737-1.182.389-.907.673-2.142.766-3.556zm1.166 4.118c.454-1.147.748-2.572.837-4.118h1.946a6.004 6.004 0 01-2.783 4.118zm-6.268 0C6.412 13.97 6.118 12.546 6.03 11H4.083a6.004 6.004 0 002.783 4.118z" fillRule="evenodd" />
</svg>,
gallery:<svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" fillRule="evenodd" /></svg>,
globe: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M4.083 9h1.946c.089-1.546.383-2.97.837-4.118A6.004 6.004 0 004.083 9zM10 2a8 8 0 100 16 8 8 0 000-16zm0 2c-.076 0-.232.032-.465.262-.238.234-.497.623-.737 1.182-.389.907-.673 2.142-.766 3.556h3.936c-.093-1.414-.377-2.649-.766-3.556-.24-.56-.5-.948-.737-1.182C10.232 4.032 10.076 4 10 4zm3.971 5c-.089-1.546-.383-2.97-.837-4.118A6.004 6.004 0 0115.917 9h-1.946zm-2.003 2H8.032c.093 1.414.377 2.649.766 3.556.24.56.5.948.737 1.182.233.23.389.262.465.262.076 0 .232-.032.465-.262.238-.234.498-.623.737-1.182.389-.907.673-2.142.766-3.556zm1.166 4.118c.454-1.147.748-2.572.837-4.118h1.946a6.004 6.004 0 01-2.783 4.118zm-6.268 0C6.412 13.97 6.118 12.546 6.03 11H4.083a6.004 6.004 0 002.783 4.118z" fillRule="evenodd" /></svg>,
hashtag: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="M9.243 3.03a1 1 0 01.727 1.213L9.53 6h2.94l.56-2.243a1 1 0 111.94.486L14.53 6H17a1 1 0 110 2h-2.97l-1 4H15a1 1 0 110 2h-2.47l-.56 2.242a1 1 0 11-1.94-.485L10.47 14H7.53l-.56 2.242a1 1 0 11-1.94-.485L5.47 14H3a1 1 0 110-2h2.97l1-4H5a1 1 0 110-2h2.47l.56-2.243a1 1 0 011.213-.727zM9.03 8l-1 4h2.938l1-4H9.031z" fillRule="evenodd" /></svg>,
// This icon is not from Heroicons. It was drawn by @grantfitzsimmons
history: <svg aria-hidden className={iconClassName} fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.043,6.984c-0.382-0.935-0.942-1.753-1.665-2.433c-0.767-0.722-1.572-1.249-2.459-1.611 c-0.911-0.391-1.94-0.597-2.979-0.597c-1.147,0-2.207,0.251-3.239,0.768C5.87,3.526,5.136,4.05,4.512,4.671V3.746 c0-0.47-0.397-0.867-0.867-0.867c-0.486,0-0.867,0.381-0.867,0.867v2.866c0,0.577,0.469,1.046,1.045,1.046h2.825 c0.51,0,0.909-0.399,0.909-0.908c0-0.471-0.355-0.826-0.826-0.826H5.752C6.191,5.5,6.688,5.131,7.258,4.802 c0.797-0.46,1.688-0.71,2.579-0.723c1.683-0.052,3.085,0.532,4.291,1.698c1.178,1.139,1.775,2.532,1.775,4.141 c0,1.729-0.555,3.125-1.696,4.267c-1.18,1.18-2.616,1.778-4.267,1.778c-1.647,0-3.016-0.568-4.181-1.733 c-0.983-1.022-1.542-2.259-1.661-3.685C4.041,10.084,3.677,9.75,3.233,9.75c-0.257,0-0.499,0.104-0.662,0.288 c-0.158,0.176-0.23,0.406-0.205,0.638c0.127,1.823,0.858,3.413,2.171,4.727c1.496,1.495,3.313,2.254,5.402,2.254 c0.977,0,1.947-0.18,2.983-0.558c0.977-0.443,1.805-1.001,2.46-1.655c0.74-0.739,1.282-1.547,1.659-2.469 c0.395-0.966,0.595-1.979,0.595-3.016C17.638,8.937,17.443,7.964,17.043,6.984z"/> <path d="M10.022,5.595H9.94c-0.426,0-0.785,0.359-0.785,0.784v3.333c0,0.411,0.158,0.796,0.445,1.084l2.345,2.303 c0.175,0.175,0.402,0.264,0.637,0.264c0.2,0,0.405-0.064,0.587-0.194c0.258-0.181,0.328-0.4,0.341-0.553 c0.021-0.251-0.09-0.519-0.31-0.737l-2.31-2.231V6.462C10.89,5.976,10.509,5.595,10.022,5.595z"/></svg>,
Expand Down
11 changes: 10 additions & 1 deletion specifyweb/frontend/js_src/lib/components/Atoms/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import type { LocalizedString } from 'typesafe-i18n';

import { commonText } from '../../localization/common';
import type { IR, RA, RR } from '../../utils/types';
import { className } from './className';
import type { IconProps } from './Icons';
Expand Down Expand Up @@ -46,7 +47,15 @@ export const Link = {
children: (
<>
{props.children}
<span aria-hidden>{icons.externalLink}</span>
<span
title={
(props.children === undefined ? props.title : undefined) ??
commonText.opensInNewTab()
}
>
<span className="sr-only">{commonText.opensInNewTab()}</span>
{icons.externalLink}
</span>
</>
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ snapshot(DataEntry.Footer, { children: 'Test' });
snapshot(DataEntry.SubForm, { children: 'Test' });
snapshot(DataEntry.SubFormHeader, { children: 'Test' });
snapshot(DataEntry.SubFormTitle, { children: 'Test' });
snapshot(DataEntry.Add, { onClick: f.never });
snapshot(DataEntry.Add, { onClick: f.never, enableShortcut: true });
snapshot(DataEntry.View, { onClick: f.never });
snapshot(DataEntry.Edit, { onClick: f.never });
snapshot(DataEntry.Clone, { onClick: f.never });
Expand Down
Loading
Loading