netbox-super-cli 1.6.0__tar.gz → 1.6.1__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.
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/CHANGELOG.md +30 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/PKG-INFO +1 -1
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/interactive-tui.md +5 -3
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/_version.py +1 -1
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/bulk.py +4 -2
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/forms.py +17 -4
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/bulk_edit_form.py +32 -5
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/edit_form.py +4 -2
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/styles.tcss +4 -1
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/pyproject.toml +1 -1
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_edit_form.py +135 -2
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_edit_form.py +28 -1
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_forms.py +15 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/uv.lock +1 -1
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/docs.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/dependabot.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/pull_request_template.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/bench.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/claude.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/docs.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/e2e.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/lint.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/release.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/skill-convention-review.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/test.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.gitignore +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.pre-commit-config.yaml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.python-version +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/AGENTS.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/CLAUDE.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/LICENSE +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/README.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/caching.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/command-generation.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/http-client.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/overview.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/schema-loading.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/detail.svg +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/filter.svg +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/list.svg +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/picker.svg +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/search.svg +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/adding-bundled-schemas.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/branching.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/development.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/release-process.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/getting-started/concepts.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/getting-started/first-run.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/getting-started/install.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/audit-log.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/ci-and-automation.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/managing-profiles.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/output-formats.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/using-with-ai-agents.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/working-with-plugins.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/writes-and-safety.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/index.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/cli.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/config.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/exit-codes.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/schemas.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/justfile +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/mkdocs.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/__main__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/aliases/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/aliases/resolver.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/auth/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/auth/verify.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/builder/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/builder/build.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cache/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cache/store.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/aliases_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/app.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/cache_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/commands_dump.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/config_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/globals.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/handlers.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/init_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/login_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/profiles_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/registration.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/runtime.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/skill_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/tui_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/apply.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/bulk.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/coercion.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/confirmation.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/input.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/preflight.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/cache_probe.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/callbacks.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/providers.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/loader.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/models.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/saved_searches.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/settings.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/writer.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/audit.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/client.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/errors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/retry.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/model/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/model/command_model.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/_console.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/colors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/csv_.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/errors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/explain.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/flatten.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/headers.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/json_.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/jsonl.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/render.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/table.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/yaml_.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/custom_fields.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/objecttypes.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/params.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/store.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/tags.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/hashing.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/loader.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/models.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/source.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/manifest.yaml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/skill/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/_bindings.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/app.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/catalog.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/columns.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/errors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/filters.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/fk.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/keymap.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/nav.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/relations.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/columns.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/detail.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/filter.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/global_search.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/list.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/picker.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/record_picker.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/saved_search_picker.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/search.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/selection.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/view.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/_modal.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/bulk_diff.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/bulk_summary.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/confirm.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/diff.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/help.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/nav_tree.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/scripts/gen_docs.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/scripts/sync_agents_md.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/skills/netbox-super-cli/SKILL.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/aliases/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/aliases/test_resolver.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/audit/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/audit/test_redaction_full.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/auth/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/auth/test_verify.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/benchmarks/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/benchmarks/test_startup.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_build.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_default_columns.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_redaction_marking.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cache/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cache/test_prune.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cache/test_store.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_aliases_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_app_smoke.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_cache_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_commands_dump.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_completion_protocol_smoke.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_completion_smoke.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_config_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_dynamic_completion.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_explain.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_globals_color.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_handlers.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_init_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_lazy_textual_import.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_login_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_ndjson_input.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_profiles_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_registration.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_respx_integration.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_runtime.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_runtime_color.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_skill_commands.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_stdin_sniffer.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_tui_command.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_writes_respx.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_apply.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_bulk.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_confirmation.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_handler_helpers.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_handlers_audit.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_input.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_preflight.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_loader.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_models.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_saved_search_fallback.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_saved_searches.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_settings_private_dir.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_writer_atomicity.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_writer_dotted_paths.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_writer_roundtrip.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/conftest.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/README.md +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/conftest.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/docker-compose.yml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_auth_error_envelope.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_full_cycle.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_login.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_ndjson.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_preflight_blocks_apply.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/wait_for_netbox.sh +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/profiles/single_profile.yaml +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/auth_401.json +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/circuits_providers_list.json +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/dcim_devices_get.json +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_audit.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_audit_permissions.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_client.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_client_redaction_threading.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_errors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_retry.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/model/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/model/test_command_model.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/model/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_colors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_console.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_csv.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_errors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_errors_aliases.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_errors_color.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_explain.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_explain_color.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_flatten.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_input_error_envelope.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_json.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_jsonl.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_render_dispatch.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_table.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_table_color.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_yaml.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_custom_fields.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_objecttypes.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_params.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_store.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_tags.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_hashing.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_loader.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_models.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_source.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_source_ttl.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/scripts/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/scripts/test_gen_docs.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/scripts/test_sync_agents_md.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/skill/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/skill/test_bundle.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/test_packaging.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/__init__.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_app.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bindings.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_apply.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_diff.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_diff_modal.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_import_isolation.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_summary.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_catalog.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_column_chooser.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_columns.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_detail_screen.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_diff.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_diff_modal.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_errors.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_filter_screen.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_filters.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_fk_resolve.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_global_search.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_help_overlay.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_keymap.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_list_screen.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_picker.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_record_picker.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_relations.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_saved_filter_integration.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_search.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_selection.py +0 -0
- {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_view.py +0 -0
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to netbox-super-cli are tracked here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely. From v1.0.0 onward, releases follow [Semantic Versioning](https://semver.org/) and the version in `pyproject.toml` matches the git tag. Pre-1.0 milestones (Phase 1-5) were pinned by tag while `pyproject.toml` stayed at `0.0.1`.
|
|
4
4
|
|
|
5
|
+
## v1.6.1 — 2026-06-29
|
|
6
|
+
|
|
7
|
+
Patch release. Fixes the TUI bulk-edit form so custom-field edits are visible and
|
|
8
|
+
actually save.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Bulk-edit include toggles were invisible, so edits silently didn't save**
|
|
13
|
+
([#137]). The per-field include switch in the bulk-edit form was clipped to
|
|
14
|
+
zero width by its stylesheet and rendered blank — users couldn't see or flip
|
|
15
|
+
it, so no field was opted in and the apply produced no change with no error.
|
|
16
|
+
The toggle now sizes to fit its slider.
|
|
17
|
+
- **Edit forms showed the raw `custom_fields.<name>` key** ([#137]). The
|
|
18
|
+
single-edit and bulk-edit forms, and the change-preview diff, now show a custom
|
|
19
|
+
field's human label (e.g. "Site Contact") instead of the raw dotted key,
|
|
20
|
+
matching the list-column labels added in v1.6.0.
|
|
21
|
+
- **Opting in a custom-field select with an unresolved choice set could silently
|
|
22
|
+
null the field** ([#137]). A select whose choices couldn't be fetched rendered
|
|
23
|
+
empty; opting it in seeded a null and cleared a value the user never edited.
|
|
24
|
+
Opting in a blank select now contributes no change; only the explicit ∅ button
|
|
25
|
+
nulls.
|
|
26
|
+
- **Bulk-editing a shared custom field could overwrite it on opt-in** ([#137]).
|
|
27
|
+
Custom-field widgets didn't seed the value the selected records share, so a
|
|
28
|
+
shared boolean rendered as `False` and opting it in unchanged flipped every
|
|
29
|
+
record to `False` (and a shared multiselect cleared to empty). Custom-field
|
|
30
|
+
widgets now seed the shared value, so opting a field in without editing it is a
|
|
31
|
+
no-op.
|
|
32
|
+
|
|
33
|
+
[#137]: https://github.com/thomaschristory/netbox-super-cli/issues/137
|
|
34
|
+
|
|
5
35
|
## v1.6.0 — 2026-06-29
|
|
6
36
|
|
|
7
37
|
Minor release. The interactive TUI gets human-readable custom-field columns,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netbox-super-cli
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.1
|
|
4
4
|
Summary: Dynamic NetBox CLI generated from the live OpenAPI schema.
|
|
5
5
|
Project-URL: Homepage, https://github.com/thomaschristory/netbox-super-cli
|
|
6
6
|
Project-URL: Documentation, https://thomaschristory.github.io/netbox-super-cli/
|
|
@@ -144,9 +144,11 @@ endpoint can filter by this one. Switch tabs with <kbd>Tab</kbd> and press
|
|
|
144
144
|
## Bulk editing
|
|
145
145
|
|
|
146
146
|
Select rows on a list with <kbd>v</kbd>/<kbd>Space</kbd>, then press
|
|
147
|
-
<kbd>B</kbd>.
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
<kbd>B</kbd>. Each field is one row: flip its **include toggle** (the switch on
|
|
148
|
+
the left) to opt it into the change, then set its value — a field with the toggle
|
|
149
|
+
off is left untouched. Custom fields appear as individual rows under their human
|
|
150
|
+
label. Fields are **prepopulated** with the value the selected records share, so
|
|
151
|
+
a small tweak doesn't mean retyping.
|
|
150
152
|
|
|
151
153
|
Press <kbd>p</kbd> to **preview** — a per-record diff of exactly what will
|
|
152
154
|
change (records already matching are left untouched). Confirm to apply; a
|
|
@@ -29,6 +29,7 @@ def bulk_diff(
|
|
|
29
29
|
bulk_set: dict[str, object],
|
|
30
30
|
sensitive_paths: tuple[str, ...],
|
|
31
31
|
new_displays: dict[str, str] | None = None,
|
|
32
|
+
field_labels: dict[str, str] | None = None,
|
|
32
33
|
) -> list[RecordChange]:
|
|
33
34
|
"""Compute the per-record patch and diff rows for a uniform bulk ``set``.
|
|
34
35
|
|
|
@@ -36,12 +37,13 @@ def bulk_diff(
|
|
|
36
37
|
record's current value contributes no patch entry and no row for that
|
|
37
38
|
record, so heterogeneous current values yield different changed subsets.
|
|
38
39
|
``new_displays`` maps a field to the human label of its chosen FK value so
|
|
39
|
-
the diff renders the name rather than the id.
|
|
40
|
+
the diff renders the name rather than the id. ``field_labels`` maps a field
|
|
41
|
+
key to its human label so ``custom_fields.<name>`` rows aren't shown raw.
|
|
40
42
|
"""
|
|
41
43
|
changes: list[RecordChange] = []
|
|
42
44
|
for record in selected:
|
|
43
45
|
patch = compute_patch(record, bulk_set)
|
|
44
|
-
rows = diff_rows(record, patch, sensitive_paths, new_displays)
|
|
46
|
+
rows = diff_rows(record, patch, sensitive_paths, new_displays, field_labels)
|
|
45
47
|
changes.append(RecordChange(record_id=record.get("id"), patch=patch, rows=rows))
|
|
46
48
|
return changes
|
|
47
49
|
|
|
@@ -35,6 +35,9 @@ class WidgetSpec(BaseModel):
|
|
|
35
35
|
|
|
36
36
|
kind: WidgetKind
|
|
37
37
|
name: str
|
|
38
|
+
# Human-friendly row label; falls back to ``name`` when blank (regular fields
|
|
39
|
+
# keep their key as the label; custom fields carry their NetBox label here).
|
|
40
|
+
label: str = ""
|
|
38
41
|
choices: tuple[str, ...] = ()
|
|
39
42
|
nullable: bool = False
|
|
40
43
|
sensitive: bool = False
|
|
@@ -83,15 +86,20 @@ def custom_field_widget(cf: CustomFieldDef) -> WidgetSpec:
|
|
|
83
86
|
kind = _CF_KINDS.get(cf.type, "text")
|
|
84
87
|
nullable = not cf.required
|
|
85
88
|
if kind == "select":
|
|
86
|
-
return WidgetSpec(
|
|
89
|
+
return WidgetSpec(
|
|
90
|
+
kind="select", name=name, label=cf.label, choices=cf.choices, nullable=nullable
|
|
91
|
+
)
|
|
87
92
|
if kind == "multi_select":
|
|
88
93
|
return WidgetSpec(
|
|
89
94
|
kind="multi_select",
|
|
90
95
|
name=name,
|
|
96
|
+
label=cf.label,
|
|
91
97
|
options=tuple((c, c) for c in cf.choices),
|
|
92
98
|
nullable=nullable,
|
|
93
99
|
)
|
|
94
|
-
return WidgetSpec(
|
|
100
|
+
return WidgetSpec(
|
|
101
|
+
kind=kind, name=name, label=cf.label, nullable=nullable, is_float=cf.type == "decimal"
|
|
102
|
+
)
|
|
95
103
|
|
|
96
104
|
|
|
97
105
|
def expand_custom_fields(defs: dict[str, CustomFieldDef]) -> list[WidgetSpec]:
|
|
@@ -242,20 +250,25 @@ def diff_rows(
|
|
|
242
250
|
patch: dict[str, object],
|
|
243
251
|
sensitive_paths: tuple[str, ...],
|
|
244
252
|
new_displays: dict[str, str] | None = None,
|
|
253
|
+
field_labels: dict[str, str] | None = None,
|
|
245
254
|
) -> list[DiffRow]:
|
|
246
255
|
"""Render ``patch`` as human-readable old -> new rows for the confirm modal.
|
|
247
256
|
|
|
248
257
|
``new_displays`` overrides a field's rendered *new* value — used for FK
|
|
249
258
|
fields whose staged value is a bare id but whose chosen label is known
|
|
250
259
|
(e.g. picked from the record chooser). The patch still carries the id.
|
|
260
|
+
``field_labels`` overrides a field's *name* — used so a ``custom_fields.<name>``
|
|
261
|
+
key renders as its human label rather than the raw dotted key.
|
|
251
262
|
"""
|
|
252
263
|
overrides = new_displays or {}
|
|
264
|
+
labels = field_labels or {}
|
|
253
265
|
rows: list[DiffRow] = []
|
|
254
266
|
for name, new_value in patch.items():
|
|
267
|
+
field = labels.get(name, name)
|
|
255
268
|
if name in sensitive_paths:
|
|
256
|
-
rows.append(DiffRow(field=
|
|
269
|
+
rows.append(DiffRow(field=field, old_display="****", new_display="****"))
|
|
257
270
|
continue
|
|
258
271
|
old_display = fk_display(original[name]) if name in original else ""
|
|
259
272
|
new_display = overrides.get(name, str(new_value))
|
|
260
|
-
rows.append(DiffRow(field=
|
|
273
|
+
rows.append(DiffRow(field=field, old_display=old_display, new_display=new_display))
|
|
261
274
|
return rows
|
|
@@ -100,9 +100,15 @@ class BulkEditForm(Screen[None]):
|
|
|
100
100
|
self._fk_labels: dict[str, str] = {}
|
|
101
101
|
body = update_op.request_body
|
|
102
102
|
field_names = list(body.fields) if body is not None else []
|
|
103
|
+
# Custom fields are staged under flattened ``custom_fields.<name>`` keys,
|
|
104
|
+
# so seed their shared value from flattened records too — otherwise a
|
|
105
|
+
# widget defaults (e.g. a boolean to False) and opting it in unchanged
|
|
106
|
+
# would silently overwrite the records' shared value.
|
|
107
|
+
cf_names = [f"custom_fields.{cf.name}" for cf in (custom_field_defs or {}).values()]
|
|
108
|
+
flattened_records = [flatten_custom_fields(record) for record in selected_records]
|
|
103
109
|
# Shared current value per field, to seed the widgets (does NOT opt the
|
|
104
110
|
# field in — the include toggle still gates what gets set).
|
|
105
|
-
self._shared = shared_values(
|
|
111
|
+
self._shared = shared_values(flattened_records, field_names + cf_names)
|
|
106
112
|
self.progress_total = 0
|
|
107
113
|
self.progress_done = 0
|
|
108
114
|
self.title = f"Bulk edit {len(selected_records)} {resource_name}"
|
|
@@ -111,6 +117,10 @@ class BulkEditForm(Screen[None]):
|
|
|
111
117
|
def bulk_set(self) -> dict[str, Any]:
|
|
112
118
|
return {name: self._values[name] for name in self._included if name in self._values}
|
|
113
119
|
|
|
120
|
+
def _field_labels(self) -> dict[str, str]:
|
|
121
|
+
"""Human labels for fields that carry one (custom fields), for the diff."""
|
|
122
|
+
return {name: spec.label for name, spec in self._specs.items() if spec.label}
|
|
123
|
+
|
|
114
124
|
def compose(self) -> ComposeResult:
|
|
115
125
|
yield Header()
|
|
116
126
|
with VerticalScroll(id="bulk-form-body"):
|
|
@@ -142,7 +152,7 @@ class BulkEditForm(Screen[None]):
|
|
|
142
152
|
def _compose_field(self, name: str, spec: WidgetSpec) -> ComposeResult:
|
|
143
153
|
with Horizontal(classes="bulk-field"):
|
|
144
154
|
yield Switch(value=False, id=f"include-{encode_field_id(name)}", classes="bulk-include")
|
|
145
|
-
yield Label(name, classes="bulk-label")
|
|
155
|
+
yield Label(spec.label or name, classes="bulk-label")
|
|
146
156
|
yield from self._compose_widget(name, spec)
|
|
147
157
|
if spec.nullable and spec.kind != "multi_select":
|
|
148
158
|
yield Button("∅", id=f"setnull-{encode_field_id(name)}", classes="bulk-setnull")
|
|
@@ -191,8 +201,16 @@ class BulkEditForm(Screen[None]):
|
|
|
191
201
|
def _compose_widget(self, name: str, spec: WidgetSpec) -> ComposeResult:
|
|
192
202
|
wid = f"field-{encode_field_id(name)}"
|
|
193
203
|
if spec.kind == "multi_select":
|
|
204
|
+
# Tags seed via spec.selected; a custom-field multiselect seeds its
|
|
205
|
+
# shared current list so opting it in unchanged isn't a destructive
|
|
206
|
+
# clear (tags are intentionally left blank — they have their own flow).
|
|
207
|
+
selected = set(spec.selected)
|
|
208
|
+
if not selected and name != "tags":
|
|
209
|
+
shared = self._shared.get(name)
|
|
210
|
+
if isinstance(shared, list):
|
|
211
|
+
selected = {str(v) for v in shared}
|
|
194
212
|
yield SelectionList[str](
|
|
195
|
-
*(Selection(label, val, val in
|
|
213
|
+
*(Selection(label, val, val in selected) for label, val in spec.options),
|
|
196
214
|
id=wid,
|
|
197
215
|
classes="bulk-multiselect",
|
|
198
216
|
)
|
|
@@ -264,7 +282,10 @@ class BulkEditForm(Screen[None]):
|
|
|
264
282
|
return self._multiselect_value(name, self.query_one(wid, SelectionList).selected)
|
|
265
283
|
if spec is not None and spec.kind == "select":
|
|
266
284
|
value = self.query_one(wid, Select).value
|
|
267
|
-
|
|
285
|
+
# A blank select (no/unresolved choices, or a current value not in the
|
|
286
|
+
# options) must not seed a null on opt-in — that would silently clear a
|
|
287
|
+
# field the user never edited. Only the explicit ∅ button nulls.
|
|
288
|
+
return _NO_SEED if value is Select.NULL else value
|
|
268
289
|
if spec is not None and spec.kind == "switch":
|
|
269
290
|
return self.query_one(wid, Switch).value
|
|
270
291
|
return self._coerce_input(name, self.query_one(wid, Input).value)
|
|
@@ -279,6 +300,10 @@ class BulkEditForm(Screen[None]):
|
|
|
279
300
|
name = self._strip(event.select.id, "field-")
|
|
280
301
|
if name is None:
|
|
281
302
|
return
|
|
303
|
+
# A blank select on a field the user hasn't opted in must not stage a null
|
|
304
|
+
# (which would clear the field on apply). Only the explicit ∅ button nulls.
|
|
305
|
+
if event.value is Select.NULL and name not in self._included:
|
|
306
|
+
return
|
|
282
307
|
self._values[name] = None if event.value is Select.NULL else event.value
|
|
283
308
|
|
|
284
309
|
def on_selection_list_selected_changed(self, event: SelectionList.SelectedChanged[str]) -> None:
|
|
@@ -329,7 +354,9 @@ class BulkEditForm(Screen[None]):
|
|
|
329
354
|
sensitive = body.sensitive_paths if body is not None else ()
|
|
330
355
|
# Flatten custom_fields so each chosen custom_fields.<name> diffs per record.
|
|
331
356
|
flattened = [flatten_custom_fields(record) for record in self._selected]
|
|
332
|
-
changes = bulk_diff(
|
|
357
|
+
changes = bulk_diff(
|
|
358
|
+
flattened, self.bulk_set, sensitive, self._fk_labels, self._field_labels()
|
|
359
|
+
)
|
|
333
360
|
|
|
334
361
|
def _on_confirm(confirmed: bool | None) -> None:
|
|
335
362
|
if confirmed:
|
|
@@ -132,7 +132,7 @@ class EditForm(Screen[None]):
|
|
|
132
132
|
def _compose_field(self, name: str, spec: WidgetSpec) -> ComposeResult:
|
|
133
133
|
value = _record_value(self._record, name)
|
|
134
134
|
with Horizontal(classes="edit-field"):
|
|
135
|
-
yield Label(name, classes="edit-label")
|
|
135
|
+
yield Label(spec.label or name, classes="edit-label")
|
|
136
136
|
yield from self._compose_widget(name, spec, value)
|
|
137
137
|
if spec.nullable and spec.kind != "multi_select":
|
|
138
138
|
yield Button("∅", id=f"setnull-{encode_field_id(name)}", classes="edit-setnull")
|
|
@@ -292,8 +292,10 @@ class EditForm(Screen[None]):
|
|
|
292
292
|
if confirmed:
|
|
293
293
|
self._apply_patch(patch, sensitive)
|
|
294
294
|
|
|
295
|
+
labels = {name: spec.label for name, spec in self._specs.items() if spec.label}
|
|
295
296
|
self.app.push_screen(
|
|
296
|
-
DiffModal(diff_rows(self._record, patch, sensitive, self._fk_labels)),
|
|
297
|
+
DiffModal(diff_rows(self._record, patch, sensitive, self._fk_labels, labels)),
|
|
298
|
+
_on_confirm,
|
|
297
299
|
)
|
|
298
300
|
|
|
299
301
|
def _apply_patch(self, patch: dict[str, Any], sensitive_paths: tuple[str, ...]) -> None:
|
|
@@ -68,7 +68,10 @@ BulkEditForm #bulk-form-body {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.bulk-include {
|
|
71
|
-
|
|
71
|
+
/* A Switch needs room for its border + padding + slider; a too-narrow
|
|
72
|
+
width clips the slider to zero so the toggle becomes invisible. */
|
|
73
|
+
width: auto;
|
|
74
|
+
padding: 0 1;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
.bulk-label {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
6
7
|
from textual.app import App, ComposeResult
|
|
7
|
-
from textual.widgets import Button, Input, ListView, Select, SelectionList, Static, Switch
|
|
8
|
+
from textual.widgets import Button, Input, Label, ListView, Select, SelectionList, Static, Switch
|
|
8
9
|
|
|
9
10
|
from nsc.http.errors import NetBoxAPIError
|
|
10
11
|
from nsc.model.command_model import (
|
|
@@ -18,7 +19,8 @@ from nsc.model.command_model import (
|
|
|
18
19
|
)
|
|
19
20
|
from nsc.savedfilters.custom_fields import CustomFieldDef
|
|
20
21
|
from nsc.savedfilters.tags import TagDef
|
|
21
|
-
from nsc.tui.
|
|
22
|
+
from nsc.tui.bulk import bulk_diff
|
|
23
|
+
from nsc.tui.forms import SET_NULL, flatten_custom_fields, tags_payload
|
|
22
24
|
from nsc.tui.screens.bulk_edit_form import BulkEditForm
|
|
23
25
|
from nsc.tui.screens.list import ListScreen
|
|
24
26
|
from nsc.tui.screens.record_picker import RecordPicker
|
|
@@ -886,6 +888,37 @@ def _cf_screen(app: _CfBulkApp) -> BulkEditForm:
|
|
|
886
888
|
return screen
|
|
887
889
|
|
|
888
890
|
|
|
891
|
+
class _StyledCfBulkApp(_CfBulkApp):
|
|
892
|
+
"""Same form, but with the real stylesheet loaded so layout is exercised."""
|
|
893
|
+
|
|
894
|
+
CSS_PATH = str(Path(__file__).resolve().parents[2] / "nsc" / "tui" / "styles.tcss")
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
@pytest.mark.asyncio
|
|
898
|
+
async def test_include_toggle_is_not_clipped_to_zero_width() -> None:
|
|
899
|
+
# Regression: a too-narrow `.bulk-include` clipped the Switch slider to zero,
|
|
900
|
+
# making every include toggle invisible so fields could never be opted in.
|
|
901
|
+
app = _StyledCfBulkApp(_SpyClient([]))
|
|
902
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
903
|
+
await pilot.pause()
|
|
904
|
+
screen = _cf_screen(app)
|
|
905
|
+
for sw in screen.query(".bulk-include").results(Switch):
|
|
906
|
+
assert sw.content_region.width > 0, f"{sw.id} slider clipped to zero width"
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
@pytest.mark.asyncio
|
|
910
|
+
async def test_custom_field_rows_show_human_labels_not_raw_keys() -> None:
|
|
911
|
+
app = _CfBulkApp(_SpyClient([]))
|
|
912
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
913
|
+
await pilot.pause()
|
|
914
|
+
screen = _cf_screen(app)
|
|
915
|
+
labels = {w.render().plain for w in screen.query(".bulk-label").results(Label)}
|
|
916
|
+
assert "Tier" in labels
|
|
917
|
+
assert "Count" in labels
|
|
918
|
+
assert "custom_fields.tier" not in labels
|
|
919
|
+
assert "custom_fields.count" not in labels
|
|
920
|
+
|
|
921
|
+
|
|
889
922
|
@pytest.mark.asyncio
|
|
890
923
|
async def test_custom_fields_expand_into_per_field_widgets_with_toggles() -> None:
|
|
891
924
|
app = _CfBulkApp(_SpyClient([]))
|
|
@@ -908,6 +941,106 @@ async def test_tags_render_as_selection_list() -> None:
|
|
|
908
941
|
assert isinstance(screen.query_one("#field-tags"), SelectionList)
|
|
909
942
|
|
|
910
943
|
|
|
944
|
+
@pytest.mark.asyncio
|
|
945
|
+
async def test_bulk_diff_preview_shows_custom_field_label_not_raw_key() -> None:
|
|
946
|
+
app = _CfBulkApp(_SpyClient([]))
|
|
947
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
948
|
+
await pilot.pause()
|
|
949
|
+
screen = _cf_screen(app)
|
|
950
|
+
screen._included.add("custom_fields.tier")
|
|
951
|
+
screen._values["custom_fields.tier"] = "gold"
|
|
952
|
+
screen.action_preview()
|
|
953
|
+
await pilot.pause()
|
|
954
|
+
modal = app.screen
|
|
955
|
+
assert isinstance(modal, BulkDiffModal)
|
|
956
|
+
text = modal.render_text()
|
|
957
|
+
assert "Tier:" in text
|
|
958
|
+
assert "custom_fields.tier" not in text
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@pytest.mark.asyncio
|
|
962
|
+
async def test_custom_field_ui_edit_saves_end_to_end() -> None:
|
|
963
|
+
"""Guard: a UI edit + opt-in of a custom field sends the nested PATCH."""
|
|
964
|
+
client = _SpyClient([])
|
|
965
|
+
app = _CfBulkApp(client)
|
|
966
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
967
|
+
await pilot.pause()
|
|
968
|
+
screen = _cf_screen(app)
|
|
969
|
+
screen.query_one("#field-custom_fields-tier", Select).value = "gold"
|
|
970
|
+
await pilot.pause()
|
|
971
|
+
await pilot.click("#include-custom_fields-tier")
|
|
972
|
+
await pilot.pause()
|
|
973
|
+
screen.action_preview()
|
|
974
|
+
await pilot.pause()
|
|
975
|
+
await pilot.press("enter")
|
|
976
|
+
await app.workers.wait_for_complete()
|
|
977
|
+
await pilot.pause()
|
|
978
|
+
assert client.patch_calls
|
|
979
|
+
for call in client.patch_calls:
|
|
980
|
+
assert call["json"] == {"custom_fields": {"tier": "gold"}}
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
@pytest.mark.asyncio
|
|
984
|
+
async def test_opting_in_shared_boolean_cf_without_editing_is_a_noop() -> None:
|
|
985
|
+
# Records share custom_fields.flag == True. The widget must seed that shared
|
|
986
|
+
# value so opting it in (no edit) does NOT silently flip every record to False.
|
|
987
|
+
defs = {"flag": CustomFieldDef("flag", "Flag", type="boolean")}
|
|
988
|
+
sel = [
|
|
989
|
+
{"id": 1, "name": "a", "custom_fields": {"flag": True}, "tags": []},
|
|
990
|
+
{"id": 2, "name": "b", "custom_fields": {"flag": True}, "tags": []},
|
|
991
|
+
]
|
|
992
|
+
app = _CfBulkApp(_SpyClient([]), defs=defs, tags=None, selected=sel)
|
|
993
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
994
|
+
await pilot.pause()
|
|
995
|
+
screen = _cf_screen(app)
|
|
996
|
+
assert screen.query_one("#field-custom_fields-flag", Switch).value is True
|
|
997
|
+
screen.query_one("#include-custom_fields-flag", Switch).value = True
|
|
998
|
+
await pilot.pause()
|
|
999
|
+
# Opted in but unchanged: stages the shared True, so no per-record patch.
|
|
1000
|
+
flattened = [flatten_custom_fields(r) for r in sel]
|
|
1001
|
+
changes = bulk_diff(flattened, screen.bulk_set, ())
|
|
1002
|
+
assert all(not c.patch for c in changes)
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
@pytest.mark.asyncio
|
|
1006
|
+
async def test_opting_in_shared_multiselect_cf_without_editing_is_a_noop() -> None:
|
|
1007
|
+
# Records share a multiselect custom field; opting it in unchanged must not
|
|
1008
|
+
# clear it to an empty list.
|
|
1009
|
+
defs = {"envs": CustomFieldDef("envs", "Envs", type="multiselect", choices=("dev", "qa"))}
|
|
1010
|
+
sel = [
|
|
1011
|
+
{"id": 1, "name": "a", "custom_fields": {"envs": ["dev", "qa"]}, "tags": []},
|
|
1012
|
+
{"id": 2, "name": "b", "custom_fields": {"envs": ["dev", "qa"]}, "tags": []},
|
|
1013
|
+
]
|
|
1014
|
+
app = _CfBulkApp(_SpyClient([]), defs=defs, tags=None, selected=sel)
|
|
1015
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
1016
|
+
await pilot.pause()
|
|
1017
|
+
screen = _cf_screen(app)
|
|
1018
|
+
screen.query_one("#include-custom_fields-envs", Switch).value = True
|
|
1019
|
+
await pilot.pause()
|
|
1020
|
+
flattened = [flatten_custom_fields(r) for r in sel]
|
|
1021
|
+
changes = bulk_diff(flattened, screen.bulk_set, ())
|
|
1022
|
+
assert all(not c.patch for c in changes)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
@pytest.mark.asyncio
|
|
1026
|
+
async def test_opting_in_blank_select_does_not_null_the_field() -> None:
|
|
1027
|
+
# A select whose choice-set didn't resolve renders empty (value == NULL).
|
|
1028
|
+
# Opting it in must NOT silently null a value the user couldn't even pick;
|
|
1029
|
+
# only the explicit ∅ button should null. So it contributes no patch.
|
|
1030
|
+
defs = {"region": CustomFieldDef("region", "Region", type="select", choices=())}
|
|
1031
|
+
sel = [
|
|
1032
|
+
{"id": 1, "name": "a", "custom_fields": {"region": "eu"}, "tags": []},
|
|
1033
|
+
{"id": 2, "name": "b", "custom_fields": {"region": "eu"}, "tags": []},
|
|
1034
|
+
]
|
|
1035
|
+
app = _CfBulkApp(_SpyClient([]), defs=defs, tags=None, selected=sel)
|
|
1036
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
1037
|
+
await pilot.pause()
|
|
1038
|
+
screen = _cf_screen(app)
|
|
1039
|
+
screen.query_one("#include-custom_fields-region", Switch).value = True
|
|
1040
|
+
await pilot.pause()
|
|
1041
|
+
assert "custom_fields.region" not in screen.bulk_set
|
|
1042
|
+
|
|
1043
|
+
|
|
911
1044
|
@pytest.mark.asyncio
|
|
912
1045
|
async def test_bulk_apply_nests_custom_field_value() -> None:
|
|
913
1046
|
client = _SpyClient([])
|
|
@@ -4,7 +4,7 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
from textual.app import App, ComposeResult
|
|
7
|
-
from textual.widgets import Button, Input, ListView, Select, SelectionList, Static, Switch
|
|
7
|
+
from textual.widgets import Button, Input, Label, ListView, Select, SelectionList, Static, Switch
|
|
8
8
|
|
|
9
9
|
from nsc.http.errors import NetBoxAPIError
|
|
10
10
|
from nsc.model.command_model import (
|
|
@@ -684,6 +684,33 @@ async def test_edit_expands_custom_field_widget() -> None:
|
|
|
684
684
|
assert isinstance(screen.query_one("#field-tags"), SelectionList)
|
|
685
685
|
|
|
686
686
|
|
|
687
|
+
@pytest.mark.asyncio
|
|
688
|
+
async def test_edit_custom_field_row_shows_human_label_not_raw_key() -> None:
|
|
689
|
+
record = {"id": 5, "name": "sw1", "custom_fields": {"tier": "silver"}, "tags": []}
|
|
690
|
+
app = _CfEditApp(_SpyClient([]), record)
|
|
691
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
692
|
+
await pilot.pause()
|
|
693
|
+
screen = _cf_edit_screen(app)
|
|
694
|
+
labels = {w.render().plain for w in screen.query(".edit-label").results(Label)}
|
|
695
|
+
assert "Tier" in labels
|
|
696
|
+
assert "custom_fields.tier" not in labels
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
@pytest.mark.asyncio
|
|
700
|
+
async def test_edit_diff_modal_shows_custom_field_label_not_raw_key() -> None:
|
|
701
|
+
record = {"id": 5, "name": "sw1", "custom_fields": {"tier": "silver"}, "tags": []}
|
|
702
|
+
app = _CfEditApp(_SpyClient([]), record)
|
|
703
|
+
async with app.run_test(size=(120, 60)) as pilot:
|
|
704
|
+
await pilot.pause()
|
|
705
|
+
screen = _cf_edit_screen(app)
|
|
706
|
+
screen.staged["custom_fields.tier"] = "gold"
|
|
707
|
+
screen.action_save()
|
|
708
|
+
await pilot.pause()
|
|
709
|
+
assert isinstance(app.screen, DiffModal)
|
|
710
|
+
fields = [row.field for row in app.screen._rows]
|
|
711
|
+
assert fields == ["Tier"]
|
|
712
|
+
|
|
713
|
+
|
|
687
714
|
@pytest.mark.asyncio
|
|
688
715
|
async def test_edit_saves_custom_field_nested() -> None:
|
|
689
716
|
client = _SpyClient([])
|
|
@@ -132,6 +132,21 @@ def test_diff_rows_override_absent_falls_back_to_str() -> None:
|
|
|
132
132
|
assert rows == [DiffRow(field="status", old_display="active", new_display="offline")]
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def test_diff_rows_uses_field_label_for_custom_field_key() -> None:
|
|
136
|
+
rows = diff_rows(
|
|
137
|
+
{"custom_fields.tier": "silver"},
|
|
138
|
+
{"custom_fields.tier": "gold"},
|
|
139
|
+
(),
|
|
140
|
+
field_labels={"custom_fields.tier": "Tier"},
|
|
141
|
+
)
|
|
142
|
+
assert rows == [DiffRow(field="Tier", old_display="silver", new_display="gold")]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_diff_rows_field_label_absent_falls_back_to_raw_key() -> None:
|
|
146
|
+
rows = diff_rows({"status": "active"}, {"status": "offline"}, (), field_labels={})
|
|
147
|
+
assert rows == [DiffRow(field="status", old_display="active", new_display="offline")]
|
|
148
|
+
|
|
149
|
+
|
|
135
150
|
# --- #134: custom-field expansion, tags multi-select, payload nesting ---
|
|
136
151
|
|
|
137
152
|
from nsc.savedfilters.custom_fields import CustomFieldDef # noqa: E402
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/feature_request.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/skill-convention-review.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/adding-bundled-schemas.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|