# Plan — #2370 Manual Tables: Per-Row/Column Visibility Toggles

## Repo
/Users/isupercoder/websites/tablecrafter/tc-work
Branch: feature/2370-visibility-toggles
Merge base: b3b2db740f1f6c63c6a2da3fea5d236ef0994d69

## Global Constraints

- TDD mandatory: failing tests first, then implementation.
- No TC_VERSION or version literal changes.
- No function names with parens in PHP comments (test-issue-342 / test-issue-133 guards).
- Admin JS = individual enqueues (not frontend bundle).
- All new settings/payload keys must be posted by save-table.js AND consumed server-side.
- PR only — no direct push to main.
- No AI attribution in commits.
- E2E: REAL interactions; click the exact inner element carrying the binding, not th/td center; no console.log in specs; afterAll cleans up created tables + resetSeededTable().
- Conventional commits: feat(builder): description (#2370).

## Architecture

### Storage
- **Row hidden flag**: stored in `row_json` as `_tc_hidden: true` (boolean).
  - All rows have cell data keyed by column keys; `_tc_hidden` is a reserved metadata key.
  - The `_tc_` prefix ensures no collision with user-defined column keys (sanitize_key strips underscores from start? No — sanitize_key allows leading underscores are stripped, so keys like `_tc_hidden` would become `tc_hidden`. CORRECTION: We store it as `__tc_hidden` or use a non-underscore-prefixed key like `tc_hidden_meta`. Actually sanitize_key keeps underscores but strips leading ones. Decision: store as a sibling key `_hidden` in the row_json (PHP sanitize_key strips the leading underscore making it just `hidden` — but save_manual_rows filters by known column keys, so _hidden would be STRIPPED). FINAL DECISION: Store `_tc_hidden` as a row-level property OUTSIDE the cell-key whitelist by not restricting it to known_keys: add `_tc_hidden` to the allowed keys explicitly in save_manual_rows).
  - Simpler: store hidden as boolean `true`/absent in row_json. The save path whitelists `_tc_hidden` as an extra allowed key.
- **Column hidden flag**: stored in `manual_columns` entry as `hidden: true` (boolean).
  - The `apply_manual_column_labels` merge path in save_manual_rows must preserve `hidden` flag when merging labels.

### JS Model
- `TCManualGridModel` gains:
  - `toggleRowHidden(rowIdx)` — toggles `_tc_hidden` on the row object, marks dirty
  - `isRowHidden(rowIdx)` — returns bool
  - `toggleColumnHidden(colIdx)` — toggles `hidden` on the column object, marks dirty
  - `isColumnHidden(colIdx)` — returns bool
  - `visibleRowCount()` — count of rows without `_tc_hidden: true`
  - `visibleColCount()` — count of columns without `hidden: true`
  - `getRows()` already returns row copies including `_tc_hidden`; no change needed
  - `getColumns()` already returns column copies including `hidden`; no change needed

### JS save-table.js
- The `gt_save_manual_rows` call already posts `manual_columns: JSON.stringify(gridEditor.model.getColumns().map(c => ({key:c.key, label:c.label})))`.
- CHANGE: include `hidden` in the mapped column objects: `{key:c.key, label:c.label, hidden:c.hidden||false}`
- Rows are posted via `gridEditor.model.getRows()` which already includes `_tc_hidden` if set.

### PHP save_manual_rows
- `known_keys` whitelist: add `_tc_hidden` to allowed keys.
- `apply_manual_column_labels` merge: preserve `hidden` boolean from incoming columns (not just label).
- Actually we rename this behavior: the column label merge in save_manual_rows receives `manual_columns` JSON with `{key, label, hidden}` — update the merge loop to also copy `hidden`.

### PHP render_manual_source_table
- After `get_rows()`, filter out rows where `$row['_tc_hidden'] === true`.
- Column filtering: filter `$column_keys` to exclude hidden columns (where the corresponding `manual_columns` entry has `hidden: true`).
- Export path: render_external_source_table_html receives the already-filtered rows/column_keys, so export exclusion is automatic.
- AJAX `get_table_data` path: check if called for manual tables and apply same hidden filter (look at how the AJAX data path works for manual).

### JS Editor UI (grid render)
- Hidden rows: `<tr>` gets class `gt-grid-row-hidden`; row menu shows "Show row" instead of "Hide row".
- Hidden columns: `<th>` and cells get class `gt-grid-col-hidden`; col menu shows "Show column" instead of "Hide column".
- Visual treatment: CSS `opacity:0.4` + a dim overlay or just opacity. Plus an eye-slash marker in the row number cell / column header.
- Row menu: add "Hide row" / "Show row" button (toggle based on current state).
- Column menu: add "Hide column" / "Show column" button (toggle based on current state).
- Guard: when toggling would hide the last visible row or column, show `window.alert('Warning: this would hide all rows/columns, leaving the table empty. Hiding anyway.')` — allowed but warned.

## Task List

### Task 1: Failing vitest tests
File: tests/js/admin-manual-grid-editor-visibility.test.js
Tests (all failing at write time):
- toggleRowHidden(0) sets _tc_hidden true, isDirty true
- toggleRowHidden(0) again clears _tc_hidden
- isRowHidden(0) returns false initially
- isRowHidden(0) returns true after toggle
- visibleRowCount() counts correctly
- toggleColumnHidden(0) sets hidden:true, isDirty true
- isColumnHidden(0) returns false initially
- isColumnHidden(0) returns true after toggle
- visibleColCount() counts correctly
- getRows() includes _tc_hidden key in result
- getColumns() includes hidden key in result

### Task 2: Failing PHPUnit shim tests
File: tests/test-issue-2370-visibility.php
Tests (all failing at write time):
- Row flag round-trip: replace_rows saves row with _tc_hidden; get_rows returns it
- Column hidden flag survives label merge in save_manual_rows (mock the wpdb calls)
- render_manual_source_table excludes hidden rows (mock get_rows returning mix of hidden/visible)
- render_manual_source_table excludes hidden column from column_keys
- hidden column not in rendered HTML headers
- hidden row data not in rendered HTML

### Task 3: PHP implementation
Files to modify:
- includes/class-tc-ajax.php: save_manual_rows — add `_tc_hidden` to allowed keys; update column label merge to preserve `hidden` flag; filter hidden rows/cols in AJAX data path for manual tables
- includes/class-tc-shortcode.php: render_manual_source_table — filter hidden rows and hidden column keys before passing to render_external_source_table_html

### Task 4: JS model implementation
File: assets/js/admin/manual-grid-editor.js
Add to TCManualGridModel:
- toggleRowHidden, isRowHidden, visibleRowCount
- toggleColumnHidden, isColumnHidden, visibleColCount
- Ensure getRows() and getColumns() pass through hidden flags

File: assets/js/admin/save-table.js
Update gt_save_manual_rows call: include `hidden` in manual_columns mapping.
Update getRows() is already included; _tc_hidden will be in row objects automatically.

### Task 5: JS editor UI
File: assets/js/admin/manual-grid-editor.js
- _render(): add `gt-grid-row-hidden` class to hidden rows; add eye-slash marker in row number cell; add `gt-grid-col-hidden` class to hidden col headers and cells.
- Row menu HTML: add "Hide row" / "Show row" button.
- Column menu HTML: add "Hide column" / "Show column" button.
- Add click handlers for hide/show row and hide/show column.
- Last-visible guard: before toggling, check visibleRowCount()/visibleColCount(); if 1, alert warning but proceed.

File: assets/css/admin/manual-grid.css (or wherever grid CSS lives)
- Add .gt-grid-row-hidden and .gt-grid-col-hidden styles (opacity:0.4 + indicator).

### Task 6: E2E spec
File: tests/e2e/manual-visibility.spec.js
Flow:
1. Login, create manual table with 2 cols × 3 rows, fill some cells.
2. Hide column 1 via column header dropdown ("Hide column").
3. Hide row 1 via row kebab menu ("Hide row").
4. Save table (primary save + gt_save_manual_rows).
5. Navigate to frontend embed page → verify hidden col/row absent.
6. Return to builder → verify hidden col/row still visible (dimmed).
7. Unhide via menu ("Show column", "Show row").
8. Save → frontend shows both again.
9. afterAll: delete created table + resetSeededTable().
