diff --git a/.github/workflows/check_translations.yaml b/.github/workflows/check_translations.yaml
index 37a31849bd..2ce87a8c1d 100644
--- a/.github/workflows/check_translations.yaml
+++ b/.github/workflows/check_translations.yaml
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
index 42a292d3d5..561e861d57 100644
--- a/.github/workflows/docker.yaml
+++ b/.github/workflows/docker.yaml
@@ -39,7 +39,7 @@ jobs:
docker: ${{ steps.filter.outputs.docker }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
@@ -67,7 +67,7 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Test Docker Image
@@ -139,7 +139,7 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run Migration Tests
@@ -163,7 +163,7 @@ jobs:
steps:
- name: Check out repo
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set Up Python ${{ env.python_version }}
diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml
index 7065f6f054..51276ddf7f 100644
--- a/.github/workflows/frontend.yaml
+++ b/.github/workflows/frontend.yaml
@@ -46,7 +46,7 @@ jobs:
force: ${{ steps.force.outputs.force }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
@@ -69,7 +69,7 @@ jobs:
timeout-minutes: 60
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -123,7 +123,7 @@ jobs:
VITE_COVERAGE: false
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -205,7 +205,7 @@ jobs:
VITE_COVERAGE: true
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -273,7 +273,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
diff --git a/.github/workflows/import_export.yaml b/.github/workflows/import_export.yaml
index 5d692a5059..e9a500b4aa 100644
--- a/.github/workflows/import_export.yaml
+++ b/.github/workflows/import_export.yaml
@@ -48,7 +48,7 @@ jobs:
outputs:
server: ${{ steps.filter.outputs.server }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
@@ -76,7 +76,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml
index d616acde94..13ce079806 100644
--- a/.github/workflows/qc_checks.yaml
+++ b/.github/workflows/qc_checks.yaml
@@ -44,7 +44,7 @@ jobs:
submit-performance: ${{ steps.runner-perf.outputs.submit-performance }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # pin@v4.0.1
@@ -108,7 +108,7 @@ jobs:
if: needs.paths-filter.outputs.cicd == 'true' || needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.frontend == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python ${{ env.python_version }}
@@ -130,7 +130,7 @@ jobs:
if: needs.paths-filter.outputs.server == 'true' || needs.paths-filter.outputs.requirements == 'true' || needs.paths-filter.outputs.force == 'true'
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -152,7 +152,7 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python ${{ env.python_version }}
@@ -190,7 +190,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -222,7 +222,7 @@ jobs:
echo "Downloaded api.yaml"
- name: Running OpenAPI Spec diff action
id: breaking_changes
- uses: oasdiff/oasdiff-action/diff@5fbe96ede8d0c53aeadef122d7a0abb79152d493 # pin@main
+ uses: oasdiff/oasdiff-action/diff@3e9d440d37f468355457604348009f50e0cddbf3 # v0.1.4
with:
base: "api.yaml"
revision: "src/backend/InvenTree/schema.yml"
@@ -275,7 +275,7 @@ jobs:
version: ${{ needs.schema.outputs.version }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
name: Checkout Code
with:
repository: inventree/schema
@@ -332,7 +332,7 @@ jobs:
node_version: '>=24'
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -363,7 +363,7 @@ jobs:
pip install .
if: needs.paths-filter.outputs.submit-performance == 'true'
- name: Performance Reporting
- uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
+ uses: CodSpeedHQ/action@63f3e98b61959fe67f146a3ff022e4136fe9bb9c # v4.17.6
# 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:
@@ -390,7 +390,7 @@ jobs:
python_version: ${{ matrix.python_version }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -441,7 +441,7 @@ jobs:
INVENTREE_AUTO_UPDATE: true
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -454,7 +454,7 @@ jobs:
env:
node_version: '>=24'
- name: Performance Reporting
- uses: CodSpeedHQ/action@c145068895e045cc725ee76fcd2307624b65c3af # pin@v4.17.5
+ uses: CodSpeedHQ/action@63f3e98b61959fe67f146a3ff022e4136fe9bb9c # v4.17.6
with:
mode: walltime
run: inv dev.test --pytest
@@ -492,7 +492,7 @@ jobs:
- 6379:6379
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -541,7 +541,7 @@ jobs:
- 3306:3306
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -584,7 +584,7 @@ jobs:
- 5432:5432
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -618,7 +618,7 @@ jobs:
INVENTREE_PLUGINS_ENABLED: false
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
name: Checkout Code
@@ -674,8 +674,8 @@ jobs:
security-events: write
steps:
- name: Checkout repository
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor 🌈
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
+ uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 314e2891a8..7a8f69897a 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -20,7 +20,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Version Check
@@ -45,7 +45,7 @@ jobs:
artifact-metadata: write
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -115,7 +115,7 @@ jobs:
INVENTREE_DEBUG: true
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
@@ -156,7 +156,7 @@ jobs:
- debian:13
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
@@ -216,7 +216,7 @@ jobs:
pip install --require-hashes -r contrib/dev_reqs/requirements.txt
python3 .github/scripts/version_check.py
- name: Package - current release channel
- uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
+ uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main
id: package
with:
target: ${{ matrix.target }}
@@ -234,7 +234,7 @@ jobs:
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
APP_REPO=inventree/InvenTree
- name: Publish to go.packager.io - current release channel
- uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
+ uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main
with:
target: ${{ matrix.target }}
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
@@ -249,7 +249,7 @@ jobs:
PACKAGE_NAME: ${{ matrix.target }}-${{ steps.setup.outputs.version }}.tar.gz
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Package - stable release channel
- uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # pin@main
+ uses: pkgr/action/package@c5666febcd31750da6428042193fc5b2fb765435 # main
id: package-stable
with:
target: ${{ matrix.target }}
@@ -267,7 +267,7 @@ jobs:
INVENTREE_CONFIG_FILE=/opt/inventree/config.yaml
APP_REPO=inventree/InvenTree
- name: Publish to go.packager.io - stable release channel
- uses: pkgr/action/publish@3bce081ae512c5020856e237d37b3f5479d4aa71 # pin@main
+ uses: pkgr/action/publish@c5666febcd31750da6428042193fc5b2fb765435 # main
with:
target: ${{ matrix.target }}
token: ${{ secrets.PACKAGER_RELEASE_TOKEN }}
diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml
index 5e74fd9529..3b28b761d0 100644
--- a/.github/workflows/scorecard.yaml
+++ b/.github/workflows/scorecard.yaml
@@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml
index e1da913823..baf461cba7 100644
--- a/.github/workflows/translations.yaml
+++ b/.github/workflows/translations.yaml
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # pin@v6.0.3
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Environment Setup
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4bea76d17c..5bb687ad6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- [#12274](https://github.com/inventree/InvenTree/pull/12274) fixes a bug in rendering the "table field" component in frontend forms. Any plugins which make use of the "table field" component in their forms may be affected by this change, and will need to update their form definitions accordingly.
+
### Removed
## 1.4.0 - 2026-06-24
diff --git a/src/backend/InvenTree/stock/models.py b/src/backend/InvenTree/stock/models.py
index 2556359840..eb9a276838 100644
--- a/src/backend/InvenTree/stock/models.py
+++ b/src/backend/InvenTree/stock/models.py
@@ -2663,7 +2663,7 @@ class StockItem(
return True
@transaction.atomic
- def stocktake(self, count, user, **kwargs):
+ def stocktake(self, count, user, **kwargs) -> None:
"""Perform item stocktake.
Arguments:
@@ -2678,7 +2678,7 @@ class StockItem(
try:
count = Decimal(count)
except InvalidOperation:
- return False
+ return
if count < 0:
return False
@@ -2706,8 +2706,8 @@ class StockItem(
)
tracking_info['old_status_logical'] = old_status_logical
- if self.updateQuantity(count):
- tracking_info['quantity'] = float(count)
+ if self.serialized or self.updateQuantity(count):
+ tracking_info['quantity'] = 1 if self.serialized else float(count)
self.stocktake_date = InvenTree.helpers.current_date()
self.stocktake_user = user
@@ -2727,14 +2727,11 @@ class StockItem(
deltas=tracking_info,
)
- trigger_event(
- StockEvents.ITEM_COUNTED,
- 'stockitem.counted',
- id=self.id,
- quantity=float(self.quantity),
- )
-
- return True
+ trigger_event(
+ StockEvents.ITEM_COUNTED,
+ id=self.id,
+ quantity=1 if self.serialized else float(self.quantity),
+ )
@transaction.atomic
def add_stock(self, quantity, user, **kwargs):
diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py
index fdc0ebce54..9a42e4beda 100644
--- a/src/backend/InvenTree/stock/test_api.py
+++ b/src/backend/InvenTree/stock/test_api.py
@@ -2289,6 +2289,32 @@ class StocktakeTest(StockAPITestCase):
self.assertEqual(response.data['items'][0]['pk'], 1234)
self.assertEqual(response.data['items'][0]['quantity'], target[endpoint])
+ def test_count_serialized(self):
+ """Test that counting a serialized stock item correctly updates stocktake_date."""
+ import datetime
+
+ # Fixture item pk=501 is a serialized item (serial=1)
+ item = StockItem.objects.get(pk=501)
+ self.assertTrue(item.serialized)
+ self.assertEqual(item.quantity, 1)
+
+ # Clear any existing stocktake date so we can verify it gets set
+ item.stocktake_date = None
+ item.save()
+
+ url = reverse('api-stock-count')
+
+ # Count the serialized item — quantity must be 1
+ data = {'items': [{'pk': item.pk, 'quantity': 1}]}
+ response = self.post(url, data, expected_code=201)
+
+ self.assertEqual(response.data['items'][0]['pk'], item.pk)
+ self.assertEqual(response.data['items'][0]['quantity'], '1.00000')
+
+ # stocktake_date must have been set to today
+ item.refresh_from_db()
+ self.assertEqual(item.stocktake_date, datetime.date.today())
+
def test_transfer(self):
"""Test stock transfers."""
stock_item = StockItem.objects.get(pk=1234)
diff --git a/src/backend/InvenTree/stock/tests.py b/src/backend/InvenTree/stock/tests.py
index 999fc030b0..c0715bda64 100644
--- a/src/backend/InvenTree/stock/tests.py
+++ b/src/backend/InvenTree/stock/tests.py
@@ -378,10 +378,8 @@ class StockTest(StockTestBase):
self.assertEqual(it.status, StockStatus.OK.value)
# Next, perform a valid stocktake
- self.assertTrue(
- it.stocktake(
- 100, None, notes='test stocktake', status=StockStatus.DAMAGED.value
- )
+ it.stocktake(
+ 100, None, notes='test stocktake', status=StockStatus.DAMAGED.value
)
it.refresh_from_db()
diff --git a/src/frontend/lib/types/Forms.tsx b/src/frontend/lib/types/Forms.tsx
index 03acfb0ad9..48323dd6a0 100644
--- a/src/frontend/lib/types/Forms.tsx
+++ b/src/frontend/lib/types/Forms.tsx
@@ -221,8 +221,6 @@ export interface BulkEditApiFormModalProps extends ApiFormModalProps {
export type StockOperationProps = {
items?: any[];
- pk?: number;
filters?: any;
- model: ModelType.stockitem | 'location' | ModelType.part;
refresh: () => void;
};
diff --git a/src/frontend/package.json b/src/frontend/package.json
index b31c97952a..416dae747f 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.6",
+ "version": "1.5.0",
"private": false,
"type": "module",
"license": "MIT",
diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx
index df6c56e5ad..86c5144ad7 100644
--- a/src/frontend/src/components/forms/ApiForm.tsx
+++ b/src/frontend/src/components/forms/ApiForm.tsx
@@ -455,6 +455,12 @@ export function ApiForm({
}
})
.then((response) => {
+ const followPk = Array.isArray(response.data)
+ ? response.data.length === 1
+ ? response.data[0]?.pk
+ : undefined
+ : response.data?.pk;
+
switch (response.status) {
case 200:
case 201:
@@ -469,12 +475,12 @@ export function ApiForm({
if (
props.follow &&
props.modelType &&
- response.data?.pk &&
+ followPk &&
!keepOpenRef.current
) {
// If we want to automatically follow the returned data
if (!!navigate && !keepOpenRef.current) {
- navigate(getDetailUrl(props.modelType, response.data?.pk));
+ navigate(getDetailUrl(props.modelType, followPk));
}
} else if (props.table) {
// If we want to automatically update or reload a linked table
diff --git a/src/frontend/src/components/forms/fields/ApiFormField.tsx b/src/frontend/src/components/forms/fields/ApiFormField.tsx
index 85e128a1fd..d9adc749f3 100644
--- a/src/frontend/src/components/forms/fields/ApiFormField.tsx
+++ b/src/frontend/src/components/forms/fields/ApiFormField.tsx
@@ -247,7 +247,9 @@ export function ApiFormField({
);
case 'tags':
diff --git a/src/frontend/src/components/forms/fields/RelatedModelField.tsx b/src/frontend/src/components/forms/fields/RelatedModelField.tsx
index bb88471c0c..a744f3f4ac 100644
--- a/src/frontend/src/components/forms/fields/RelatedModelField.tsx
+++ b/src/frontend/src/components/forms/fields/RelatedModelField.tsx
@@ -419,6 +419,7 @@ export function RelatedModelField({
...definition,
addCreateFields: undefined,
autoFill: undefined,
+ autoFillFilters: undefined,
modelRenderer: undefined,
onValueChange: undefined,
adjustFilters: undefined,
diff --git a/src/frontend/src/components/forms/fields/TableField.tsx b/src/frontend/src/components/forms/fields/TableField.tsx
index 5d209b1fee..d0792a2172 100644
--- a/src/frontend/src/components/forms/fields/TableField.tsx
+++ b/src/frontend/src/components/forms/fields/TableField.tsx
@@ -1,9 +1,23 @@
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
-import { Alert, Container, Group, Stack, Table, Text } from '@mantine/core';
+import {
+ Alert,
+ Container,
+ Group,
+ NumberInput,
+ Stack,
+ Table,
+ Text
+} from '@mantine/core';
import { IconExclamationCircle } from '@tabler/icons-react';
-import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
-import type { FieldValues, UseControllerReturn } from 'react-hook-form';
+import {
+ type ReactNode,
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef
+} from 'react';
import { AddItemButton } from '@lib/components/AddItemButton';
import { identifierString } from '@lib/functions/Conversion';
@@ -13,35 +27,34 @@ import { StandaloneField } from '../StandaloneField';
export interface TableFieldRowProps {
item: any;
- idx: number;
+ rowId: string | number;
rowErrors: any;
- control: UseControllerReturn;
- changeFn: (idx: number, key: string, value: any) => void;
- removeFn: (idx: number) => void;
+ changeFn: (rowId: number | string, key: string, value: any) => void;
+ removeFn: (rowId: number | string) => void;
}
function TableFieldRow({
item,
- idx,
- errors,
- definition,
- control,
+ rowId,
+ rowErrors,
+ modelRenderer,
+ columnCount,
changeFn,
removeFn
}: Readonly<{
item: any;
- idx: number;
- errors: any;
- definition: ApiFormFieldType;
- control: UseControllerReturn;
- changeFn: (idx: number, key: string, value: any) => void;
- removeFn: (idx: number) => void;
+ rowId: string | number;
+ rowErrors: any;
+ modelRenderer?: ApiFormFieldType['modelRenderer'];
+ columnCount?: number;
+ changeFn: (rowId: number | string, key: string, value: any) => void;
+ removeFn: (rowId: number | string) => void;
}>) {
// Table fields require render function
- if (!definition.modelRenderer) {
+ if (!modelRenderer) {
return (
-
+
}>
{t`modelRenderer entry required for tables`}
@@ -50,16 +63,60 @@ function TableFieldRow({
);
}
- return definition.modelRenderer({
+ return modelRenderer({
item: item,
- idx: idx,
- rowErrors: errors,
- control: control,
+ rowId: rowId,
+ rowErrors: rowErrors,
changeFn: changeFn,
removeFn: removeFn
});
}
+// Memoize each table field row, so that we don't re-render the entire table when a single row is updated
+function areShallowEqual(previousValue: any, nextValue: any): boolean {
+ if (previousValue === nextValue) {
+ return true;
+ }
+
+ if (!previousValue || !nextValue) {
+ return false;
+ }
+
+ if (typeof previousValue !== 'object' || typeof nextValue !== 'object') {
+ return previousValue === nextValue;
+ }
+
+ const previousKeys = Object.keys(previousValue);
+ const nextKeys = Object.keys(nextValue);
+
+ if (previousKeys.length !== nextKeys.length) {
+ return false;
+ }
+
+ for (const key of previousKeys) {
+ if (previousValue[key] !== nextValue[key]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+const MemoizedTableFieldRow = memo(
+ TableFieldRow,
+ (previousProps, nextProps) => {
+ return (
+ previousProps.rowId === nextProps.rowId &&
+ areShallowEqual(previousProps.item, nextProps.item) &&
+ areShallowEqual(previousProps.rowErrors, nextProps.rowErrors) &&
+ previousProps.modelRenderer === nextProps.modelRenderer &&
+ previousProps.changeFn === nextProps.changeFn &&
+ previousProps.removeFn === nextProps.removeFn &&
+ previousProps.columnCount === nextProps.columnCount
+ );
+ }
+);
+
export function TableFieldErrorWrapper({
props,
errorKey,
@@ -83,33 +140,131 @@ export function TableFieldErrorWrapper({
);
}
-export function TableField({
+function TableFieldComponent({
definition,
fieldName,
- control
+ value,
+ onChange,
+ error
}: Readonly<{
definition: ApiFormFieldType;
fieldName: string;
- control: UseControllerReturn;
+ value: any;
+ onChange: (value: any) => void;
+ error?: any;
}>) {
- const {
- field,
- fieldState: { error }
- } = control;
- const { value } = field;
+ const valueRef = useRef(value);
+ const onChangeRef = useRef(onChange);
+ const rowIndexByIdRef = useRef(new Map());
+ const generatedRowIdsRef = useRef(new WeakMap