feature/image-columns: composed-doc data persistence, Google Docs multi-column helpers, unified Active/Trash tabs#20
Conversation
Composer now writes a caller-supplied :data opt into the Document
changeset, and schema_to_file_map/1 exposes the data field. This lets
the host app store the template/image selection ("recipe") that
produced a document so it can be re-created later.
Replace boolean categories_trash/types_trash with string status_mode assigns (active/trashed); replace toggle_categories_trash and toggle_types_trash with a single switch_status event using phx-value-target and phx-value-mode, matching DocumentsLive. Extract status_subtabs/1 component shared by Categories and Types columns; render in the DocumentsLive visual style (border-b-2 with border-primary for Active and border-error for Trash). Auto-hide the entire sub-tab row when the Trash list is empty and the current mode is active. Track trashed_categories_count and trashed_types_count, reusing the loaded list as count when viewing trash to avoid a second query.
timujinne
left a comment
There was a problem hiding this comment.
Verdict
Comment — minor issues only, no blockers.
Stage 1 — Spec compliance
Commit 1: 1643a97 — Persist composed-document creation settings in Document.data
composer.ex: reads:datafrom opts with%{}default, passes it intoDocument.changeset. Verified.documents.ex:schema_to_file_map/1now includes:data. Verified.composed_document_test.exs: two new tests covering presence and absence of:data. Verified.- No silent unrelated changes. PASS.
Commit 2: ee0b312 — Add page content width and column width helpers
content_width_pt/1andimage_width_for_columns/2added with docs. Verified.- Private
magnitude/1helper added. Verified. - Module attributes
@image_gap_pt,@default_content_width_pt,@max_columnsadded. Verified. google_docs_client_width_test.exscovers normal, fallback, and clamp cases. Verified.- No silent unrelated changes. PASS.
Commit 3: 5d13816 — Add table-based image insertion helpers for multi-column slots
table_image_inserts/3(Phase A) andfill_table_cells/3(Phase B) added. Verified.google_docs_client_table_test.exscovers both phases. Verified.- No silent unrelated changes. PASS.
Commit 4: 064c0d2 — Unify Active/Trash sub-tabs on Categories page with Documents style
- Boolean assigns replaced with string mode assigns. Verified.
toggle_categories_trash/toggle_types_trashreplaced byswitch_statuswithphx-value-targetandphx-value-mode. Verified.status_subtabs/1extracted as a shared component. Verified.- Auto-hide when trash is empty implemented via
:if={@trashed_count > 0 or @status_mode == "trashed"}. Verified. trashed_categories_count/trashed_types_counttracked. Verified.- No silent unrelated changes. PASS.
Commit 5: 91605f2 — Regenerate _phoenix_kit_sources.css
- Single-line change: empty line replaced with
@sourcedirective. Verified. - Consistent with the "auto-generated" header comment. PASS.
Stage 2 — Code quality
[MINOR] Extra DB query on every reload_categories in active mode
File: lib/phoenix_kit_document_creator/web/categories_live.ex:637-640
When status_mode == "active" (the common case), reload_categories fires a second Taxonomy.list_categories(status: "deleted") query solely to count trashed items. Same pattern in reload_types at lines 669-672 — every reload in active mode doubles the query count.
Suggestion: introduce a Taxonomy.count_categories(status: "deleted") that does Repo.aggregate(query, :count) instead of loading full records to call length/1. Acceptable for now since category/type lists are small, but worth flagging before it becomes ingrained.
[MINOR] table_image_inserts/3 opts accept but ignore content_width_pt
File: lib/phoenix_kit_document_creator/google_docs_client.ex:925
The function takes opts (map), and the test passes %{columns: 2, content_width_pt: 468.0}, but content_width_pt is never used inside the function — it is only used by Phase B (fill_table_cells).
Suggestion: either document that content_width_pt is meaningful only for Phase B, or narrow Phase A's opts to %{columns: _} and drop the unused key from the test.
[MINOR] scale_height/3 returns integer via round/1; callers depend on * 1.0 to coerce to float
File: lib/phoenix_kit_document_creator/google_docs_client.ex:1099-1100
scale_height/3 calls round(target_width * src_height / src_width) which returns an integer, while the Google Docs API magnitude field expects a float. The * 1.0 multiplication in fill_table_cells coerces it back, so it works at runtime, but this is an implicit caller obligation.
Suggestion: drop round/1 (the API accepts fractional points) or use Float.round/2. Pre-existing code, so flagging as a note rather than a request.
[NITPICK] Integer ceiling via float path
File: lib/phoenix_kit_document_creator/google_docs_client.ex:929
(length(media) / cols) |> Float.ceil() |> trunc() routes integer math through a float for the ceil. Idiomatic integer ceil-div is div(length(media) + cols - 1, cols). Style preference only — current code is correct.
[NITPICK] Catch-all handle_event("switch_status", _params, socket) silently drops malformed events
File: lib/phoenix_kit_document_creator/web/categories_live.ex:86-88
If a switch_status event arrives with an unexpected target or mode, it's silently ignored. Fine for production robustness, but a Logger.warning("Unexpected switch_status params: #{inspect(params)}") would help catch template typos during development.
[NOTE] Trash tab now uses border-error instead of border-primary
File: lib/phoenix_kit_document_creator/web/categories_live.ex:479-497
Deliberate UX change to match DocumentsLive — flagged as intentional, not a finding.
Summary
- 0 critical, 0 major, 3 minor, 2 nitpick, 1 note
- All five commits do exactly what their messages claim with no silent side effects.
- Tests cover the new helpers well; LiveView DB-backed tests acknowledged as requiring PostgreSQL and not run locally.
- The duplicate count query in
reload_categories/reload_types(minor) is the most actionable item but is not blocking given current list sizes.
Go/no-go: ship it. None of the minor/nitpick items block merge.
Two related soft-delete UX improvements for Documents/Templates:
1. Deletion metadata is now stamped into the existing JSONB data field
on Document and Template (no migration). On soft-delete,
data["deleted"] = %{"at" => ISO-8601, "by_uuid" => actor_uuid} is
merged via PostgreSQL || operator so existing keys (composer
recipe etc.) are preserved. On restore, data - 'deleted' removes
only the deletion key. A new "Deleted" column appears only on
the trash tab (both list and cards views, Documents and Templates),
showing formatted timestamp + display name resolved via a single
PhoenixKit.Users.Auth.get_users_by_uuids/1 batch lookup.
2. When the underlying Google Drive file is missing (HTTP 404 from
the Drive parents/move endpoint), restore no longer fails with
the generic 'try again' message. do_move_file/2 distinguishes
404 and returns {:error, :drive_file_not_found}, propagated
through restore_document/2 and restore_template/2 to the
LiveView, which renders a yellow dismissible warning:
'File is missing in Google Drive — it cannot be restored. You
can permanently delete this record.'
Coverage:
- 80 existing unit tests still pass.
- New tests in test/errors_test.exs (error message) and
test/integration/drive_bound_actions_test.exs (stamp on delete
with and without actor, preserve other data keys, clear on
restore, Drive 404 on GET-parents and PATCH-move, both Document
and Template paths).
…, empty-uuid short-circuit
- Wrap data column with COALESCE(data, '{}'::jsonb) in both fragments
of stamp_deleted_data/2 and clear_deleted_data/1, so a row with a
NULL data column still gets the deleted metadata written / cleared
correctly. The data column is nullable in the migration.
- Extend restore_document/2 and restore_template/2 tests to include a
sibling 'recipe' key in data alongside 'deleted', and assert it
survives restore — previously only the delete path verified
preservation.
- Skip Auth.get_users_by_uuids/1 when the trashed UUID list is empty
(active tab, or trash tab with no by_uuid stamped rows).
timujinne
left a comment
There was a problem hiding this comment.
Verdict (new commits since previous review)
Comment — no blockers. Two carryovers from the previous review have now been addressed in a follow-up commit; remaining items are minor and one nitpick.
Stage 1 — Spec compliance
Commit 182de63 — Wire columns through orchestrator with two-phase table insertion
- Column-aware
build_image_batch_requests/3added; arity-2 delegates to it with@default_content_width_pt(backward compatible). list_image_insertsdispatches on:columns:>= 2→table_image_inserts(Phase 1),== 1→inline_image_inserts_pt.inline_image_inserts_ptbuildsinsertInlineImagerequests with PT-based width.- Two-phase orchestration in
substitute_all_images: Phase 1 creates tables + inline slots; Phase 2 re-fetches the doc and fills table cells. normalize_columnshandles integer, binary, and nil inputs; clamps to[1, @max_columns].- Test coverage in
google_docs_client_phase_test.exscovers columns=1 inline path, columns≥2 table path, mixed slots, cell extraction, and column normalization pass-through.
PASS.
Commit fc69fa1 — Show deletion metadata in trash view and handle Drive 404 on restore
- JSONB stamp on delete via
stamp_deleted_data/2(data || jsonb_build_object(...)); clear on restore viaclear_deleted_data/1(data - 'deleted'). - "Deleted" column appears only on the trash tab in both list and cards views for Documents and Templates.
- User-name resolution via batch
PhoenixKit.Users.Auth.get_users_by_uuids/1. do_move_file/2distinguishes HTTP 404 and returns{:error, :drive_file_not_found}; propagated cleanly throughrestore_document/2/restore_template/2to the LiveView, which shows a yellow dismissiblealert-warning.:drive_file_not_foundadded toerrors.extype union andmessage/1.- Tests in
errors_test.exsanddrive_bound_actions_test.exscover the error message, stamp on delete (with and without actor, and preserving sibling keys), clear on restore, and Drive 404 for both Document and Template.
PASS.
Commit 6944db2 — Address previous review carryovers
COALESCE(data, '{}'::jsonb)added to both fragments ofstamp_deleted_data/2(Template + Document) and both fragments ofclear_deleted_data/1. Handles legacy rows with NULLdata.- Restore tests for Document and Template now include a sibling
"recipe" => "preserved"key and assert it survives the restore. build_deleted_by_names/1short-circuits to%{}when the extracted UUID list is empty, avoiding an unnecessary call toAuth.get_users_by_uuids/1on the active tab.
PASS.
Stage 2 — Code quality
[MINOR] "Take last K tables" assumption in do_fill_table_cells/4
File: lib/phoenix_kit_document_creator/google_docs_client.ex:1634
Enum.take(all_tables, -k) assumes that newly inserted tables are the last K tables in the document. If the template already contains tables (layout table, data table, etc.), pre-existing tables shift the mapping and the wrong tables may be filled.
Suggestion: capture the count of pre-existing tables before Phase 1 (from doc2) and slice from that offset; or match by start_index against the known insertion points. At minimum, document the "no pre-existing tables" constraint in a moduledoc / function-doc comment.
[NITPICK] Atom vs string key inconsistency in batch requests
File: lib/phoenix_kit_document_creator/google_docs_client.ex:1119-1145
insert_inline_image_request_pt/3 uses atom keys (:insertInlineImage); table_image_inserts/fill_table_cells use string keys ("insertTable", "insertInlineImage"). Both go through the same batch_update/2. Works due to the JSON encoder, but reading the code requires switching mental models. Recommend standardising on one (preferably string keys, matching Google Docs API conventions).
Status of previous-review carryovers
- COALESCE in stamp/clear: fixed in
6944db2. - Restore-preserves-other-keys test: fixed in
6944db2.
Summary
- 0 critical, 0 major, 1 minor, 1 nitpick.
- All previous-review carryovers addressed.
- The remaining minor item (table-take assumption) is an edge case that doesn't affect templates with no pre-existing tables — fine to ship and revisit when needed.
Go/no-go: ship it.
- google_docs_client: make Phase-2 table identification drift-proof.
`match_new_tables/3` matches new tables by document order instead of a
startIndex set-difference, so pre-existing tables located after a
multi-column placeholder no longer trip the count guard and leave grids
empty. Extract `fill_matched_tables/5`. Adds 5 unit tests.
- documents_live: resolve trashed-by display names when the trashed lists
change (load/sync/patch) instead of on every render via assign_files.
- taxonomy: add count_categories/1 and count_types_for_category/2; use them
for the trash badges instead of length(list_*(status: "deleted")).
- documents: stamp/clear data["deleted"] only on the schema that owns the
google_doc_id (derived from folder_key/type), not both tables.
- tests: fix stale assertions that never run in the sandbox (no local DB) —
image_slots_for_template now returns %{name, kind, config}; image
object sizes are emitted in PT, not EMU.
- docs: add dev_docs/pull_requests entries for #20 and #21 with review and
follow-up notes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles five commits accumulated on
feature/image-columns:Persist composed-document creation settings in
Document.data— Composer writes a caller-supplied:dataopt into the Document changeset;schema_to_file_map/1exposes the data field. Lets the host app store the template/image selection ("recipe") that produced a document so it can be re-created later.Page content width and column width helpers in
google_docs_client.ex—content_width_pt/1extracts page width minus margins;image_width_for_columns/2computes per-column image width accounting for inter-column gaps. Covered bygoogle_docs_client_width_test.exs.Table-based image insertion helpers for multi-column slots —
table_image_inserts/3(Phase A: delete placeholder + insertTable) andfill_table_cells/3(Phase B: zip cells with media, reverse for last-first insertion to avoid index drift, build insertInlineImage requests with properobjectSize). Covered bygoogle_docs_client_table_test.exs.Unify Active/Trash sub-tabs on Categories page with Documents style — replaces boolean
categories_trash/types_trashwith stringstatus_modeassigns andswitch_statusevent matchingDocumentsLive. Extractsstatus_subtabs/1shared by Categories and Types columns, rendered in the sameborder-b-2/border-primary/border-errorstyle as Documents. Auto-hides the sub-tab row when Trash is empty and the current mode is active. Newtrashed_categories_count/trashed_types_countassigns are populated by the reload helpers, reusing the loaded list as the count when viewing trash to avoid a second query.Regenerate
_phoenix_kit_sources.csswith@sourcedirective — auto-generated content brought in sync withcss_sources/0callback.mix formatandmix qualityare clean for the touched files. DB-backed CategoriesLive tests not run locally (no PostgreSQL).