mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 19:08:47 +00:00
Merge commit '457fe16f9c31d08fbb6f3493f17105aac2638e86' into block-notes
This commit is contained in:
@@ -17,18 +17,22 @@ export type InvenTreeHotkeyItem = [
|
||||
HotkeyItemOptions?
|
||||
];
|
||||
|
||||
export function useInvenTreeHotkeys(hotkeys: InvenTreeHotkeyItem[]) {
|
||||
// Register the hotkeys using the Mantine hook
|
||||
useHotkeys(
|
||||
hotkeys.map(([key, _, handler, options]) => [key, handler, options])
|
||||
);
|
||||
|
||||
// register to helper state to store hotkeys
|
||||
// This allows us to display the hotkeys in the UI
|
||||
const keyelems: [string, string][] = hotkeys.map(([key, description]) => [
|
||||
export function useInvenTreeHotkeys(_keys: InvenTreeHotkeyItem[]) {
|
||||
const keyelems: [string, string][] = _keys.map(([key, description]) => [
|
||||
key,
|
||||
description
|
||||
]);
|
||||
|
||||
const mappedHotkeys: [
|
||||
string,
|
||||
(event: KeyboardEvent) => void,
|
||||
HotkeyItemOptions?
|
||||
][] = _keys.map(([key, _, handler, options]) => [key, handler, options]);
|
||||
// Register the hotkeys using the Mantine hook
|
||||
useHotkeys(mappedHotkeys);
|
||||
|
||||
// register to helper state to store hotkeys
|
||||
// This allows us to display the hotkeys in the UI
|
||||
useEffect(() => {
|
||||
useLocalLibState.getState().addHotkeys(keyelems);
|
||||
return () =>
|
||||
|
||||
@@ -17,6 +17,7 @@ export type PanelType = {
|
||||
disabled?: boolean;
|
||||
showHeadline?: boolean;
|
||||
supportsDirty?: boolean;
|
||||
hotkey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@babel/runtime": "^7.28.6",
|
||||
"@codecov/vite-plugin": "^1.9.1",
|
||||
"@flakiness/playwright": "^1.13.0",
|
||||
"@lingui/babel-plugin-lingui-macro": "^5.9.2",
|
||||
"@lingui/cli": "^5.9.2",
|
||||
"@lingui/macro": "^5.9.2",
|
||||
|
||||
@@ -43,7 +43,12 @@ export default defineConfig({
|
||||
retries: MAX_RETRIES,
|
||||
workers: MAX_WORKERS,
|
||||
reporter: IS_CI
|
||||
? [['html', { open: 'never' }], ['blob'], ['github']]
|
||||
? [
|
||||
['html', { open: 'never' }],
|
||||
['blob'],
|
||||
['github'],
|
||||
['@flakiness/playwright', { flakinessProject: 'InvenTree/InvenTree' }]
|
||||
]
|
||||
: 'list',
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
|
||||
@@ -11,6 +11,7 @@ import { api } from '../../App';
|
||||
import { extractAvailableFields } from '../../functions/forms';
|
||||
import useDataOutput from '../../hooks/UseDataOutput';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import {
|
||||
useGlobalSettingsState,
|
||||
useUserSettingsState
|
||||
@@ -32,6 +33,11 @@ export function PrintingActions({
|
||||
}) {
|
||||
const userSettings = useUserSettingsState();
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
const localState = useLocalState();
|
||||
|
||||
const lastUsedPrinting = useMemo(() => {
|
||||
return modelType ? localState.lastUsedPrinting[modelType] : undefined;
|
||||
}, [localState.lastUsedPrinting, modelType]);
|
||||
|
||||
const enabled = useMemo(() => items.length > 0, [items]);
|
||||
|
||||
@@ -118,6 +124,7 @@ export function PrintingActions({
|
||||
fields.template = {
|
||||
...fields.template,
|
||||
autoFill: true,
|
||||
value: lastUsedPrinting?.template,
|
||||
filters: {
|
||||
enabled: true,
|
||||
model_type: modelType,
|
||||
@@ -147,7 +154,14 @@ export function PrintingActions({
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [defaultLabelPlugin, pluginKey, printingFields.data, itemIdList]);
|
||||
}, [
|
||||
defaultLabelPlugin,
|
||||
pluginKey,
|
||||
printingFields.data,
|
||||
itemIdList,
|
||||
lastUsedPrinting,
|
||||
modelType
|
||||
]);
|
||||
|
||||
const labelModal = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.label_print),
|
||||
@@ -158,15 +172,21 @@ export function PrintingActions({
|
||||
onOpen: () => {
|
||||
setLabelDialogOpen(true);
|
||||
setItemIdList(items);
|
||||
setPluginKey(lastUsedPrinting?.plugin ?? null);
|
||||
},
|
||||
onClose: () => {
|
||||
setLabelDialogOpen(false);
|
||||
setPluginKey('');
|
||||
},
|
||||
submitText: t`Print`,
|
||||
successMessage: null,
|
||||
onFormSuccess: (response: any) => {
|
||||
setPluginKey('');
|
||||
onFormSuccess: (response: any, form: any) => {
|
||||
if (modelType) {
|
||||
const values = form?.getValues?.();
|
||||
localState.setLastUsedPrinting(modelType, {
|
||||
plugin: pluginKey || undefined,
|
||||
template: values?.template ? Number(values.template) : undefined
|
||||
});
|
||||
}
|
||||
setLabelId(response.pk);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ export function ScanButton({
|
||||
if (hotkey) {
|
||||
useInvenTreeHotkeys([
|
||||
[
|
||||
'mod+b',
|
||||
'mod+Shift+B',
|
||||
t`Open barcode scanner`,
|
||||
() => {
|
||||
open();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
CalendarOptions,
|
||||
DatesSetArg,
|
||||
DayCellContentArg,
|
||||
EventContentArg
|
||||
} from '@fullcalendar/core';
|
||||
import allLocales from '@fullcalendar/core/locales-all';
|
||||
@@ -182,6 +183,25 @@ export default function Calendar({
|
||||
[calendarProps.eventContent, eventTooltipContent]
|
||||
);
|
||||
|
||||
const monthDayCellClassNames = useCallback(
|
||||
(arg: DayCellContentArg): string[] => {
|
||||
const monthClass =
|
||||
arg.date.getMonth() % 2 === 0
|
||||
? 'fc-day-month-even'
|
||||
: 'fc-day-month-odd';
|
||||
const existing = calendarProps.dayCellClassNames;
|
||||
if (!existing) return [monthClass];
|
||||
if (typeof existing === 'function') {
|
||||
const result = existing(arg);
|
||||
const arr = Array.isArray(result) ? result : result ? [result] : [];
|
||||
return [monthClass, ...arr];
|
||||
}
|
||||
if (Array.isArray(existing)) return [monthClass, ...existing];
|
||||
return [monthClass, existing as string];
|
||||
},
|
||||
[calendarProps.dayCellClassNames]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.exportModal.modal}
|
||||
@@ -309,6 +329,11 @@ export default function Calendar({
|
||||
{...calendarProps}
|
||||
datesSet={datesSet}
|
||||
eventContent={wrappedEventContent}
|
||||
dayCellClassNames={
|
||||
isScrollView
|
||||
? monthDayCellClassNames
|
||||
: calendarProps.dayCellClassNames
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -34,6 +34,7 @@ export type ActionDropdownItem = {
|
||||
hidden?: boolean;
|
||||
onClick: (event?: any) => void;
|
||||
indicator?: Omit<IndicatorProps, 'children'>;
|
||||
hotkey?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -184,7 +185,8 @@ export function BarcodeActionDropdown({
|
||||
title: t`Link Barcode`,
|
||||
icon: <IconLink />,
|
||||
tooltip: t`Link a custom barcode to this item`,
|
||||
ChildItem: QRCodeLink
|
||||
ChildItem: QRCodeLink,
|
||||
hotkey: 'mod+L'
|
||||
}),
|
||||
GeneralBarcodeAction({
|
||||
hidden: hidden || !hash || !permission,
|
||||
@@ -220,6 +222,7 @@ function GeneralBarcodeAction({
|
||||
icon: ReactNode;
|
||||
tooltip: string;
|
||||
ChildItem: any;
|
||||
hotkey?: string;
|
||||
}): ActionDropdownItem {
|
||||
const onClick = () => {
|
||||
modals.open({
|
||||
@@ -255,7 +258,8 @@ export function DeleteItemAction(
|
||||
...props,
|
||||
icon: <IconTrash color='red' />,
|
||||
name: t`Delete`,
|
||||
tooltip: props.tooltip ?? t`Delete item`
|
||||
tooltip: props.tooltip ?? t`Delete item`,
|
||||
hotkey: 'mod+X'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -275,7 +279,8 @@ export function CancelItemAction(
|
||||
...props,
|
||||
icon: <InvenTreeIcon icon='cancel' iconProps={{ color: 'red' }} />,
|
||||
name: t`Cancel`,
|
||||
tooltip: props.tooltip ?? t`Cancel`
|
||||
tooltip: props.tooltip ?? t`Cancel`,
|
||||
hotkey: 'mod+X'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -287,6 +292,7 @@ export function DuplicateItemAction(
|
||||
...props,
|
||||
icon: <IconCopy color='green' />,
|
||||
name: t`Duplicate`,
|
||||
tooltip: props.tooltip ?? t`Duplicate item`
|
||||
tooltip: props.tooltip ?? t`Duplicate item`,
|
||||
hotkey: 'mod+D'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ export function HotkeyModal({
|
||||
);
|
||||
keys.sort((a, b) => a.key.localeCompare(b.key));
|
||||
return keys;
|
||||
}, []);
|
||||
}, [context, id]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
return {
|
||||
head: ['Hotkey', 'Action'],
|
||||
|
||||
@@ -57,7 +57,7 @@ export function PageDetail({
|
||||
useInvenTreeHotkeys([
|
||||
[
|
||||
'mod+E',
|
||||
title ? t`Edit ${title}` : t`Edit`,
|
||||
t`Edit`,
|
||||
(event) => {
|
||||
if (event.repeat) {
|
||||
return;
|
||||
@@ -68,6 +68,7 @@ export function PageDetail({
|
||||
}
|
||||
]
|
||||
]);
|
||||
useActionHotkeys(actions);
|
||||
|
||||
const pageTitleString = useMemo(
|
||||
() =>
|
||||
@@ -196,3 +197,74 @@ export function PageDetail({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useActionHotkeys(actions: ReactNode[] = []) {
|
||||
const hotkeys = useMemo(() => extractHotkeys(actions), [actions]);
|
||||
|
||||
useInvenTreeHotkeys(
|
||||
hotkeys.map(({ hotkey, onClick, name }) => [
|
||||
hotkey,
|
||||
name,
|
||||
(event) => {
|
||||
if (event.repeat) {
|
||||
return;
|
||||
}
|
||||
onClick();
|
||||
}
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function extractHotkeys(actions: ReactNode[]) {
|
||||
const calcActions = actions
|
||||
.filter(
|
||||
(action) =>
|
||||
action &&
|
||||
typeof action === 'object' &&
|
||||
'hotkey' in action &&
|
||||
action.hotkey
|
||||
)
|
||||
.map((action: any) => {
|
||||
return {
|
||||
hotkey: action?.hotkey,
|
||||
name: action?.name,
|
||||
onClick: action?.onClick
|
||||
};
|
||||
})
|
||||
.filter((action) => action !== null);
|
||||
|
||||
let primaryActionHotkeyAdded = false;
|
||||
// now iterate over the actions to extract more possible hotkeys
|
||||
actions.forEach((action: any) => {
|
||||
const typeName = action?.type?.name;
|
||||
|
||||
// dropdowns - nested actions
|
||||
if (typeName === 'ActionDropdown' || typeName === 'OptionsActionDropdown') {
|
||||
const dropdownActions = action?.props?.actions as any[];
|
||||
dropdownActions.forEach((dropdownAction: any) => {
|
||||
if (dropdownAction.hotkey) {
|
||||
calcActions.push({
|
||||
hotkey: dropdownAction.hotkey,
|
||||
name: dropdownAction.name,
|
||||
onClick: dropdownAction.onClick
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// PrimaryActionButton - use the 'mod+A' hotkey if it is enabled
|
||||
if (typeName === 'PrimaryActionButton' && action?.props?.hidden !== true) {
|
||||
if (primaryActionHotkeyAdded) return;
|
||||
|
||||
const hotkey = action?.props?.hotkey ?? 'mod+A';
|
||||
calcActions.push({
|
||||
hotkey,
|
||||
name:
|
||||
action?.props?.tooltip ?? action?.props?.title ?? t`Primary Action`,
|
||||
onClick: action?.props?.onClick
|
||||
});
|
||||
primaryActionHotkeyAdded = true;
|
||||
}
|
||||
});
|
||||
return calcActions;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function AttachmentPanel({
|
||||
name: 'attachments',
|
||||
label: t`Attachments`,
|
||||
icon: <IconPaperclip />,
|
||||
hotkey: 'mod+Shift+A',
|
||||
notification_dot: async () => {
|
||||
if (!model_type || !model_id) {
|
||||
return null;
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function NotesPanel({
|
||||
name: 'notes',
|
||||
label: t`Notes`,
|
||||
icon: <IconNotes />,
|
||||
hotkey: 'mod+Shift+N',
|
||||
notification_dot: has_note ? 'info' : null,
|
||||
content:
|
||||
model_type && model_id ? (
|
||||
|
||||
@@ -38,7 +38,11 @@ import { Boundary } from '@lib/components/Boundary';
|
||||
import { StylishText } from '@lib/components/StylishText';
|
||||
import type { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||
import { identifierString } from '@lib/functions/Conversion';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import {
|
||||
type InvenTreeHotkeyItem,
|
||||
cancelEvent,
|
||||
useInvenTreeHotkeys
|
||||
} from '@lib/functions/Events';
|
||||
import { eventModified, getBaseUrl } from '@lib/functions/Navigation';
|
||||
import { navigateToLink } from '@lib/functions/Navigation';
|
||||
import type {
|
||||
@@ -355,6 +359,22 @@ function BasePanelGroup({
|
||||
}
|
||||
}, [activePanels, panel]);
|
||||
|
||||
// hotkeys
|
||||
const hotkeys = useMemo(() => {
|
||||
const keys: InvenTreeHotkeyItem[] = [];
|
||||
activePanels.forEach((panel) => {
|
||||
if (panel.hotkey) {
|
||||
keys.push([
|
||||
panel.hotkey,
|
||||
t`Navigate to panel ${panel.name}`,
|
||||
() => handlePanelChange(panel.name)
|
||||
]);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}, [activePanels]);
|
||||
useInvenTreeHotkeys(hotkeys);
|
||||
|
||||
return (
|
||||
<Boundary label={`PanelGroup-${pageKey}`}>
|
||||
<Paper p='sm' radius='xs' shadow='xs' aria-label={`${pageKey}`}>
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function ParametersPanel({
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconListDetails />,
|
||||
hotkey: 'mod+Shift+P',
|
||||
hidden: hidden ?? false,
|
||||
notification_dot: async () => {
|
||||
if (!model_type || !model_id) {
|
||||
|
||||
@@ -33,6 +33,12 @@ interface LocalStateProps {
|
||||
setLayouts: (layouts: any, noPatch?: boolean) => void;
|
||||
showSampleDashboard: boolean;
|
||||
setShowSampleDashboard: (value: boolean) => void;
|
||||
// printing
|
||||
lastUsedPrinting: Record<string, { plugin?: string; template?: number }>;
|
||||
setLastUsedPrinting: (
|
||||
modelType: string,
|
||||
values: { plugin?: string; template?: number }
|
||||
) => void;
|
||||
// panels
|
||||
lastUsedPanels: Record<string, string>;
|
||||
setLastUsedPanel: (panelKey: string) => (value: string) => void;
|
||||
@@ -122,6 +128,25 @@ export const useLocalState = create<LocalStateProps>()(
|
||||
setShowSampleDashboard: (value) => {
|
||||
set({ showSampleDashboard: value });
|
||||
},
|
||||
// printing
|
||||
lastUsedPrinting: {},
|
||||
setLastUsedPrinting: (modelType, values) => {
|
||||
const current = get().lastUsedPrinting[modelType] || {};
|
||||
if (
|
||||
current.plugin !== values.plugin ||
|
||||
current.template !== values.template
|
||||
) {
|
||||
set({
|
||||
lastUsedPrinting: {
|
||||
...get().lastUsedPrinting,
|
||||
[modelType]: {
|
||||
...current,
|
||||
...values
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
// panels
|
||||
lastUsedPanels: {},
|
||||
setLastUsedPanel: (panelKey) => (value) => {
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
padding: 0.35em 0;
|
||||
}
|
||||
|
||||
/* Alternating month backgrounds in the multi-month scroll calendar view */
|
||||
[data-mantine-color-scheme='light'] .fc-day-month-odd:not(.fc-day-today) {
|
||||
background-color: var(--mantine-color-gray-1);
|
||||
}
|
||||
[data-mantine-color-scheme='dark'] .fc-day-month-odd:not(.fc-day-today) {
|
||||
background-color: var(--mantine-color-dark-4);
|
||||
}
|
||||
|
||||
/* mantine-datatable overrides */
|
||||
.mantine-datatable-pointer-cursor,
|
||||
.mantine-datatable-context-menu-cursor {
|
||||
|
||||
@@ -65,7 +65,42 @@ test('Printing - Label Printing', async ({ browser }) => {
|
||||
await page.getByRole('button', { name: 'Print', exact: true }).isEnabled();
|
||||
await page.getByRole('button', { name: 'Print', exact: true }).click();
|
||||
|
||||
await page.getByText('Process completed successfully').first().waitFor();
|
||||
const successMessage = page
|
||||
.getByText('Process completed successfully')
|
||||
.first();
|
||||
await successMessage.waitFor();
|
||||
await successMessage.waitFor({ state: 'hidden' });
|
||||
|
||||
// Re-open print dialog to verify persistence (issue #12129)
|
||||
await page
|
||||
.getByLabel('Stock Items')
|
||||
.getByLabel('action-menu-printing-actions')
|
||||
.click();
|
||||
await page.getByLabel('action-menu-printing-actions-print-labels').click();
|
||||
|
||||
const labelDialog = page.getByRole('dialog', { name: 'Print Label' });
|
||||
|
||||
// Wait for the dialog to fully load
|
||||
await labelDialog.getByLabel('related-field-template').waitFor();
|
||||
await labelDialog.getByLabel('related-field-plugin').waitFor();
|
||||
|
||||
// Verify the last-used template is preselected
|
||||
await expect(labelDialog).toContainText('InvenTree Stock Item Label');
|
||||
|
||||
// Verify the last-used plugin is preselected
|
||||
await expect(labelDialog).toContainText('InvenTreeLabel');
|
||||
|
||||
// Submit again without re-selecting template or plugin
|
||||
const printResponse = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/api/label/print/') &&
|
||||
response.request().method() === 'POST' &&
|
||||
response.ok()
|
||||
);
|
||||
await labelDialog.getByRole('button', { name: 'Print', exact: true }).click();
|
||||
|
||||
await printResponse;
|
||||
|
||||
await page.context().close();
|
||||
});
|
||||
|
||||
|
||||
+574
-575
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user