diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index 5da4c295a8..acd89ed51e 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -138,7 +138,7 @@ jobs: - name: Install dependencies run: invoke int.frontend-compile --extract - name: Cache Playwright browsers - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # pin@v4.3.0 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5 id: playwright-cache with: path: ~/.cache/ms-playwright @@ -217,7 +217,7 @@ jobs: - name: Install dependencies run: invoke int.frontend-compile --extract - name: Cache Playwright browsers - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # pin@v4.3.0 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # pin@v5.0.5 id: playwright-cache with: path: ~/.cache/ms-playwright diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 879aa99f1d..7737d2b7a3 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -222,7 +222,7 @@ jobs: echo "Downloaded api.yaml" - name: Running OpenAPI Spec diff action id: breaking_changes - uses: oasdiff/oasdiff-action/diff@50e6a3413e5aa9c3ae4d8393c34745be44288b46 # pin@main + uses: oasdiff/oasdiff-action/diff@f30668f65075c93440bd59ce2de73ce9e78751f4 # pin@main with: base: "api.yaml" revision: "src/backend/InvenTree/schema.yml" @@ -363,7 +363,7 @@ jobs: pip install . if: needs.paths-filter.outputs.submit-performance == 'true' - name: Performance Reporting - uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # pin@v4.15.1 + uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # pin@v4.17.0 # check if we are in inventree/inventree - reporting only works in that OIDC context if: github.repository == 'inventree/InvenTree' && needs.paths-filter.outputs.submit-performance == 'true' with: @@ -454,7 +454,7 @@ jobs: env: node_version: '>=24' - name: Performance Reporting - uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # pin@v4.15.1 + uses: CodSpeedHQ/action@9d332c4d90b43981c3e55ae8e38e68709996240f # pin@v4.17.0 with: mode: walltime run: inv dev.test --pytest diff --git a/CHANGELOG.md b/CHANGELOG.md index 678181546f..052cdafdcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking Changes -- [#9604](https://github.com/inventree/InvenTree/pull/9604) - refactors user API endpoint to be less ambiguous +- [#12107](https://github.com/inventree/InvenTree/pull/12107) makes a breaking change to the `SalesOrderStatusGroups` enum, fixing a bug where the "shipped" status was not included in the "active" group. This change may affect any external client applications which make use of the `SalesOrderStatusGroups` enum, as the "shipped" status will now be included in the "active" group instead of the "complete" group. If you are using this enum in an external client application, you will need to update your application to account for this change. +- [#9604](https://github.com/inventree/InvenTree/pull/9604) refactors user API endpoint to be less ambiguous - [#11893](https://github.com/inventree/InvenTree/pull/11893) bumps Node environment to version 24 LTS - this is only relevant if you build the frontend assets yourself ### Added +- [#12103](https://github.com/inventree/InvenTree/pull/12103) adds column-based filtering to table views in the user interface. This extends the existing table filtering functionality by allowing users to apply filters directly to individual columns. - [#12093](https://github.com/inventree/InvenTree/pull/12093) adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API) - [#12079](https://github.com/inventree/InvenTree/pull/12079) adds the ability to save filter groups for table and calendar views in the user interface. This allows users to save and reuse commonly used filter configurations, improving the usability and efficiency of the interface. - [#12077](https://github.com/inventree/InvenTree/pull/12077) adds "tags" fields to multiple new model types and a /api/tag/ endpoint for fetching tags. Also adds the ability to filter various model types by tags. diff --git a/docs/docs/assets/images/concepts/ui_table_column_filter_popover.png b/docs/docs/assets/images/concepts/ui_table_column_filter_popover.png new file mode 100644 index 0000000000..8e979ed999 Binary files /dev/null and b/docs/docs/assets/images/concepts/ui_table_column_filter_popover.png differ diff --git a/docs/docs/concepts/user_interface.md b/docs/docs/concepts/user_interface.md index 64255d6145..c9973c514c 100644 --- a/docs/docs/concepts/user_interface.md +++ b/docs/docs/concepts/user_interface.md @@ -158,6 +158,23 @@ Select the "table filters" button to open the filter selection menu Table filters are saved across browser sessions, allowing users to maintain their preferred filter settings when returning to the particular table view. +#### Column Filters + +Many table columns expose an inline filter icon directly in the column header, providing a quick way to filter by that column without opening the full filter drawer. Columns that support filtering display a small filter icon alongside the column title. The icon is highlighted when a filter for that column is currently active, giving an at-a-glance indication of which columns have active filters. + +Clicking the icon opens a compact popover anchored to the column header: + +{{ image("concepts/ui_table_column_filter_popover.png", "Column Filter Popover") }} + +**Single-filter columns** — for columns linked to one filter (e.g. *Active*, *Has IPN*, *Status*), selecting a value immediately applies the filter and the popover closes automatically. + +**Range columns** — for columns that represent a range concept (e.g. *Start Date*, *Target Date*, *Creation Date*), the popover stays open and presents multiple controls — for example *before* and *after* date pickers — so both bounds can be set in a single interaction. + +Once a filter is active, the popover shows a badge with the current value and a remove button (red ×) instead of the value picker. Clicking the × clears only that column's filter. + +!!! info "Column filters and the filter drawer share the same state" + Filters applied via a column popover appear immediately in the filter drawer's active-filter list, and filters added through the drawer are reflected in the column icons. Clearing all filters from the drawer also removes any filters set via column popovers. + #### Saved Filter Groups Frequently used combinations of filters can be saved as a named *filter group*, allowing them to be quickly recalled later without having to re-add each filter individually. diff --git a/docs/docs/report/context_variables.md b/docs/docs/report/context_variables.md index af38d594dc..e37891b711 100644 --- a/docs/docs/report/context_variables.md +++ b/docs/docs/report/context_variables.md @@ -256,7 +256,6 @@ Each part object has access to a lot of context variables about the part. The fo | expiry_date | Expiry date of the [StockItem](#stock-item) (optional) | | stocktake_date | Date of last stocktake for this item | | stocktake_user | User that performed the most recent stocktake | -| review_needed | Flag if [StockItem](#stock-item) needs review | | delete_on_deplete | If True, [StockItem](#stock-item) will be deleted when the stock level gets to zero | | status | Status of this [StockItem](#stock-item) (ref: InvenTree.status_codes.StockStatus) | | status_label | Textual representation of the status e.g. "OK" | @@ -342,7 +341,6 @@ Each part object has access to a lot of context variables about the part. The fo | note | Longer form note field | | base_cost | Base charge added to order independent of quantity e.g. "Reeling Fee" | | multiple | Multiple that the part is provided in | -| lead_time | Supplier lead time | | packaging | packaging that the part is supplied in, e.g. "Reel" | | pretty_name | The IPN, supplier name, supplier SKU and (if not null) manufacturer string joined by `|`. Ex. `P00037 | Company | 000021` | | unit_pricing | The price for one unit. | diff --git a/src/backend/InvenTree/company/models.py b/src/backend/InvenTree/company/models.py index 888bafe92e..29af645fbf 100644 --- a/src/backend/InvenTree/company/models.py +++ b/src/backend/InvenTree/company/models.py @@ -621,7 +621,6 @@ class SupplierPart( note: Longer form note field base_cost: Base charge added to order independent of quantity e.g. "Reeling Fee" multiple: Multiple that the part is provided in - lead_time: Supplier lead time packaging: packaging that the part is supplied in, e.g. "Reel" pack_quantity: Quantity of item supplied in a single pack (e.g. 30ml in a single tube) pack_quantity_native: Pack quantity, converted to "native" units of the referenced part diff --git a/src/backend/InvenTree/order/fixtures/sales_order.yaml b/src/backend/InvenTree/order/fixtures/sales_order.yaml index dfffb25fd4..9d32bd2d3b 100644 --- a/src/backend/InvenTree/order/fixtures/sales_order.yaml +++ b/src/backend/InvenTree/order/fixtures/sales_order.yaml @@ -38,6 +38,14 @@ customer: 5 status: 60 # Returned +- model: order.salesorder + pk: 6 + fields: + reference: 'ABC128' + description: "One sales order, please" + customer: 5 + status: 30 # Complete + # 1 x R_4K7_0603 - model: order.salesorderlineitem pk: 1 diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 1109e65a02..428f9f1d29 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1191,6 +1191,10 @@ class PurchaseOrder(TotalPriceMixin, Order): for item in new_items: item.set_status(status, custom_values=custom_stock_status_values) + # run validation for serialized items plugin.validate_batch_code + item.validate_batch_code() + # run validation for serialized items plugin.validate_model_instance + item.run_plugin_validation() stock_items.append(item) else: @@ -1213,6 +1217,13 @@ class PurchaseOrder(TotalPriceMixin, Order): # Bulk create new stock items if len(bulk_create_items) > 0: + # bulk_create() bypasses save()/clean() methods, so manual validation is required for each item + for item in bulk_create_items: + # run validation for items plugin.validate_batch_code + item.validate_batch_code() + # run validation for items plugin.validate_model_instance + item.run_plugin_validation() + stock.models.StockItem.objects.bulk_create( bulk_create_items, batch_size=250 ) diff --git a/src/backend/InvenTree/order/status_codes.py b/src/backend/InvenTree/order/status_codes.py index d8893a1fa3..170a4a2a44 100644 --- a/src/backend/InvenTree/order/status_codes.py +++ b/src/backend/InvenTree/order/status_codes.py @@ -63,10 +63,11 @@ class SalesOrderStatusGroups: SalesOrderStatus.PENDING.value, SalesOrderStatus.ON_HOLD.value, SalesOrderStatus.IN_PROGRESS.value, + SalesOrderStatus.SHIPPED.value, ] # Completed orders - COMPLETE = [SalesOrderStatus.SHIPPED.value, SalesOrderStatus.COMPLETE.value] + COMPLETE = [SalesOrderStatus.COMPLETE.value] class ReturnOrderStatus(StatusCode): diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 508d1849ee..4e4347a92e 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -1458,19 +1458,21 @@ class SalesOrderTest(OrderTest): def test_so_list(self): """Test the SalesOrder list API endpoint.""" # All orders - self.filter({}, 5) + self.filter({}, 6) # Filter by customer self.filter({'customer': 4}, 3) - self.filter({'customer': 5}, 2) + self.filter({'customer': 5}, 3) # Filter by outstanding - self.filter({'outstanding': True}, 3) + self.filter({'outstanding': True}, 4) self.filter({'outstanding': False}, 2) # Filter by status - self.filter({'status': SalesOrderStatus.PENDING.value}, 3) # PENDING - self.filter({'status': SalesOrderStatus.SHIPPED.value}, 1) # SHIPPED + self.filter({'status': SalesOrderStatus.PENDING.value}, 3) + self.filter({'status': SalesOrderStatus.SHIPPED.value}, 1) + self.filter({'status': SalesOrderStatus.COMPLETE.value}, 1) + self.filter({'status': SalesOrderStatus.CANCELLED.value}, 0) self.filter({'status': 99}, 0) # Invalid # Filter by "reference" @@ -1479,7 +1481,7 @@ class SalesOrderTest(OrderTest): # Filter by "assigned_to_me" self.filter({'assigned_to_me': 1}, 0) - self.filter({'assigned_to_me': 0}, 5) + self.filter({'assigned_to_me': 0}, 6) def test_total_price(self): """Unit tests for the 'total_price' field.""" @@ -1553,7 +1555,7 @@ class SalesOrderTest(OrderTest): def test_overdue(self): """Test "overdue" status.""" self.filter({'overdue': True}, 0) - self.filter({'overdue': False}, 5) + self.filter({'overdue': False}, 6) for pk in [1, 2]: order = models.SalesOrder.objects.get(pk=pk) @@ -1561,7 +1563,7 @@ class SalesOrderTest(OrderTest): order.save() self.filter({'overdue': True}, 2) - self.filter({'overdue': False}, 3) + self.filter({'overdue': False}, 4) def test_so_detail(self): """Test the SalesOrder detail endpoint.""" diff --git a/src/frontend/CHANGELOG.md b/src/frontend/CHANGELOG.md index c9c55f7a16..54d5f6f2aa 100644 --- a/src/frontend/CHANGELOG.md +++ b/src/frontend/CHANGELOG.md @@ -2,6 +2,36 @@ This file contains historical changelog information for the InvenTree UI components library. +### 1.4.6 - June 2026 + +Adds `useInvenTreeHotkeys` hook that mirrors mantine's `useHotkeys` hook, but adds visibility of the hotkeys to the user interface. + +### 1.4.5 - June 2026 + +Fixes callback signature for `` to allow for an optional `loadLocale` function to be passed in, which is used to dynamically load locale messages for the plugin. + +### 1.4.4 - June 2026 + +Fixes bundling issues associated with the `InventreeHmrPlugin` plugin function. + +### 1.4.3 - June 2026 + +Expose the `InventreeHmrPlugin` on a different path (`@inventreedb/ui/vite`) to avoid vite bundling issues. + +### 1.4.2 - June 2026 + +Fixes a bug in the `LocalizedComponent` function + +### 1.4.1 - June 2026 + +### HMR Support + +Adds support for React Fast Refresh in plugin development. This allows for a much smoother development experience when working on UI plugins, as changes to React components will now trigger a component-level update rather than a full page reload. + +### Localized Components + +Exposes a new `LocalizedComponent` function, which can be used to create React components that are automatically localized using the InvenTree server's localization system. + ### 1.4.0 - May 2026 #### Version Numbering diff --git a/src/frontend/lib/components/TableColumnSelect.tsx b/src/frontend/lib/components/TableColumnSelect.tsx index c686ce65c6..5b99ffc82f 100644 --- a/src/frontend/lib/components/TableColumnSelect.tsx +++ b/src/frontend/lib/components/TableColumnSelect.tsx @@ -23,7 +23,7 @@ export function TableColumnSelect({ {t`Select Columns`} {columns - .filter((col) => col.switchable ?? true) + .filter((col) => (col.switchable ?? true) && !col.propHidden) .map((col) => ( void, + 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]) => [ + key, + description + ]); + useEffect(() => { + useLocalLibState.getState().addHotkeys(keyelems); + return () => + useLocalLibState.getState().removeHotkeys(keyelems.map(([key]) => key)); + }, []); +} diff --git a/src/frontend/lib/index.ts b/src/frontend/lib/index.ts index 0dfb4f4742..cb554301e0 100644 --- a/src/frontend/lib/index.ts +++ b/src/frontend/lib/index.ts @@ -139,6 +139,8 @@ export { type TableStateExtraProps } from './hooks/UseTable'; +export { useInvenTreeHotkeys } from './functions/Events'; + export { type DrawerProps, DetailDrawer, @@ -153,3 +155,9 @@ export { useStoredTableState } from './states/StoredTableState'; export { useLocalLibState } from './states/LocalLibState'; + +// Plugin development utilities and hooks +export { + default as LocalizedComponent, + type LocaleLoader +} from './plugin/LocalizedComponent'; diff --git a/src/frontend/lib/plugin/InventreeHmrPlugin.tsx b/src/frontend/lib/plugin/InventreeHmrPlugin.tsx new file mode 100644 index 0000000000..c5b3ea9146 --- /dev/null +++ b/src/frontend/lib/plugin/InventreeHmrPlugin.tsx @@ -0,0 +1,40 @@ +import type { Plugin } from 'vite'; + +/** + * Vite plugin which enables hot module replacement (HMR) for InvenTree plugin development. + * + * This is for use with the InvenTree plugin creator tool, + * allowing frontend plugin code to be "live reloaded" during development. + */ +export default function InventreeHmrPlugin(): Plugin { + const fileRegex = /\.(js|jsx|ts|tsx)(\?|$)/; + + const hmrBlock = [ + '', + '// __inventree_hmr_injected__', + 'if (import.meta.hot) {', + ' import.meta.hot.accept((newModule) => {', + ' const key = new URL(import.meta.url).origin + new URL(import.meta.url).pathname;', + ' window.__plugin_hmr_callbacks?.[key]?.forEach(callback => {', + ' callback(newModule);', + ' });', + ' })', + '}' + ]; + + return { + name: 'inventree-hmr-plugin', + enforce: 'post', + + transform(code, id) { + if (!fileRegex.test(id)) return; + if (id.includes('node_modules')) return; + if (code.includes('__inventree_hmr_injected__')) return; + + return { + code: code + hmrBlock.join('\n'), + map: null + }; + } + }; +} diff --git a/src/frontend/lib/plugin/LocalizedComponent.tsx b/src/frontend/lib/plugin/LocalizedComponent.tsx new file mode 100644 index 0000000000..4a86956b3a --- /dev/null +++ b/src/frontend/lib/plugin/LocalizedComponent.tsx @@ -0,0 +1,113 @@ +import type { I18n } from '@lingui/core'; +import { I18nProvider } from '@lingui/react'; +import { Skeleton } from '@mantine/core'; +import { useEffect, useState } from 'react'; + +/* + * To dynamically load locale messages from a plugin context, + * the plugin MUST supply a callback function which can be used to load the locale messages for the plugin. + * This is because the plugin frontend code is built separately from the main frontend, + * and so cannot directly import locale messages from the main frontend. + * + * Refer to the inventree-plugin-creator tool for an example of how to use this component in a plugin context. + */ +export type LocaleLoader = (locale: string) => Promise; + +async function tryLoadLocale( + locale: string, + loader: LocaleLoader +): Promise { + try { + return await loader(locale); + } catch (error) { + console.warn(`Failed to load locale ${locale}`); + return null; + } +} + +/** + * @param i18n - The i18n instance from the plugin context + * @param locale - The current locale to load + * @param loader - The callback function to load the locale messages for the plugin + * @returns A React component which will load the locale messages and render the children once loaded + */ +async function loadPluginLocale( + i18n: I18n, + locale: string, + loader: LocaleLoader +) { + let messages = null; + + messages = await tryLoadLocale(locale, loader); + + if (!messages && locale.includes('-')) { + const fallbackLocale = locale.split('-')[0]; + console.debug( + `Locale ${locale} not found, trying fallback locale ${fallbackLocale}` + ); + messages = await tryLoadLocale(fallbackLocale, loader); + } + + if (!messages && locale.includes('_')) { + const fallbackLocale = locale.split('_')[0]; + console.debug( + `Locale ${locale} not found, trying fallback locale ${fallbackLocale}` + ); + messages = await tryLoadLocale(fallbackLocale, loader); + } + + if (!messages && locale !== 'en') { + console.debug(`Locale ${locale} not found, trying fallback locale en`); + messages = await tryLoadLocale('en', loader); + } + + if (messages?.messages) { + i18n.load(locale, messages.messages); + i18n.activate(locale); + } else { + console.error(`Failed to load any locale for ${locale}`); + } +} + +// A default locale loader which can be used if the plugin does not supply its own loader function +// Note: This will return null, as the plugin is expected to supply its own loader function which can load the locale messages for the plugin +const defaultLocaleLoader: LocaleLoader = async (_locale: string) => null; + +/** + * Wrapper function for a plugin-defined component which needs to support dynamic locale loading. + * + * This is primarily designed for usage by the InvenTree plugin creator tool + * + * @param i18n - The i18n instance from the plugin context + * @param locale - The current locale to load + * @param loadLocale - The callback function to load the locale messages for the plugin + * @param children - The child components to render once the locale is loaded + */ +export default function LocalizedComponent({ + i18n, + locale, + loadLocale, + children +}: { + i18n: I18n; + locale: string; + loadLocale?: LocaleLoader; + children: React.ReactNode; +}) { + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + setLoaded(false); + loadPluginLocale(i18n, locale, loadLocale ?? defaultLocaleLoader).then( + () => { + setLoaded(true); + } + ); + }, [i18n, locale, loadLocale]); + + return loaded ? ( + {children} + ) : ( + + ); +} diff --git a/src/frontend/lib/states/LocalLibState.tsx b/src/frontend/lib/states/LocalLibState.tsx index 76431c8d79..8089a165b2 100644 --- a/src/frontend/lib/states/LocalLibState.tsx +++ b/src/frontend/lib/states/LocalLibState.tsx @@ -10,8 +10,24 @@ export const useLocalLibState = create()( detailDrawerStack: value === false ? 0 : get().detailDrawerStack + value }); + }, + hotkeys: {}, + addHotkeys: (hotkeys) => { + const newHotkeys = { ...get().hotkeys }; + for (const [ref, details] of hotkeys) { + newHotkeys[ref] = details; + } + set({ hotkeys: newHotkeys }); + }, + removeHotkeys: (hotkeys) => { + const newHotkeys = { ...get().hotkeys }; + for (const ref of hotkeys) { + delete newHotkeys[ref]; + } + set({ hotkeys: newHotkeys }); } }), + { name: 'session-settings-inventreedb_lib' } @@ -20,4 +36,7 @@ export const useLocalLibState = create()( export interface LocalLibStateProps { detailDrawerStack: number; addDetailDrawer: (value: number | false) => void; + hotkeys: Record; + addHotkeys: (hotkeys: [string, string][]) => void; + removeHotkeys: (hotkeys: string[]) => void; } diff --git a/src/frontend/lib/types/Tables.tsx b/src/frontend/lib/types/Tables.tsx index 4b3d064f79..d47e3818e9 100644 --- a/src/frontend/lib/types/Tables.tsx +++ b/src/frontend/lib/types/Tables.tsx @@ -84,7 +84,7 @@ export type TableState = { * @param editable - Whether the value of this column can be edited * @param definition - Optional field definition for the column * @param render - A custom render function - * @param filter - A custom filter function + * @param filter - Filter name (string) to look up from tableFilters and attach an inline icon, or a custom render function for the filter popover * @param filtering - Whether the column is filterable * @param width - The width of the column * @param minWidth - The minimum width of the column @@ -109,7 +109,10 @@ export type TableColumnProps = { editable?: boolean; definition?: ApiFormFieldType; render?: (record: T, index?: number) => any; - filter?: any; + filter?: + | string + | string[] + | (({ close }: { close: () => void }) => ReactNode); filtering?: boolean; width?: number; minWidth?: string | number; diff --git a/src/frontend/package.json b/src/frontend/package.json index c1748d8d16..2d6ee20ef6 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@inventreedb/ui", "description": "UI components for the InvenTree project", - "version": "1.4.0", + "version": "1.4.6", "private": false, "type": "module", "license": "MIT", @@ -11,7 +11,8 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { - ".": "./dist/index.js" + ".": "./dist/index.js", + "./vite": "./dist/plugin/InventreeHmrPlugin.js" }, "files": [ "dist", diff --git a/src/frontend/src/components/buttons/PrintingActions.tsx b/src/frontend/src/components/buttons/PrintingActions.tsx index 7f028320b8..19e8b08e9d 100644 --- a/src/frontend/src/components/buttons/PrintingActions.tsx +++ b/src/frontend/src/components/buttons/PrintingActions.tsx @@ -1,6 +1,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ModelType } from '@lib/enums/ModelType'; import { apiUrl } from '@lib/functions/Api'; +import { useInvenTreeHotkeys } from '@lib/functions/Events'; import type { ApiFormFieldSet } from '@lib/types/Forms'; import { t } from '@lingui/core/macro'; import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react'; @@ -34,6 +35,33 @@ export function PrintingActions({ const enabled = useMemo(() => items.length > 0, [items]); + useInvenTreeHotkeys([ + [ + 'mod+P', + t`Open Print Report dialog`, + (event) => { + if (event.repeat) { + return; + } + if (enabled && !hidden) { + reportModal.open(); + } + } + ], + [ + 'mod+L', + t`Open Print Label dialog`, + (event) => { + if (event.repeat) { + return; + } + if (enabled && !hidden) { + labelModal.open(); + } + } + ] + ]); + const defaultLabelPlugin = useMemo( () => userSettings.getSetting('LABEL_DEFAULT_PRINTER'), [userSettings] diff --git a/src/frontend/src/components/buttons/ScanButton.tsx b/src/frontend/src/components/buttons/ScanButton.tsx index 286f3ef4ac..420ce8d0c7 100644 --- a/src/frontend/src/components/buttons/ScanButton.tsx +++ b/src/frontend/src/components/buttons/ScanButton.tsx @@ -1,3 +1,4 @@ +import { useInvenTreeHotkeys } from '@lib/functions/Events'; import type { ModelType } from '@lib/index'; import { t } from '@lingui/core/macro'; import { ActionIcon, Tooltip } from '@mantine/core'; @@ -14,14 +15,28 @@ import BarcodeScanDialog, { export function ScanButton({ modelType, callback, - onScanSuccess + onScanSuccess, + hotkey = false }: { modelType?: ModelType; callback?: BarcodeScanCallback; onScanSuccess?: BarcodeScanSuccessCallback; + hotkey?: boolean; }) { const [opened, { open, close }] = useDisclosure(false); + if (hotkey) { + useInvenTreeHotkeys([ + [ + 'mod+b', + t`Open barcode scanner`, + () => { + open(); + } + ] + ]); + } + return ( <> diff --git a/src/frontend/src/components/buttons/SpotlightButton.tsx b/src/frontend/src/components/buttons/SpotlightButton.tsx index f9faf95e41..3331e5a3df 100644 --- a/src/frontend/src/components/buttons/SpotlightButton.tsx +++ b/src/frontend/src/components/buttons/SpotlightButton.tsx @@ -2,12 +2,18 @@ import { t } from '@lingui/core/macro'; import { ActionIcon, Tooltip } from '@mantine/core'; import { IconCommand } from '@tabler/icons-react'; -import { firstSpotlight } from '../nav/Layout'; +import { useLocalLibState } from '@lib/states/LocalLibState'; +import { firstSpotlight, searchShortcutKey } from '../nav/Layout'; /** * A button which opens the quick command modal */ -export function SpotlightButton() { +export function SpotlightButton({ hotkey = false }: { hotkey?: boolean }) { + if (hotkey) { + useLocalLibState + .getState() + .addHotkeys([[searchShortcutKey, t`Open spotlight`]]); + } return ( ; filters?: TableFilter[]; + initialFilters?: TableFilter[]; tooltip?: (event: EventContentArg) => React.ReactNode; }) { const navigate = useNavigate(); @@ -94,7 +96,8 @@ export default function OrderCalendar({ const calendarState = useCalendar({ endpoint: modelInfo.api_endpoint, name: model.toString(), - queryParams: params + queryParams: params, + initialFilters: initialFilters }); // Build the events diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index 2bc425c174..0278bc60f8 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -8,11 +8,12 @@ import { Space, Text } from '@mantine/core'; -import { useDisclosure, useHotkeys } from '@mantine/hooks'; +import { useDisclosure } from '@mantine/hooks'; import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { type Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import { useInvenTreeHotkeys } from '@lib/functions/Events'; import { useShallow } from 'zustand/react/shallow'; import { useDashboardItems } from '../../hooks/UseDashboardItems'; import { useLocalState } from '../../states/LocalState'; @@ -61,9 +62,10 @@ export default function DashboardLayout() { const [loaded, setLoaded] = useState(false); // Keyboard shortcut for editing the dashboard layout - useHotkeys([ + useInvenTreeHotkeys([ [ 'mod+E', + t`Toggle dashboard edit mode`, () => { setEditing.toggle(); } diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index 76ada27f8d..57d99f9636 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -8,7 +8,6 @@ import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; import GetStartedWidget from './widgets/GetStartedWidget'; import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget'; import NewsWidget from './widgets/NewsWidget'; -import OrderHistoryWidget from './widgets/OrderHistoryWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; import QueryDashboardWidget from './widgets/QueryDashboardWidget'; import StocktakeDashboardWidget from './widgets/StocktakeDashboardWidget'; @@ -218,26 +217,6 @@ function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { }); } -function BuiltinHistoryWidgets(): DashboardWidgetProps[] { - return [ - OrderHistoryWidget({ - modelType: ModelType.build - }), - OrderHistoryWidget({ - modelType: ModelType.salesorder - }), - OrderHistoryWidget({ - modelType: ModelType.purchaseorder - }), - OrderHistoryWidget({ - modelType: ModelType.returnorder - }), - OrderHistoryWidget({ - modelType: ModelType.transferorder - }) - ]; -} - function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { return [ { @@ -276,7 +255,6 @@ function BuiltinActionWidgets(): DashboardWidgetProps[] { export default function DashboardWidgetLibrary(): DashboardWidgetProps[] { return [ ...BuiltinQueryCountWidgets(), - ...BuiltinHistoryWidgets(), ...BuiltinGettingStartedWidgets(), ...BuiltinSettingsWidgets(), ...BuiltinActionWidgets() diff --git a/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx deleted file mode 100644 index 88569a9cee..0000000000 --- a/src/frontend/src/components/dashboard/widgets/OrderHistoryWidget.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { ModelInformationDict } from '@lib/enums/ModelInformation'; -import type { ModelType } from '@lib/enums/ModelType'; -import { apiUrl } from '@lib/functions/Api'; -import { StylishText } from '@lib/index'; -import { t } from '@lingui/core/macro'; -import { BarChart } from '@mantine/charts'; -import { Box, LoadingOverlay, Stack } from '@mantine/core'; -import { useDocumentVisibility } from '@mantine/hooks'; -import { useQuery } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -import { useMemo } from 'react'; -import { useApi } from '../../../contexts/ApiContext'; -import { useUserState } from '../../../states/UserState'; -import type { DashboardWidgetProps } from '../DashboardWidget'; - -function OrderHistoryComponent({ - modelType, - title -}: { - modelType: ModelType; - title: string; -}) { - const modelInfo = useMemo(() => { - return ModelInformationDict[modelType]; - }, [modelType]); - - const url = useMemo(() => { - return apiUrl(modelInfo.api_endpoint); - }, [modelInfo]); - - const api = useApi(); - const visibility = useDocumentVisibility(); - - const endDate = dayjs().add(1, 'day').format('YYYY-MM-DD'); - const startDate = dayjs() - .subtract(12, 'month') - .subtract(1, 'day') - .format('YYYY-MM-DD'); - - const params = { - completed_after: startDate, - completed_before: endDate - }; - - const query = useQuery({ - queryKey: ['dashboard-order-summary', modelType, params, visibility], - enabled: visibility === 'visible', - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchInterval: 10 * 60 * 1000, // 10 minute refetch interval - staleTime: 5 * 60 * 1000, // 5 minute stale time - queryFn: () => { - if (visibility !== 'visible') { - return []; - } - - return api.get(url, { params }).then((res) => { - return res.data ?? []; - }); - } - }); - - const chartData = useMemo(() => { - const months = Array.from({ length: 13 }, (_, i) => ({ - month: dayjs() - .subtract(12 - i, 'month') - .format('MMM YY'), - count: 0 - })); - - for (const order of query.data || []) { - // Build orders use `completion_date`; all other order types use `complete_date` - const dateStr = - order.complete_date ?? order.completion_date ?? order.shipment_date; - if (!dateStr) continue; - const label = dayjs(dateStr).format('MMM YY'); - const entry = months.find((m) => m.month === label); - if (entry) entry.count++; - } - - return months; - }, [query.data]); - - return ( - - {title} - - - - - - ); -} - -/** - * Display a simple chart of the number of completed orders per month, for the last 12 months. - */ -export default function OrderHistoryWidget({ - modelType -}: { - modelType: ModelType; -}): DashboardWidgetProps { - const user = useUserState(); - - const modelInfo = useMemo(() => { - return ModelInformationDict[modelType]; - }, [modelType]); - - // Extract translated model labels - const models = modelInfo.label_multiple(); - - return { - label: `${modelType}-history`, - title: t`Completed ${models}`, - description: t`Display number of completed ${models} per month`, - minHeight: 2, - minWidth: 3, - modelType: modelType, - icon: 'chart_bar', - visible: () => user.hasViewPermission(modelType), - render: () => ( - - ) - }; -} diff --git a/src/frontend/src/components/modals/HotkeyModal.tsx b/src/frontend/src/components/modals/HotkeyModal.tsx new file mode 100644 index 0000000000..80c158d4fe --- /dev/null +++ b/src/frontend/src/components/modals/HotkeyModal.tsx @@ -0,0 +1,66 @@ +import { useLocalLibState } from '@lib/states/LocalLibState'; +import { Kbd, Table } from '@mantine/core'; +import { type UseOSReturnValue, useOs } from '@mantine/hooks'; +import type { ContextModalProps } from '@mantine/modals'; +import { Fragment, useMemo } from 'react'; + +function modRenderer(value: string, os: UseOSReturnValue) { + if (os === 'macos') { + return value.replace('mod', '⌘'); + } + return value.replace('mod', 'Ctrl'); +} + +function kbdRenderer(value: string, os: UseOSReturnValue) { + const parts = value.split('+'); + if (parts.length > 1) { + return ( + <> + {parts.map((part, idx) => ( + + {modRenderer(part, os)} + {idx < parts.length - 1 && ' + '} + + ))} + + ); + } + return {modRenderer(value, os)}; +} + +export function HotkeyModal({ + context, + id +}: ContextModalProps<{ modalBody: string }>) { + const os = useOs(); + + const hotkeys = useMemo(() => { + const keys = Object.entries(useLocalLibState.getState().hotkeys).map( + ([hotkey, description]) => { + return { + key: hotkey, + dec: description + }; + } + ); + keys.sort((a, b) => a.key.localeCompare(b.key)); + return keys; + }, []); + const data = useMemo(() => { + return { + head: ['Hotkey', 'Action'], + body: [...hotkeys.map((item) => [kbdRenderer(item.key, os), item.dec])] + }; + }, [os, hotkeys]); + + return ( + + ); +} diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index d1c7f3fb04..5872988554 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -10,11 +10,7 @@ import { Tooltip, UnstyledButton } from '@mantine/core'; -import { - useDisclosure, - useDocumentVisibility, - useHotkeys -} from '@mantine/hooks'; +import { useDisclosure, useDocumentVisibility } from '@mantine/hooks'; import { IconBell, IconSearch, IconUserBolt } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { type ReactNode, useEffect, useMemo, useState } from 'react'; @@ -22,6 +18,7 @@ import { useMatch, useNavigate } from 'react-router-dom'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; +import { useInvenTreeHotkeys } from '@lib/functions/Events'; import { getBaseUrl } from '@lib/functions/Navigation'; import { navigateToLink } from '@lib/functions/Navigation'; import { t } from '@lingui/core/macro'; @@ -60,15 +57,17 @@ export function Header() { { open: openSearchDrawer, close: closeSearchDrawer } ] = useDisclosure(false); - useHotkeys([ + useInvenTreeHotkeys([ [ '/', + t`Open search`, () => { openSearchDrawer(); } ], [ 'mod+/', + t`Open search`, () => { openSearchDrawer(); } @@ -198,8 +197,8 @@ export function Header() { - {userSettings.isSet('SHOW_SPOTLIGHT') && } - {globalSettings.isSet('BARCODE_ENABLE') && } + {userSettings.isSet('SHOW_SPOTLIGHT') && } + {globalSettings.isSet('BARCODE_ENABLE') && } { export const [firstStore, firstSpotlight] = createSpotlight(); +export const searchShortcutKey = 'mod+K'; + export default function LayoutComponent() { const navigate = useNavigate(); const location = useLocation(); @@ -140,7 +142,7 @@ export default function LayoutComponent() { leftSection: , placeholder: t`Search...` }} - shortcut={['mod + K']} + shortcut={[searchShortcutKey]} nothingFound={t`Nothing found...`} /> )} diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index 65754bdd98..5f5a441a95 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -1,8 +1,9 @@ import { Group, Paper, Space, Stack, Text } from '@mantine/core'; -import { useHotkeys } from '@mantine/hooks'; import { StylishText } from '@lib/components/StylishText'; +import { useInvenTreeHotkeys } from '@lib/functions/Events'; import { shortenString } from '@lib/functions/String'; +import { t } from '@lingui/core/macro'; import { Fragment, type ReactNode, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { usePluginUIFeature } from '../../hooks/UsePluginUIFeature'; @@ -52,9 +53,11 @@ export function PageDetail({ const userSettings = useUserSettingsState(); const navigate = useNavigate(); const location = useLocation(); - useHotkeys([ + + useInvenTreeHotkeys([ [ 'mod+E', + title ? t`Edit ${title}` : t`Edit`, (event) => { if (event.repeat) { return; diff --git a/src/frontend/src/contexts/ThemeContext.tsx b/src/frontend/src/contexts/ThemeContext.tsx index 192878f559..9e9d9e574b 100644 --- a/src/frontend/src/contexts/ThemeContext.tsx +++ b/src/frontend/src/contexts/ThemeContext.tsx @@ -11,6 +11,7 @@ import { ContextMenuProvider } from 'mantine-contextmenu'; import type { JSX } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { AboutInvenTreeModal } from '../components/modals/AboutInvenTreeModal'; +import { HotkeyModal } from '../components/modals/HotkeyModal'; import { LicenseModal } from '../components/modals/LicenseModal'; import { QrModal } from '../components/modals/QrModal'; import { ServerInfoModal } from '../components/modals/ServerInfoModal'; @@ -59,7 +60,8 @@ export function ThemeContext({ info: ServerInfoModal, about: AboutInvenTreeModal, license: LicenseModal, - qr: QrModal + qr: QrModal, + hotkey: HotkeyModal }} > diff --git a/src/frontend/src/defaults/actions.tsx b/src/frontend/src/defaults/actions.tsx index fb0af4ef78..c33b80d812 100644 --- a/src/frontend/src/defaults/actions.tsx +++ b/src/frontend/src/defaults/actions.tsx @@ -14,7 +14,8 @@ import { import type { NavigateFunction } from 'react-router-dom'; import { ModelInformationDict } from '@lib/enums/ModelInformation'; -import { ModelType, UserRoles } from '@lib/index'; +import { ModelType, StylishText, UserRoles } from '@lib/index'; +import { Trans } from '@lingui/react/macro'; import { openContextModal } from '@mantine/modals'; import { useMemo } from 'react'; import { useShallow } from 'zustand/react/shallow'; @@ -23,13 +24,26 @@ import { useGlobalSettingsState } from '../states/SettingsStates'; import { useUserState } from '../states/UserState'; import { aboutInvenTree, docLinks, licenseInfo, serverInfo } from './links'; -export function openQrModal(navigate: NavigateFunction) { +function openQrModal(navigate: NavigateFunction) { return openContextModal({ modal: 'qr', innerProps: { navigate: navigate } }); } +function openHotkeys() { + return openContextModal({ + modal: 'hotkey', + title: ( + + Hotkeys + + ), + size: 'xl', + innerProps: {} + }); +} + export function getActions(navigate: NavigateFunction) { const setNavigationOpen = useLocalState( useShallow((state) => state.setNavigationOpen) @@ -91,6 +105,13 @@ export function getActions(navigate: NavigateFunction) { description: t`Go to your user settings`, onClick: () => navigate('/settings/user'), leftSection: + }, + { + id: 'hotkeys', + label: t`Hotkeys`, + description: t`View a list of available hotkeys`, + onClick: () => openHotkeys(), + leftSection: } ]; diff --git a/src/frontend/src/hooks/UseCalendar.tsx b/src/frontend/src/hooks/UseCalendar.tsx index 2f7ec5b226..ec1d32c7ab 100644 --- a/src/frontend/src/hooks/UseCalendar.tsx +++ b/src/frontend/src/hooks/UseCalendar.tsx @@ -2,7 +2,7 @@ import type FullCalendar from '@fullcalendar/react'; import type { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; import useFilterSet from '@lib/hooks/UseFilterSet'; -import type { FilterSetState } from '@lib/types/Filters'; +import type { FilterSetState, TableFilter } from '@lib/types/Filters'; import type { UseModalReturn } from '@lib/types/Modals'; import type { DateValue } from '@mantine/dates'; import { type UseQueryResult, useQuery } from '@tanstack/react-query'; @@ -54,15 +54,17 @@ export type CalendarState = { export default function useCalendar({ name, endpoint, - queryParams + queryParams, + initialFilters }: { name: string; endpoint: ApiEndpoints; queryParams?: any; + initialFilters?: TableFilter[]; }): CalendarState { const ref = useRef(null as any); - const filterSet = useFilterSet(`calendar-${name}`); + const filterSet = useFilterSet(`calendar-${name}`, initialFilters); const [searchTerm, setSearchTerm] = useState(''); diff --git a/src/frontend/src/pages/build/BuildIndex.tsx b/src/frontend/src/pages/build/BuildIndex.tsx index f52441923d..43456c6138 100644 --- a/src/frontend/src/pages/build/BuildIndex.tsx +++ b/src/frontend/src/pages/build/BuildIndex.tsx @@ -48,8 +48,9 @@ function BuildOrderCalendar() { ); diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index bdaa271c0e..8e8887144e 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -51,8 +51,9 @@ function PurchaseOrderCalendar() { ); diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 0f1c0879b0..447229a747 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -49,8 +49,9 @@ function SalesOrderCalendar() { ); @@ -73,8 +74,9 @@ const ReturnOrderCalendar = () => { ); diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index eef0eb6e0b..5fbab60d46 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -70,8 +70,9 @@ function TransferOrderCalendar() { ); } diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index f3c61e8990..b2d4eb4f81 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -117,6 +117,7 @@ export function IPNColumn(props: TableColumnProps): TableColumn { switchable: true, title: t`IPN`, copyable: true, + filter: 'has_ipn', ...props }; } @@ -433,6 +434,7 @@ export function BooleanColumn(props: TableColumn): TableColumn { sortable: true, switchable: true, minWidth: '75px', + filter: props.filter ?? props.accessor, render: (record: any) => (
@@ -564,6 +566,7 @@ export function ProjectCodeColumn(props: TableColumnProps): TableColumn { sortable: true, title: t`Project Code`, hidden: !enabled, + filter: 'project_code', render: (record: any) => { const project_code = resolveItem( record, @@ -584,6 +587,7 @@ export function StatusColumn(props: StatusColumnProps): TableColumn { return { accessor: 'status', + filter: 'status', sortable: true, switchable: true, minWidth: '50px', @@ -636,6 +640,7 @@ export function CreatedByColumn(props: TableColumnProps): TableColumn { accessor: 'created_by', ordering: 'created_by', title: t`Created By`, + filter: 'created_by', ...props }); } @@ -665,6 +670,7 @@ export function ResponsibleColumn(props: TableColumnProps): TableColumn { accessor: 'responsible_detail', ordering: 'responsible', title: t`Responsible`, + filter: 'assigned_to', ...props }); } @@ -688,6 +694,7 @@ export function StartDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'start_date', title: t`Start Date`, + filter: ['has_start_date', 'start_date_before', 'start_date_after'], ...props }); } @@ -696,6 +703,7 @@ export function TargetDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'target_date', title: t`Target Date`, + filter: ['has_target_date', 'target_date_before', 'target_date_after'], ...props }); } @@ -704,6 +712,7 @@ export function CreationDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'creation_date', title: t`Creation Date`, + filter: ['created_before', 'created_after'], ...props }); } @@ -712,6 +721,7 @@ export function CompletionDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'completion_date', title: t`Completion Date`, + filter: ['completed_before', 'completed_after'], ...props }); } @@ -720,6 +730,7 @@ export function ShipmentDateColumn(props: TableColumnProps): TableColumn { return DateColumn({ accessor: 'shipment_date', title: t`Shipment Date`, + filter: ['shipment_date_before', 'shipment_date_after'], ...props }); } @@ -729,6 +740,7 @@ export function UpdatedAtColumn(props: TableColumnProps): TableColumn { accessor: 'updated_at', title: t`Updated`, defaultVisible: false, + filter: ['updated_before', 'updated_after'], extra: { showTime: true }, ...props }); diff --git a/src/frontend/src/tables/FilterSelectDrawer.tsx b/src/frontend/src/tables/FilterSelectDrawer.tsx index 78eecb16ea..8be8e4f9f8 100644 --- a/src/frontend/src/tables/FilterSelectDrawer.tsx +++ b/src/frontend/src/tables/FilterSelectDrawer.tsx @@ -182,15 +182,17 @@ function MultiApiFilterElement({ ); } -function FilterElement({ +export function FilterElement({ filterName, filterProps, valueOptions, + brief = false, onValueChange }: { filterName: string; filterProps: TableFilter; valueOptions: TableFilterChoice[]; + brief?: boolean; onValueChange: (value: string | null, displayValue?: any) => void; }) { const setDateValue = useCallback( @@ -226,7 +228,7 @@ function FilterElement({ filters: filterProps.apiFilter, placeholder: t`Select filter value`, model: filterProps.model, - label: t`Select filter value`, + label: brief ? undefined : t`Select filter value`, onValueChange: (value: any, instance: any) => { if (filterProps.transform) { const choice = filterProps.transform(instance); @@ -244,9 +246,10 @@ function FilterElement({ case 'text': return ( } - onChange={(e) => setTextValue(e.currentTarget.value)} - onKeyDown={(e) => { + onChange={(e: React.ChangeEvent) => + setTextValue(e.currentTarget.value) + } + onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') { onValueChange(textValue); } @@ -267,9 +272,11 @@ function FilterElement({ case 'date': return ( ); case 'choice': @@ -278,8 +285,9 @@ function FilterElement({ return (