simple-module-settings 0.0.40__tar.gz → 0.0.42__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/PKG-INFO +4 -4
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/package.json +4 -4
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/pyproject.toml +4 -4
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_managed_keys.py +9 -1
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/constants.py +1 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/deps.py +10 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/_create_form.py +3 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/api.py +16 -16
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/views.py +14 -9
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/errors.py +9 -1
- simple_module_settings-0.0.42/settings/known_keys.py +47 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/FieldSource.tsx +3 -3
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/ModuleFieldRow.tsx +11 -11
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/ModuleForm.tsx +31 -4
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/service.py +25 -1
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_known_keys_meta.py +21 -53
- simple_module_settings-0.0.42/tests/test_settings_generic_module_guard.py +109 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_secret_urls.py +11 -11
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/ModuleForm.test.tsx +1 -1
- simple_module_settings-0.0.42/tests-js/settings_ModuleFieldLayout.test.tsx +62 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/settings_ModuleFormRevert.test.tsx +62 -0
- simple_module_settings-0.0.40/settings/known_keys.py +0 -86
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/.gitignore +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/LICENSE +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/README.md +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/__init__.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_announce.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_field_meta.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_listing.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_module_settings.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_module_settings_props.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_row_masking.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_secrets.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/_unique_write.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/browse_query.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/cli.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/contracts/__init__.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/contracts/accessor.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/contracts/events.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/contracts/invalidation.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/contracts/registry.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/contracts/schemas.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/__init__.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/module_api.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/tenant_api.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/env_vars.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/exceptions.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/hydrate.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/invalidation.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/locales/de.json +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/locales/en.json +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/models.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/module.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/module_registry.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/Browse.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/Create.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/Edit.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/ModulesEdit.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/api.generated.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/api.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/FieldInput.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/JsonInput.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/KeyField.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/ResolvedValue.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/ScopeTabs.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/StoreCards.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/StoreTable.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/TestConnectionButton.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/ValueInput.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/last-test.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/validation-errors.test.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/components/validation-errors.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/routes.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/pages/types.ts +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/py.typed +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/registration.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/reload.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/services.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/settings.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/store.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/tenant_scope.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/tenant_view.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_clear_route_per_scope.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_cli_import.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_concurrent_overrides.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_duplicate_and_race.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_env_pinned.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_hydrate.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_hydrate_related_fields.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_lenient_related_fields_validators.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_module_api.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_module_api_json_fields.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_module_registry.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_module_settings.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_module_settings_render.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_module_settings_serialize.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_registration.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_reload.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_accessor.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_api.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_commit_invalidation.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_default_clears_override.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_domain_errors.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_env_unprefixed.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_events.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_field_sources.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_framework_reexports.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_group_metadata.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_host_restart_flags.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_module.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_schemas.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_secret_json_roundtrip.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_secret_masking.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_service.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_typed_secret_masking.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_settings_view_authz.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_store.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_store_filters.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_store_secret_masking.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_tenant_settings.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_testable_checks.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests/test_upload_key_delete.py +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/JsonFields.test.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/ModulesEdit.test.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/RestartHint.test.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/ScopeTabs.test.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tests-js/settings_TestConnectionButton.test.tsx +0 -0
- {simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_settings
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.42
|
|
4
4
|
Summary: Runtime settings UI — modules plug their own settings panels into a shared admin view
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -21,9 +21,9 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: simple-module-core==0.0.
|
|
25
|
-
Requires-Dist: simple-module-db==0.0.
|
|
26
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
24
|
+
Requires-Dist: simple-module-core==0.0.42
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.42
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.42
|
|
27
27
|
Requires-Dist: typer<0.28,>=0.12
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simple-module-py/settings",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.42",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Frontend assets for the Settings module",
|
|
6
6
|
"peerDependencies": {
|
|
7
|
+
"@inertiajs/react": "^3.9.1",
|
|
8
|
+
"@simple-module-py/ui": "*",
|
|
7
9
|
"react": "^19.0.0",
|
|
8
|
-
"react-dom": "^19.
|
|
9
|
-
"@inertiajs/react": "^3.7.0",
|
|
10
|
-
"@simple-module-py/ui": "*"
|
|
10
|
+
"react-dom": "^19.3.1"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@simple-module-py/tsconfig": "*"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_settings"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.42"
|
|
4
4
|
description = "Runtime settings UI — modules plug their own settings panels into a shared admin view"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,9 +21,9 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"simple_module_core==0.0.
|
|
25
|
-
"simple_module_db==0.0.
|
|
26
|
-
"simple_module_hosting==0.0.
|
|
24
|
+
"simple_module_core==0.0.42",
|
|
25
|
+
"simple_module_db==0.0.42",
|
|
26
|
+
"simple_module_hosting==0.0.42",
|
|
27
27
|
"typer>=0.12,<0.28",
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -22,7 +22,7 @@ from __future__ import annotations
|
|
|
22
22
|
|
|
23
23
|
from collections.abc import Awaitable, Callable
|
|
24
24
|
|
|
25
|
-
from settings.constants import ERR_MANAGED_KEY_DELETE
|
|
25
|
+
from settings.constants import ERR_MANAGED_KEY_DELETE, ERR_MODULE_KEY_EDIT
|
|
26
26
|
from settings.contracts.registry import SettingsRegistry, clear_route
|
|
27
27
|
from settings.contracts.schemas import SettingScope
|
|
28
28
|
from settings.exceptions import SettingsError
|
|
@@ -58,3 +58,11 @@ async def ensure_deletable(
|
|
|
58
58
|
if orphaned:
|
|
59
59
|
return
|
|
60
60
|
raise ManagedKeyError(key, clear_route(definition, scope))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ModuleKeyError(SettingsError):
|
|
64
|
+
"""A generic mutation would bypass the module editor's validation."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, package: str, edit_via: str) -> None:
|
|
67
|
+
self.edit_via = edit_via
|
|
68
|
+
super().__init__(ERR_MODULE_KEY_EDIT.format(package=package, edit_via=edit_via))
|
|
@@ -145,6 +145,7 @@ ERR_VALUE_MISMATCH: Final = "value does not parse as declared value_type"
|
|
|
145
145
|
ERR_UNKNOWN_TENANT: Final = "Unknown tenant"
|
|
146
146
|
ERR_NO_ACTIVE_TENANT: Final = "No active organisation for this request"
|
|
147
147
|
ERR_MANAGED_KEY_DELETE: Final = "This setting is managed elsewhere; clear it at {clear_via}"
|
|
148
|
+
ERR_MODULE_KEY_EDIT: Final = "Module settings must be edited at {edit_via} (package: {package})"
|
|
148
149
|
ERR_NOT_TENANT_OVERRIDABLE: Final = "This setting cannot be changed per organisation"
|
|
149
150
|
|
|
150
151
|
# ── HTTP ─────────────────────────────────────────────────────────────
|
|
@@ -12,6 +12,7 @@ from typing import Annotated
|
|
|
12
12
|
|
|
13
13
|
from fastapi import Depends, Request
|
|
14
14
|
from simple_module_core.app_state import get_services
|
|
15
|
+
from simple_module_core.module_settings import get_module_settings_registry
|
|
15
16
|
from simple_module_db.deps import get_db
|
|
16
17
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
18
|
|
|
@@ -36,6 +37,15 @@ async def get_setting_service(
|
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
async def get_generic_setting_service(
|
|
41
|
+
request: Request,
|
|
42
|
+
service: SettingService = Depends(get_setting_service),
|
|
43
|
+
) -> SettingService:
|
|
44
|
+
"""Raw store routes must send module configuration through its own editor."""
|
|
45
|
+
service.module_registry = get_module_settings_registry(request.app)
|
|
46
|
+
return service
|
|
47
|
+
|
|
48
|
+
|
|
39
49
|
def get_settings_registry(request: Request) -> SettingsRegistry:
|
|
40
50
|
"""Return the app-wide settings registry populated during boot."""
|
|
41
51
|
services = getattr(request.app.state, MODULE_PACKAGE)
|
{simple_module_settings-0.0.40 → simple_module_settings-0.0.42}/settings/endpoints/_create_form.py
RENAMED
|
@@ -7,6 +7,7 @@ from pydantic import ValidationError
|
|
|
7
7
|
from simple_module_hosting.inertia_utils import redirect_back_with_errors, validation_errors_to_dict
|
|
8
8
|
from starlette.responses import RedirectResponse
|
|
9
9
|
|
|
10
|
+
from settings._managed_keys import ModuleKeyError
|
|
10
11
|
from settings._unique_write import DuplicateSettingError
|
|
11
12
|
from settings.constants import ERR_SETTING_EXISTS
|
|
12
13
|
from settings.contracts.schemas import SettingCreate, SettingScope
|
|
@@ -31,4 +32,6 @@ async def create_from_form(
|
|
|
31
32
|
await service.create(data)
|
|
32
33
|
except DuplicateSettingError:
|
|
33
34
|
return redirect_back_with_errors(request, {"key": ERR_SETTING_EXISTS})
|
|
35
|
+
except ModuleKeyError as exc:
|
|
36
|
+
return redirect_back_with_errors(request, {"key": str(exc)})
|
|
34
37
|
return RedirectResponse(redirect_to, status_code=303)
|
|
@@ -37,7 +37,7 @@ from settings.contracts.schemas import (
|
|
|
37
37
|
SettingUpdate,
|
|
38
38
|
SettingUpsert,
|
|
39
39
|
)
|
|
40
|
-
from settings.deps import
|
|
40
|
+
from settings.deps import get_generic_setting_service
|
|
41
41
|
from settings.service import SettingService
|
|
42
42
|
from settings.tenant_scope import (
|
|
43
43
|
is_known_tenant,
|
|
@@ -69,7 +69,7 @@ def _not_found() -> HTTPException:
|
|
|
69
69
|
async def list_settings(
|
|
70
70
|
scope: SettingScope | None = Query(default=None, alias=QP_SCOPE),
|
|
71
71
|
scope_id: str = Query(default=SYSTEM_SCOPE_ID, alias=QP_SCOPE_ID),
|
|
72
|
-
service: SettingService = Depends(
|
|
72
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
73
73
|
) -> list[SettingOut]:
|
|
74
74
|
if scope is None:
|
|
75
75
|
return await service.list_all()
|
|
@@ -84,7 +84,7 @@ async def resolve_setting(
|
|
|
84
84
|
key: str,
|
|
85
85
|
user_id: str | None = Query(default=None, alias=QP_USER_ID),
|
|
86
86
|
tenant_id: str | None = Query(default=None, alias=QP_TENANT_ID),
|
|
87
|
-
service: SettingService = Depends(
|
|
87
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
88
88
|
) -> SettingOut:
|
|
89
89
|
result = await service.resolve(key, user_id=user_id, tenant_id=tenant_id)
|
|
90
90
|
if result is None:
|
|
@@ -97,7 +97,7 @@ async def resolve_setting(
|
|
|
97
97
|
|
|
98
98
|
@router.get(API_SYSTEM_PATH, response_model=SettingOut, dependencies=_VIEW)
|
|
99
99
|
async def get_system_setting(
|
|
100
|
-
key: str, service: SettingService = Depends(
|
|
100
|
+
key: str, service: SettingService = Depends(get_generic_setting_service)
|
|
101
101
|
) -> SettingOut:
|
|
102
102
|
result = await service.get_scoped(SettingScope.SYSTEM, SYSTEM_SCOPE_ID, key)
|
|
103
103
|
if result is None:
|
|
@@ -109,14 +109,14 @@ async def get_system_setting(
|
|
|
109
109
|
async def upsert_system_setting(
|
|
110
110
|
key: str,
|
|
111
111
|
data: SettingUpsert,
|
|
112
|
-
service: SettingService = Depends(
|
|
112
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
113
113
|
) -> SettingOut:
|
|
114
114
|
return await service.upsert_scoped(SettingScope.SYSTEM, SYSTEM_SCOPE_ID, key, data)
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
@router.delete(API_SYSTEM_PATH, status_code=STATUS_NO_CONTENT, dependencies=_DELETE)
|
|
118
118
|
async def delete_system_setting(
|
|
119
|
-
key: str, service: SettingService = Depends(
|
|
119
|
+
key: str, service: SettingService = Depends(get_generic_setting_service)
|
|
120
120
|
) -> None:
|
|
121
121
|
if not await service.delete_scoped(SettingScope.SYSTEM, SYSTEM_SCOPE_ID, key):
|
|
122
122
|
raise _not_found()
|
|
@@ -133,7 +133,7 @@ async def get_tenant_setting(
|
|
|
133
133
|
scope_id: str,
|
|
134
134
|
key: str,
|
|
135
135
|
request: Request,
|
|
136
|
-
service: SettingService = Depends(
|
|
136
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
137
137
|
) -> SettingOut:
|
|
138
138
|
await require_known_tenant(request, scope_id)
|
|
139
139
|
result = await service.get_scoped(SettingScope.TENANT, scope_id, key)
|
|
@@ -148,7 +148,7 @@ async def upsert_tenant_setting(
|
|
|
148
148
|
key: str,
|
|
149
149
|
data: SettingUpsert,
|
|
150
150
|
request: Request,
|
|
151
|
-
service: SettingService = Depends(
|
|
151
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
152
152
|
) -> SettingOut:
|
|
153
153
|
await require_known_tenant(request, scope_id)
|
|
154
154
|
await run_check(request, scope_id, key, data.value)
|
|
@@ -159,7 +159,7 @@ async def upsert_tenant_setting(
|
|
|
159
159
|
async def delete_tenant_setting(
|
|
160
160
|
scope_id: str,
|
|
161
161
|
key: str,
|
|
162
|
-
service: SettingService = Depends(
|
|
162
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
163
163
|
) -> None:
|
|
164
164
|
if not await service.delete_scoped(SettingScope.TENANT, scope_id, key):
|
|
165
165
|
raise _not_found()
|
|
@@ -169,7 +169,7 @@ async def delete_tenant_setting(
|
|
|
169
169
|
async def get_user_setting(
|
|
170
170
|
scope_id: str,
|
|
171
171
|
key: str,
|
|
172
|
-
service: SettingService = Depends(
|
|
172
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
173
173
|
) -> SettingOut:
|
|
174
174
|
result = await service.get_scoped(SettingScope.USER, scope_id, key)
|
|
175
175
|
if result is None:
|
|
@@ -182,7 +182,7 @@ async def upsert_user_setting(
|
|
|
182
182
|
scope_id: str,
|
|
183
183
|
key: str,
|
|
184
184
|
data: SettingUpsert,
|
|
185
|
-
service: SettingService = Depends(
|
|
185
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
186
186
|
) -> SettingOut:
|
|
187
187
|
return await service.upsert_scoped(SettingScope.USER, scope_id, key, data)
|
|
188
188
|
|
|
@@ -191,7 +191,7 @@ async def upsert_user_setting(
|
|
|
191
191
|
async def delete_user_setting(
|
|
192
192
|
scope_id: str,
|
|
193
193
|
key: str,
|
|
194
|
-
service: SettingService = Depends(
|
|
194
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
195
195
|
) -> None:
|
|
196
196
|
if not await service.delete_scoped(SettingScope.USER, scope_id, key):
|
|
197
197
|
raise _not_found()
|
|
@@ -204,7 +204,7 @@ async def delete_user_setting(
|
|
|
204
204
|
async def create_setting(
|
|
205
205
|
data: SettingCreate,
|
|
206
206
|
request: Request,
|
|
207
|
-
service: SettingService = Depends(
|
|
207
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
208
208
|
) -> SettingOut:
|
|
209
209
|
if data.scope is SettingScope.TENANT:
|
|
210
210
|
# A body field, not a path segment: an unknown tenant is invalid input.
|
|
@@ -219,7 +219,7 @@ async def create_setting(
|
|
|
219
219
|
|
|
220
220
|
@router.get(API_BY_ID_PATH, response_model=SettingOut, dependencies=_VIEW)
|
|
221
221
|
async def get_setting(
|
|
222
|
-
setting_id: int, service: SettingService = Depends(
|
|
222
|
+
setting_id: int, service: SettingService = Depends(get_generic_setting_service)
|
|
223
223
|
) -> SettingOut:
|
|
224
224
|
result = await service.get_by_id(setting_id)
|
|
225
225
|
if result is None:
|
|
@@ -232,7 +232,7 @@ async def update_setting(
|
|
|
232
232
|
setting_id: int,
|
|
233
233
|
data: SettingUpdate,
|
|
234
234
|
request: Request,
|
|
235
|
-
service: SettingService = Depends(
|
|
235
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
236
236
|
) -> SettingOut:
|
|
237
237
|
current = await service.get_by_id(setting_id)
|
|
238
238
|
if current is not None and current.scope is SettingScope.TENANT and data.value is not None:
|
|
@@ -245,7 +245,7 @@ async def update_setting(
|
|
|
245
245
|
|
|
246
246
|
@router.delete(API_BY_ID_PATH, status_code=STATUS_NO_CONTENT, dependencies=_DELETE)
|
|
247
247
|
async def delete_setting(
|
|
248
|
-
setting_id: int, service: SettingService = Depends(
|
|
248
|
+
setting_id: int, service: SettingService = Depends(get_generic_setting_service)
|
|
249
249
|
) -> None:
|
|
250
250
|
if not await service.delete(setting_id):
|
|
251
251
|
raise _not_found()
|
|
@@ -22,7 +22,7 @@ from simple_module_inertia import InertiaResponse
|
|
|
22
22
|
from starlette.responses import RedirectResponse
|
|
23
23
|
|
|
24
24
|
from settings import browse_query, known_keys
|
|
25
|
-
from settings._managed_keys import ManagedKeyError
|
|
25
|
+
from settings._managed_keys import ManagedKeyError, ModuleKeyError
|
|
26
26
|
from settings._module_settings import (
|
|
27
27
|
_package_of,
|
|
28
28
|
collect_module_settings,
|
|
@@ -53,7 +53,7 @@ from settings.constants import (
|
|
|
53
53
|
VIEW_STORE_PATH,
|
|
54
54
|
)
|
|
55
55
|
from settings.contracts.schemas import SettingUpdate
|
|
56
|
-
from settings.deps import
|
|
56
|
+
from settings.deps import get_generic_setting_service
|
|
57
57
|
from settings.endpoints._create_form import create_from_form
|
|
58
58
|
from settings.service import SettingService
|
|
59
59
|
from settings.tenant_scope import tenant_update_error
|
|
@@ -81,7 +81,7 @@ router = APIRouter(dependencies=[Depends(RequiresPermission(PERM_VIEW))])
|
|
|
81
81
|
@router.get(VIEW_STORE_PATH, response_model=None)
|
|
82
82
|
async def browse(
|
|
83
83
|
inertia: InertiaDep,
|
|
84
|
-
service: SettingService = Depends(
|
|
84
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
85
85
|
scope: str = SCOPE_ALL,
|
|
86
86
|
q: str = "",
|
|
87
87
|
page: browse_query.PageParam = 1,
|
|
@@ -139,7 +139,7 @@ async def create_view(request: Request, inertia: InertiaDep) -> InertiaResponse:
|
|
|
139
139
|
async def edit_view(
|
|
140
140
|
setting_id: int,
|
|
141
141
|
inertia: InertiaDep,
|
|
142
|
-
service: SettingService = Depends(
|
|
142
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
143
143
|
) -> InertiaResponse:
|
|
144
144
|
item = await service.get_by_id(setting_id)
|
|
145
145
|
if item is None:
|
|
@@ -159,7 +159,7 @@ async def edit_view(
|
|
|
159
159
|
)
|
|
160
160
|
async def create_action(
|
|
161
161
|
request: Request,
|
|
162
|
-
service: SettingService = Depends(
|
|
162
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
163
163
|
) -> RedirectResponse:
|
|
164
164
|
return await create_from_form(request, service, _REDIRECT_SETTINGS)
|
|
165
165
|
|
|
@@ -172,7 +172,7 @@ async def create_action(
|
|
|
172
172
|
async def update_action(
|
|
173
173
|
setting_id: int,
|
|
174
174
|
request: Request,
|
|
175
|
-
service: SettingService = Depends(
|
|
175
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
176
176
|
) -> RedirectResponse:
|
|
177
177
|
body = await request.json()
|
|
178
178
|
try:
|
|
@@ -181,7 +181,10 @@ async def update_action(
|
|
|
181
181
|
return redirect_back_with_errors(request, validation_errors_to_dict(exc))
|
|
182
182
|
if error := await tenant_update_error(request, await service.get_by_id(setting_id), data.value):
|
|
183
183
|
return redirect_back_with_errors(request, {"value": error})
|
|
184
|
-
|
|
184
|
+
try:
|
|
185
|
+
await service.update(setting_id, data)
|
|
186
|
+
except ModuleKeyError as exc:
|
|
187
|
+
return redirect_back_with_errors(request, {"value": str(exc)})
|
|
185
188
|
return RedirectResponse(_REDIRECT_SETTINGS, status_code=303)
|
|
186
189
|
|
|
187
190
|
|
|
@@ -194,7 +197,7 @@ async def delete_action(
|
|
|
194
197
|
setting_id: int,
|
|
195
198
|
request: Request,
|
|
196
199
|
t: TranslatorDep,
|
|
197
|
-
service: SettingService = Depends(
|
|
200
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
198
201
|
) -> RedirectResponse:
|
|
199
202
|
try:
|
|
200
203
|
await service.delete(setting_id)
|
|
@@ -204,6 +207,8 @@ async def delete_action(
|
|
|
204
207
|
"settings.browse.delete_managed_error", setting=exc.key, clear_via=exc.clear_via
|
|
205
208
|
)
|
|
206
209
|
return redirect_back_with_errors(request, {"delete": message})
|
|
210
|
+
except ModuleKeyError as exc:
|
|
211
|
+
return redirect_back_with_errors(request, {"delete": str(exc)})
|
|
207
212
|
return RedirectResponse(_REDIRECT_SETTINGS, status_code=303)
|
|
208
213
|
|
|
209
214
|
|
|
@@ -211,7 +216,7 @@ async def delete_action(
|
|
|
211
216
|
async def modules_view(
|
|
212
217
|
request: Request,
|
|
213
218
|
inertia: InertiaDep,
|
|
214
|
-
service: SettingService = Depends(
|
|
219
|
+
service: SettingService = Depends(get_generic_setting_service),
|
|
215
220
|
) -> InertiaResponse:
|
|
216
221
|
"""Read-only view of every module's pydantic ``BaseSettings`` instance.
|
|
217
222
|
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from fastapi import FastAPI, Request
|
|
6
6
|
from fastapi.responses import JSONResponse, Response
|
|
7
7
|
|
|
8
|
-
from settings._managed_keys import ManagedKeyError
|
|
8
|
+
from settings._managed_keys import ManagedKeyError, ModuleKeyError
|
|
9
9
|
from settings.constants import STATUS_UNPROCESSABLE
|
|
10
10
|
|
|
11
11
|
|
|
@@ -16,5 +16,13 @@ async def _managed_key(request: Request, exc: Exception) -> Response:
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
async def _module_key(request: Request, exc: Exception) -> Response:
|
|
20
|
+
assert isinstance(exc, ModuleKeyError)
|
|
21
|
+
return JSONResponse(
|
|
22
|
+
{"detail": str(exc), "edit_via": exc.edit_via}, status_code=STATUS_UNPROCESSABLE
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def install_exception_handlers(app: FastAPI) -> None:
|
|
20
27
|
app.add_exception_handler(ManagedKeyError, _managed_key)
|
|
28
|
+
app.add_exception_handler(ModuleKeyError, _module_key)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Autocomplete for declared generic keys; module settings use their own editor.
|
|
2
|
+
|
|
3
|
+
Defaults are masked here because Browse renders them in its resolved-value panel.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
from fastapi.encoders import jsonable_encoder
|
|
12
|
+
from simple_module_core.module_settings import get_module_settings_registry
|
|
13
|
+
from simple_module_core.secret_fields import conceals_secret, mask
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _from_definition(definition) -> dict[str, Any]:
|
|
17
|
+
"""Mask declarations directly, as module fields are no longer suggested."""
|
|
18
|
+
value_type = str(definition.value_type)
|
|
19
|
+
default = jsonable_encoder(definition.default)
|
|
20
|
+
secret = conceals_secret(definition.key, default, value_type)
|
|
21
|
+
return {
|
|
22
|
+
"key": definition.key,
|
|
23
|
+
"type": value_type,
|
|
24
|
+
"env_var": "",
|
|
25
|
+
"env_readable": False,
|
|
26
|
+
"env_set": False,
|
|
27
|
+
"env_value": None,
|
|
28
|
+
"default": mask(default) if secret else default,
|
|
29
|
+
"requires_restart": False,
|
|
30
|
+
"is_secret": secret,
|
|
31
|
+
"choices": None,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build(app: FastAPI) -> list[dict[str, Any]]:
|
|
36
|
+
"""Declared generic keys only; registered module packages use their editor."""
|
|
37
|
+
suggestions: dict[str, dict[str, Any]] = {}
|
|
38
|
+
modules = get_module_settings_registry(app)
|
|
39
|
+
registry = getattr(getattr(app.state, "settings", None), "registry", None)
|
|
40
|
+
if registry is not None:
|
|
41
|
+
for definition in registry.all_definitions:
|
|
42
|
+
package, sep, _ = definition.key.partition(".")
|
|
43
|
+
if sep and modules is not None and modules.get(package) is not None:
|
|
44
|
+
continue
|
|
45
|
+
suggestions[definition.key] = _from_definition(definition)
|
|
46
|
+
|
|
47
|
+
return [suggestions[key] for key in sorted(suggestions)]
|
|
@@ -55,10 +55,10 @@ export function FieldSource({ field, onReset }: { field: FieldMeta; onReset: ()
|
|
|
55
55
|
const showDefault = !isBlank(field.default) && String(field.default) !== String(field.value);
|
|
56
56
|
return (
|
|
57
57
|
// Wraps rather than clips: `SM_FILE_STORAGE_QUOTA_BYTES` has no space to
|
|
58
|
-
// break at,
|
|
59
|
-
// `
|
|
58
|
+
// break at, and as an unbreakable token it would set the column's minimum
|
|
59
|
+
// width; `overflow-wrap:anywhere` lets it break inside the identifier.
|
|
60
60
|
<span
|
|
61
|
-
className="block
|
|
61
|
+
className="block [overflow-wrap:anywhere] font-mono text-[11.5px] leading-snug text-muted-foreground"
|
|
62
62
|
title={
|
|
63
63
|
field.env_readable
|
|
64
64
|
? t(keys.settings.modules.env_var_hint)
|
|
@@ -16,10 +16,11 @@ interface Props {
|
|
|
16
16
|
/**
|
|
17
17
|
* One settings row: label, control, trailing meta.
|
|
18
18
|
*
|
|
19
|
-
* Three columns
|
|
20
|
-
*
|
|
21
|
-
* this value from?" in the same place, so the exceptions stand out
|
|
22
|
-
*
|
|
19
|
+
* Three columns when the editor card has room, because the trailing column is
|
|
20
|
+
* what makes the screen readable at a glance — every row answers "where is
|
|
21
|
+
* this value from?" in the same place, so the exceptions stand out. The tracks
|
|
22
|
+
* follow the card's width (container query), not the viewport: two sidebars
|
|
23
|
+
* can leave a narrow card on a wide screen, and the row stacks instead.
|
|
23
24
|
*/
|
|
24
25
|
export function ModuleFieldRow({
|
|
25
26
|
field,
|
|
@@ -37,15 +38,14 @@ export function ModuleFieldRow({
|
|
|
37
38
|
const showDescription = field.description && field.type !== 'bool';
|
|
38
39
|
|
|
39
40
|
return (
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
<div className="grid gap-3 sm:grid-cols-[minmax(170px,auto)_1fr] lg:grid-cols-[minmax(170px,auto)_1fr_210px] lg:items-center">
|
|
41
|
+
// Query the editor card: two sidebars can leave little space at a wide viewport.
|
|
42
|
+
<div className="grid grid-cols-1 gap-3 @md:grid-cols-[minmax(0,140px)_minmax(180px,1fr)] @xl:grid-cols-[minmax(0,140px)_minmax(180px,1fr)_minmax(0,160px)] @xl:items-center">
|
|
43
43
|
<div className="min-w-0">
|
|
44
|
-
<label htmlFor={id} className="block
|
|
44
|
+
<label htmlFor={id} className="block [overflow-wrap:anywhere] text-[13px] font-medium">
|
|
45
45
|
{field.name}
|
|
46
46
|
</label>
|
|
47
47
|
{showDescription && (
|
|
48
|
-
<p className="mt-0.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
48
|
+
<p className="mt-0.5 [overflow-wrap:anywhere] text-[11.5px] leading-snug text-muted-foreground">
|
|
49
49
|
{field.description}
|
|
50
50
|
</p>
|
|
51
51
|
)}
|
|
@@ -65,7 +65,7 @@ export function ModuleFieldRow({
|
|
|
65
65
|
</fieldset>
|
|
66
66
|
</div>
|
|
67
67
|
|
|
68
|
-
<div className="
|
|
68
|
+
<div className="min-w-0 [overflow-wrap:anywhere] @md:col-span-2 @xl:col-span-1 @xl:justify-self-start">
|
|
69
69
|
<FieldSource field={field} onReset={onReset} />
|
|
70
70
|
</div>
|
|
71
71
|
|
|
@@ -75,7 +75,7 @@ export function ModuleFieldRow({
|
|
|
75
75
|
{error && (
|
|
76
76
|
<p
|
|
77
77
|
data-testid={`field-error-${field.name}`}
|
|
78
|
-
className="
|
|
78
|
+
className="min-w-0 [overflow-wrap:anywhere] text-xs text-destructive @md:col-span-1 @md:col-start-2 @xl:col-span-2"
|
|
79
79
|
>
|
|
80
80
|
{error}
|
|
81
81
|
</p>
|
|
@@ -46,6 +46,7 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
46
46
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
47
47
|
const [busy, setBusy] = useState(false);
|
|
48
48
|
const [saved, setSaved] = useState(false);
|
|
49
|
+
const [savedOverrides, setSavedOverrides] = useState<Record<string, boolean>>({});
|
|
49
50
|
const [formErrors, setFormErrors] = useState<string[]>([]);
|
|
50
51
|
|
|
51
52
|
// Refresh saved values without discarding drafts in other fields (#637).
|
|
@@ -61,6 +62,7 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
61
62
|
}));
|
|
62
63
|
setErrors({});
|
|
63
64
|
setSaved(false);
|
|
65
|
+
setSavedOverrides({});
|
|
64
66
|
}, [initial]);
|
|
65
67
|
|
|
66
68
|
const modifiedFields = useMemo(() => {
|
|
@@ -113,6 +115,14 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
113
115
|
// Keeping the form mounted lets the confirmation remain visible instead
|
|
114
116
|
// of being discarded by an immediate Inertia reload.
|
|
115
117
|
setBuffer((prev) => ({ ...prev, baseline: { ...values } }));
|
|
118
|
+
setSavedOverrides((prev) => ({
|
|
119
|
+
...prev,
|
|
120
|
+
...Object.fromEntries(
|
|
121
|
+
m.fields
|
|
122
|
+
.filter((f) => f.name in changed)
|
|
123
|
+
.map((f) => [f.name, notEqual(changed[f.name], f.default)]),
|
|
124
|
+
),
|
|
125
|
+
}));
|
|
116
126
|
setSaved(true);
|
|
117
127
|
}
|
|
118
128
|
setBusy(false);
|
|
@@ -126,15 +136,32 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
126
136
|
params: { path: { package: m.package, field: name } },
|
|
127
137
|
}),
|
|
128
138
|
);
|
|
139
|
+
setSavedOverrides((prev) => ({ ...prev, [name]: false }));
|
|
129
140
|
setBuffer((prev) => ({
|
|
130
141
|
...prev,
|
|
131
142
|
values: { ...prev.values, [name]: prev.baseline[name] },
|
|
132
143
|
}));
|
|
133
|
-
|
|
144
|
+
// Take the field's live value from the reload itself: when the page never
|
|
145
|
+
// reloaded after a save, the fresh props equal the old ones and the
|
|
146
|
+
// [initial] effect does not fire, leaving the reverted value on screen.
|
|
147
|
+
router.reload({
|
|
148
|
+
only: ['modules'],
|
|
149
|
+
onSuccess: (page) => {
|
|
150
|
+
const modules = (page.props as { modules?: ModuleView[] }).modules ?? [];
|
|
151
|
+
const fresh = modules
|
|
152
|
+
.find((other) => other.package === m.package)
|
|
153
|
+
?.fields.find((f) => f.name === name);
|
|
154
|
+
if (!fresh) return;
|
|
155
|
+
setBuffer((prev) => ({
|
|
156
|
+
values: { ...prev.values, [name]: fresh.value },
|
|
157
|
+
baseline: { ...prev.baseline, [name]: fresh.value },
|
|
158
|
+
}));
|
|
159
|
+
},
|
|
160
|
+
});
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
return (
|
|
137
|
-
<div className="flex flex-1 flex-col gap-4">
|
|
164
|
+
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
|
138
165
|
{/* Outside the card: the heading names the pane, and the deck keeps the
|
|
139
166
|
card for the fields alone. */}
|
|
140
167
|
<header className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
@@ -178,7 +205,7 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
178
205
|
</div>
|
|
179
206
|
)}
|
|
180
207
|
|
|
181
|
-
<Card className="flex flex-1 flex-col gap-4 border-border p-6">
|
|
208
|
+
<Card className="@container flex min-w-0 flex-1 flex-col gap-4 border-border p-6">
|
|
182
209
|
{/* No inner scroller: the card grows and the page scrolls, which is
|
|
183
210
|
one scrollbar for the screen instead of three nested ones. */}
|
|
184
211
|
<div className="flex flex-col gap-4">
|
|
@@ -188,7 +215,7 @@ export function ModuleForm({ module: m, checks = [] }: Props) {
|
|
|
188
215
|
{fields.map((f) => (
|
|
189
216
|
<ModuleFieldRow
|
|
190
217
|
key={f.name}
|
|
191
|
-
field={f}
|
|
218
|
+
field={{ ...f, db_override: savedOverrides[f.name] ?? f.db_override }}
|
|
192
219
|
package={m.package}
|
|
193
220
|
value={values[f.name]}
|
|
194
221
|
modified={modifiedFields.has(f.name)}
|