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.
Files changed (334) hide show
  1. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/CHANGELOG.md +30 -0
  2. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/PKG-INFO +1 -1
  3. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/interactive-tui.md +5 -3
  4. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/_version.py +1 -1
  5. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/bulk.py +4 -2
  6. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/forms.py +17 -4
  7. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/bulk_edit_form.py +32 -5
  8. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/edit_form.py +4 -2
  9. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/styles.tcss +4 -1
  10. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/pyproject.toml +1 -1
  11. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_edit_form.py +135 -2
  12. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_edit_form.py +28 -1
  13. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_forms.py +15 -0
  14. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/uv.lock +1 -1
  15. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/docs.yml +0 -0
  18. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  19. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/dependabot.yml +0 -0
  20. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/pull_request_template.md +0 -0
  21. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/bench.yml +0 -0
  22. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/claude.yml +0 -0
  23. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/docs.yml +0 -0
  24. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/e2e.yml +0 -0
  25. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/lint.yml +0 -0
  26. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/release.yml +0 -0
  27. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/skill-convention-review.yml +0 -0
  28. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.github/workflows/test.yml +0 -0
  29. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.gitignore +0 -0
  30. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.pre-commit-config.yaml +0 -0
  31. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/.python-version +0 -0
  32. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/AGENTS.md +0 -0
  33. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/CLAUDE.md +0 -0
  34. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/LICENSE +0 -0
  35. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/README.md +0 -0
  36. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/caching.md +0 -0
  37. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/command-generation.md +0 -0
  38. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/http-client.md +0 -0
  39. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/overview.md +0 -0
  40. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/architecture/schema-loading.md +0 -0
  41. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/detail.svg +0 -0
  42. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/filter.svg +0 -0
  43. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/list.svg +0 -0
  44. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/picker.svg +0 -0
  45. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/assets/tui/search.svg +0 -0
  46. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/adding-bundled-schemas.md +0 -0
  47. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/branching.md +0 -0
  48. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/development.md +0 -0
  49. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/contributing/release-process.md +0 -0
  50. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/getting-started/concepts.md +0 -0
  51. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/getting-started/first-run.md +0 -0
  52. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/getting-started/install.md +0 -0
  53. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/audit-log.md +0 -0
  54. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/ci-and-automation.md +0 -0
  55. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/managing-profiles.md +0 -0
  56. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/output-formats.md +0 -0
  57. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/using-with-ai-agents.md +0 -0
  58. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/working-with-plugins.md +0 -0
  59. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/guides/writes-and-safety.md +0 -0
  60. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/index.md +0 -0
  61. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/cli.md +0 -0
  62. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/config.md +0 -0
  63. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/exit-codes.md +0 -0
  64. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/docs/reference/schemas.md +0 -0
  65. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/justfile +0 -0
  66. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/mkdocs.yml +0 -0
  67. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/__init__.py +0 -0
  68. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/__main__.py +0 -0
  69. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/aliases/__init__.py +0 -0
  70. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/aliases/resolver.py +0 -0
  71. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/auth/__init__.py +0 -0
  72. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/auth/verify.py +0 -0
  73. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/builder/__init__.py +0 -0
  74. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/builder/build.py +0 -0
  75. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cache/__init__.py +0 -0
  76. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cache/store.py +0 -0
  77. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/__init__.py +0 -0
  78. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/aliases_commands.py +0 -0
  79. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/app.py +0 -0
  80. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/cache_commands.py +0 -0
  81. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/commands_dump.py +0 -0
  82. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/config_commands.py +0 -0
  83. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/globals.py +0 -0
  84. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/handlers.py +0 -0
  85. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/init_commands.py +0 -0
  86. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/login_commands.py +0 -0
  87. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/profiles_commands.py +0 -0
  88. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/registration.py +0 -0
  89. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/runtime.py +0 -0
  90. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/skill_commands.py +0 -0
  91. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/tui_commands.py +0 -0
  92. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/__init__.py +0 -0
  93. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/apply.py +0 -0
  94. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/bulk.py +0 -0
  95. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/coercion.py +0 -0
  96. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/confirmation.py +0 -0
  97. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/input.py +0 -0
  98. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/cli/writes/preflight.py +0 -0
  99. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/__init__.py +0 -0
  100. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/cache_probe.py +0 -0
  101. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/callbacks.py +0 -0
  102. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/completion/providers.py +0 -0
  103. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/__init__.py +0 -0
  104. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/loader.py +0 -0
  105. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/models.py +0 -0
  106. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/saved_searches.py +0 -0
  107. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/settings.py +0 -0
  108. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/config/writer.py +0 -0
  109. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/__init__.py +0 -0
  110. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/audit.py +0 -0
  111. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/client.py +0 -0
  112. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/errors.py +0 -0
  113. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/http/retry.py +0 -0
  114. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/model/__init__.py +0 -0
  115. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/model/command_model.py +0 -0
  116. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/__init__.py +0 -0
  117. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/_console.py +0 -0
  118. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/colors.py +0 -0
  119. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/csv_.py +0 -0
  120. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/errors.py +0 -0
  121. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/explain.py +0 -0
  122. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/flatten.py +0 -0
  123. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/headers.py +0 -0
  124. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/json_.py +0 -0
  125. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/jsonl.py +0 -0
  126. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/render.py +0 -0
  127. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/table.py +0 -0
  128. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/output/yaml_.py +0 -0
  129. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/__init__.py +0 -0
  130. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/custom_fields.py +0 -0
  131. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/objecttypes.py +0 -0
  132. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/params.py +0 -0
  133. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/store.py +0 -0
  134. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/savedfilters/tags.py +0 -0
  135. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/__init__.py +0 -0
  136. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/hashing.py +0 -0
  137. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/loader.py +0 -0
  138. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/models.py +0 -0
  139. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schema/source.py +0 -0
  140. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/__init__.py +0 -0
  141. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/__init__.py +0 -0
  142. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/manifest.yaml +0 -0
  143. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
  144. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
  145. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/skill/__init__.py +0 -0
  146. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/__init__.py +0 -0
  147. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/_bindings.py +0 -0
  148. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/app.py +0 -0
  149. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/catalog.py +0 -0
  150. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/columns.py +0 -0
  151. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/errors.py +0 -0
  152. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/filters.py +0 -0
  153. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/fk.py +0 -0
  154. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/keymap.py +0 -0
  155. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/nav.py +0 -0
  156. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/relations.py +0 -0
  157. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/__init__.py +0 -0
  158. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/columns.py +0 -0
  159. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/detail.py +0 -0
  160. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/filter.py +0 -0
  161. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/global_search.py +0 -0
  162. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/list.py +0 -0
  163. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/picker.py +0 -0
  164. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/record_picker.py +0 -0
  165. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/screens/saved_search_picker.py +0 -0
  166. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/search.py +0 -0
  167. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/selection.py +0 -0
  168. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/view.py +0 -0
  169. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/__init__.py +0 -0
  170. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/_modal.py +0 -0
  171. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/bulk_diff.py +0 -0
  172. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/bulk_summary.py +0 -0
  173. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/confirm.py +0 -0
  174. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/diff.py +0 -0
  175. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/help.py +0 -0
  176. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/nsc/tui/widgets/nav_tree.py +0 -0
  177. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/scripts/gen_docs.py +0 -0
  178. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/scripts/sync_agents_md.py +0 -0
  179. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/skills/netbox-super-cli/SKILL.md +0 -0
  180. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/__init__.py +0 -0
  181. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/aliases/__init__.py +0 -0
  182. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/aliases/test_resolver.py +0 -0
  183. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/audit/__init__.py +0 -0
  184. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/audit/test_redaction_full.py +0 -0
  185. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/auth/__init__.py +0 -0
  186. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/auth/test_verify.py +0 -0
  187. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/benchmarks/__init__.py +0 -0
  188. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/benchmarks/test_startup.py +0 -0
  189. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/__init__.py +0 -0
  190. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_build.py +0 -0
  191. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_default_columns.py +0 -0
  192. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_redaction_marking.py +0 -0
  193. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/builder/test_request_body_shape.py +0 -0
  194. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cache/__init__.py +0 -0
  195. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cache/test_prune.py +0 -0
  196. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cache/test_store.py +0 -0
  197. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/__init__.py +0 -0
  198. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_aliases_commands.py +0 -0
  199. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_app_smoke.py +0 -0
  200. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_cache_commands.py +0 -0
  201. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_commands_dump.py +0 -0
  202. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_completion_protocol_smoke.py +0 -0
  203. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_completion_smoke.py +0 -0
  204. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_config_commands.py +0 -0
  205. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_dynamic_completion.py +0 -0
  206. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_explain.py +0 -0
  207. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_globals_color.py +0 -0
  208. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_handlers.py +0 -0
  209. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_init_commands.py +0 -0
  210. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_lazy_textual_import.py +0 -0
  211. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_login_commands.py +0 -0
  212. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
  213. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_ndjson_input.py +0 -0
  214. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_profiles_commands.py +0 -0
  215. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_registration.py +0 -0
  216. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_respx_integration.py +0 -0
  217. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_runtime.py +0 -0
  218. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_runtime_color.py +0 -0
  219. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_skill_commands.py +0 -0
  220. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_stdin_sniffer.py +0 -0
  221. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_tui_command.py +0 -0
  222. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/test_writes_respx.py +0 -0
  223. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/__init__.py +0 -0
  224. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_apply.py +0 -0
  225. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_bulk.py +0 -0
  226. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_confirmation.py +0 -0
  227. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_handler_helpers.py +0 -0
  228. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_handlers_audit.py +0 -0
  229. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_input.py +0 -0
  230. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/cli/writes/test_preflight.py +0 -0
  231. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/__init__.py +0 -0
  232. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_loader.py +0 -0
  233. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_models.py +0 -0
  234. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_saved_search_fallback.py +0 -0
  235. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_saved_searches.py +0 -0
  236. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_settings_private_dir.py +0 -0
  237. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_writer_atomicity.py +0 -0
  238. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_writer_dotted_paths.py +0 -0
  239. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/config/test_writer_roundtrip.py +0 -0
  240. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/conftest.py +0 -0
  241. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/README.md +0 -0
  242. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/__init__.py +0 -0
  243. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/conftest.py +0 -0
  244. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/docker-compose.yml +0 -0
  245. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_audit_redaction.py +0 -0
  246. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_auth_error_envelope.py +0 -0
  247. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
  248. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_full_cycle.py +0 -0
  249. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_login.py +0 -0
  250. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_ndjson.py +0 -0
  251. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_preflight_blocks_apply.py +0 -0
  252. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
  253. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/e2e/wait_for_netbox.sh +0 -0
  254. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/profiles/single_profile.yaml +0 -0
  255. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/auth_401.json +0 -0
  256. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/circuits_providers_list.json +0 -0
  257. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/dcim_devices_get.json +0 -0
  258. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
  259. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
  260. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/__init__.py +0 -0
  261. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_audit.py +0 -0
  262. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_audit_permissions.py +0 -0
  263. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_audit_redaction.py +0 -0
  264. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_client.py +0 -0
  265. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_client_redaction_threading.py +0 -0
  266. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_errors.py +0 -0
  267. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/http/test_retry.py +0 -0
  268. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/model/__init__.py +0 -0
  269. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/model/test_command_model.py +0 -0
  270. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/model/test_request_body_shape.py +0 -0
  271. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/__init__.py +0 -0
  272. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_colors.py +0 -0
  273. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_console.py +0 -0
  274. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_csv.py +0 -0
  275. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_errors.py +0 -0
  276. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_errors_aliases.py +0 -0
  277. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_errors_color.py +0 -0
  278. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_explain.py +0 -0
  279. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_explain_color.py +0 -0
  280. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_flatten.py +0 -0
  281. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_input_error_envelope.py +0 -0
  282. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_json.py +0 -0
  283. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_jsonl.py +0 -0
  284. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_render_dispatch.py +0 -0
  285. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_table.py +0 -0
  286. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_table_color.py +0 -0
  287. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/output/test_yaml.py +0 -0
  288. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/__init__.py +0 -0
  289. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_custom_fields.py +0 -0
  290. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_objecttypes.py +0 -0
  291. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_params.py +0 -0
  292. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_store.py +0 -0
  293. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/savedfilters/test_tags.py +0 -0
  294. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/__init__.py +0 -0
  295. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_hashing.py +0 -0
  296. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_loader.py +0 -0
  297. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_models.py +0 -0
  298. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_source.py +0 -0
  299. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/schema/test_source_ttl.py +0 -0
  300. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/scripts/__init__.py +0 -0
  301. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/scripts/test_gen_docs.py +0 -0
  302. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/scripts/test_sync_agents_md.py +0 -0
  303. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/skill/__init__.py +0 -0
  304. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/skill/test_bundle.py +0 -0
  305. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/test_packaging.py +0 -0
  306. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/__init__.py +0 -0
  307. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_app.py +0 -0
  308. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bindings.py +0 -0
  309. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_apply.py +0 -0
  310. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_diff.py +0 -0
  311. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_diff_modal.py +0 -0
  312. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_import_isolation.py +0 -0
  313. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_bulk_summary.py +0 -0
  314. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_catalog.py +0 -0
  315. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_column_chooser.py +0 -0
  316. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_columns.py +0 -0
  317. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_detail_screen.py +0 -0
  318. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_diff.py +0 -0
  319. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_diff_modal.py +0 -0
  320. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_errors.py +0 -0
  321. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_filter_screen.py +0 -0
  322. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_filters.py +0 -0
  323. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_fk_resolve.py +0 -0
  324. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_global_search.py +0 -0
  325. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_help_overlay.py +0 -0
  326. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_keymap.py +0 -0
  327. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_list_screen.py +0 -0
  328. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_picker.py +0 -0
  329. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_record_picker.py +0 -0
  330. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_relations.py +0 -0
  331. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_saved_filter_integration.py +0 -0
  332. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_search.py +0 -0
  333. {netbox_super_cli-1.6.0 → netbox_super_cli-1.6.1}/tests/tui/test_selection.py +0 -0
  334. {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.0
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>. Choose *which* fields to set (each has an include toggle) and one
148
- value each. Fields are **prepopulated** with the value the selected records
149
- share, so a small tweak doesn't mean retyping.
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
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "1.6.0"
3
+ __version__ = "1.6.1"
@@ -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(kind="select", name=name, choices=cf.choices, nullable=nullable)
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(kind=kind, name=name, nullable=nullable, is_float=cf.type == "decimal")
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=name, old_display="****", new_display="****"))
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=name, old_display=old_display, new_display=new_display))
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(selected_records, field_names)
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 spec.selected) for label, val in spec.options),
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
- return None if value is Select.NULL else value
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(flattened, self.bulk_set, sensitive, self._fk_labels)
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)), _on_confirm
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
- width: 6;
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "netbox-super-cli"
3
- version = "1.6.0"
3
+ version = "1.6.1"
4
4
  description = "Dynamic NetBox CLI generated from the live OpenAPI schema."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -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.forms import SET_NULL, tags_payload
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
@@ -675,7 +675,7 @@ wheels = [
675
675
 
676
676
  [[package]]
677
677
  name = "netbox-super-cli"
678
- version = "1.6.0"
678
+ version = "1.6.1"
679
679
  source = { editable = "." }
680
680
  dependencies = [
681
681
  { name = "click" },