Releases: BeamLabEU/phoenix_kit_document_creator
Releases · BeamLabEU/phoenix_kit_document_creator
0.2.10 - 2026-05-05
0.2.10 - 2026-05-05
Added
- Per-template
:languagefield — admins tag each template with a locale so parent apps can fill template variables in the matching language regardless of the admin's UI locale. Stored as a full BCP-47 code (e.g."en-US","et-EE","ja") — whatPhoenixKit.Modules.Languages.get_enabled_languages/0returns; consumers wanting bare base codes can derive viaDialectMapper.dialect_to_base/1. Documents intentionally don't store a language; they inherit fromtemplate_uuid → templates.languageat fill time. Requires phoenix_kit core ≥ 1.7.105 (V110 column). Documents.update_template_language/3— set/clear a template's locale bygoogle_doc_id. Passnilor""to clear; otherwise a full locale code. Logstemplate.language_updatedwithlanguage_from/language_tometadata; broadcasts:files_changedso connected admin LiveViews resync. Failure-side audit row written on:not_foundso the activity feed reflects the attempt.Documents.list_enabled_languages/0— returns[%{code, name}]sorted by configured position, or[]whenPhoenixKit.Modules.Languagesis disabled or unreachable. Safe to call from LiveView mount — failure swallowed via narrow rescue clauses.Documents.create_template/2:languageopt — defaults to the project's primary language fromPhoenixKit.Modules.Languages.get_default_language/0; passnilto leave unset; pass an explicit code to override. Lookup is guarded withrescue+catch :exitso a disabled Languages module never crashes template creation.Schemas.Template.language_changeset/2— focused single-field changeset honouringvalidate_length(:language, max: 10). Used by both the create-time language stamp (Documents.create_template/2) and the post-create updater (Documents.update_template_language/3) so both write paths produce a clean{:error, %Ecto.Changeset{}}on oversized codes instead of a Postgrexvalue too longexception.- Per-card popover language picker on the templates LV (card + list views). Native HTML
popoverAPI + CSS Anchor Positioning so the menu escapes the card'soverflow: hiddenclipping container without bespoke JS. Gated on templates tab + non-trash status + Languages module enabled. Documents tab and trash view do not render the picker. Web.Helpersmodule —actor_opts/1andactor_uuid/1lifted out of duplicated LV-private helpers. Canonical home for future LV cross-cutting helpers.AGENTS.md"Per-template locale" subsection underPublic API Layersdocumenting the V110 schema, the new opts, and the doc-vs-template inheritance rule.
Changed
- Both admin LiveViews cut over to mount→handle_info. Disconnected mount returns an empty shell with no DB / Settings / Integrations calls; the connected mount subscribes to PubSub (BEFORE the read, closing the broadcast-arrives-between-read-and-subscribe window) then triggers
:load_initial/:load_settingsto do the file-list reads and the initial Drive sync. Pre-fix the four-call burst per page load (folder_config + active_integration_uuid + list_connections + get_integration + connected? on the settings side; list_*_from_db ×4 + load_cached_thumbnails on the main LV) ran twice per session. discover_folders/0swapped from bareTask.async/1×4 +Task.await_many/2toTask.Supervisor.async_stream_nolink(PhoenixKit.TaskSupervisor, ...). Caller-LV exit now lets the supervisor clean the children automatically; per-task failure surfaces as{:exit, reason}in the stream so the explicitcatch :exit, _block is gone.verify_known_file/2is O(1) viaMapSet.member?/2on aknown_file_idsassign rebuilt onmount+:sync_complete. Replaces the prior 4×Enum.any?/2shape (O(N) per event) that was noticeable on folders with thousands of files.- Symmetric boot vs lazy legacy-migration paths. Removed the lazy on-read path's silent "any connected row of this provider" fallback that picked between multi-account installs (a user with
google:workANDgoogle:personalwho had"google"in settings would have one of those chosen arbitrarily). Both paths now require an exactprovider:namematch; on no match the setting is cleared and the admin sees a clean "not configured" prompt. Both paths log a warning + activity row on failure so the audit trail covers both outcomes. already_migrated?/0prefersIntegrations.find_uuid_by_provider_name/1(core 1.7.105+) via afunction_exported?/3runtime guard +apply/3(the apply/3 dodges the compile-time "undefined function" warning on older cores). Falls back to the legacyprovider:namelookup. The fallback can be deleted once~> 1.7.105is the floor inmix.exs.Test.StubIntegrationsclaim/release ownership. Concurrent calls from different live pids raise loudly with:concurrent_stub_useinstead of silently racing the named ETS table. Test files using the stub MUST declareasync: false. The named ETS table stays (cross-process LV→test boundary requires it) butclaim!/0enforces async-false at runtime.Documents.create_template/2's create-time language stamp now routes throughTemplate.language_changeset/2instead ofupdate_all— the V110max: 10validation runs on the create path the same way it runs onupdate_template_language/3. Invalid language is logged and swallowed since the Drive doc is already created at that point; the user can still recover via the post-create picker.- LV
set_template_languageevent patches the:templatesassign in place via a smallpatch_template_language/3helper instead of re-reading the entirelist_templates_from_db/0per click. The self-broadcast is filtered out, so without the in-place patch the badge would lag until the next sync. - Test-helper migration cutover. Per
dev_docs/migration_cleanup.md,test/test_helper.exswas on the known-buggyEcto.Migrator.run([{0, PhoenixKit.Migration}], :up, all: true)pattern that silently stopped re-applying once0was recorded inschema_migrations. Swapped toPhoenixKit.Migration.ensure_current/2(core 1.7.105+) which passes a fresh wall-clock version to Ecto.Migrator on every boot.
Fixed
- M1 (PR #11 follow-up):
mount/3in both LVs no longer queries Settings / Integrations / DB — work moved to ahandle_infoso the disconnected mount is a fast empty shell and the read-bursts run once per session, not twice. - M2 (PR #11 follow-up):
Test.StubIntegrationscross-process safety — concurrent stub use across test pids now raises rather than silently racing. - S2 / S3 / S5 (PR #11 follow-up): Task supervision, O(1) known-file lookup, helpers extraction (see Changed).
- §1.2 / §1.3 (PR #12 follow-up): boot-vs-lazy fallback symmetry; uuid-strict
already_migrated?/0(see Changed). - Dead
_ = changesetline inupdate_template_language/3's error branch (no-op carried over from an earlier iteration).
Tests
test/schemas/template_test.exs: 6 new tests for the V110:languagefield (cast, base + full codes, nil/empty clearing, validate_length boundary), 4 new tests forlanguage_changeset/2(cast + length + nil + cast-allowlist isolation), and a regression pin thatsync_changeset/2does NOT cast:language.test/integration/documents_test.exs: 9 new tests forDocuments.update_template_language/3— happy path, overwrite, clear (nil + empty string),:not_founderror,{:error, changeset}on length validation, activity-log pinning the from→to metadata on success, the failure-side audit row on:not_found, and a PubSub broadcast assertion.test/phoenix_kit_document_creator/web/documents_live_test.exs: 3 new LV tests —verify_known_filerejects unknown ids, the connected-stateset_template_languageevent threadsactor_uuidthrough to the activity row, the clear-language path captureslanguage_fromcorrectly.test/integration/active_integration_test.exs: updated two pre-existing tests to match the new symmetric §1.2 behavior; added a new test for the "no exact match" failure branch.test/support/stub_integrations.ex:get_integration/1now returns the seeded connection'sdatamap (matches realPhoenixKit.Integrationsresponse shape) when the requested key matches a seeded{provider, name}pair. Closes a pre-existing footgun where the stub's degenerate response short-circuited tests meant to exercise exact-match paths.
Known limitations
- Templates language picker uses CSS Anchor Positioning (
anchor-name/position-anchor/position-area) — Chrome/Edge 125+, Safari 26+, not Firefox as of this release. Firefox renders the popover unanchored at the spec-default position (visibility still gated by[&:not(:popover-open)]:hiddenso it's not a blocker, but the picker is unusable on Firefox until anchor positioning ships there).
0.2.9 - 2026-05-02
Added
PhoenixKitDocumentCreator.migrate_legacy/0— boot-time legacy migration callback covering both kinds of pre-uuid data: (1) the olddocument_creator_google_oauthsettings key with locally-stored OAuth tokens → migrated into aPhoenixKit.Integrationsrow under"google:default"; (2) name-stringgoogle_connectionreferences ("google"/"google:my-name") → rewritten to the matching row's uuid. Idempotent across boots; activity emissions per migration (action: "integration.legacy_migrated"); errors logged but never crash boot. Host apps trigger viaPhoenixKit.ModuleRegistry.run_all_legacy_migrations/0fromApplication.start/2.GoogleDocsClient.active_integration_uuid/0— uuid-shaped read accessor for the active Google integration row. Replaces the oldactive_provider_key/0. Detects legacy values, resolves them to the matching integration's uuid, rewrites the setting in place, and returns the uuid; subsequent reads are direct.GoogleDocsClient.uuid?/1—@doc falseshared regex helper for "is this a uuid-shaped string". Used by both the lazy on-read path and the boot-time sweep.
Changed
- (potentially breaking — module API)
GoogleDocsClient.active_provider_key/0→active_integration_uuid/0. Returns the integration row's uuid (string) ornil, rather than aprovider:nameslug. Settings shape:document_creator_settings.google_connectionis now a uuid, not aprovider:namestring. End-users transparent (auto-migrated on read + at boot); module consumers callingactive_provider_key/0directly need to switch. - Strict-UUID Integrations API.
do_migrate_oauth_credentials/1creates the integration row viaadd_connection/3(the row-birth path) and writes migrated tokens viasave_setup(uuid, ...), replacing the old upsert-by-string-key flow. Newensure_connection/2helper handles:already_existson re-runs by resolving the existing uuid. get_credentials/0,connection_status/0, andauthenticated_request/3gate on uuid presence and return:not_configuredcleanly when nothing's picked.GoogleOAuthSettingsLive.mount/3reads the uuid via the new accessor and handlesnilgracefully.- Cross-version compat:
ensure_connection/2's:already_existsresolve step is gated byfunction_exported?(Integrations, :find_uuid_by_provider_name, 1)— uses the V107 primitive when available, falls back to scanninglist_connections/1on Hex~> 1.7. Themigrate_legacy/0@impl PhoenixKit.Moduleannotation was dropped because the published behaviour doesn't list it; the orchestrator dispatches byfunction_exported?/3regardless. - After a successful credentials migration the legacy
document_creator_google_oauthrow is reset to%{}so plaintextclient_secret/access_token/refresh_tokendon't survive the move to encrypted Integrations storage. Failure to clear is best-effort with a warning log; doesn't roll back the migration. phx-disable-withadded to the three Drive folder-browse buttons inGoogleOAuthSettingsLive(templates / documents / deleted path) — multiple rapid clicks no longer spawn concurrentTask.start_linkcalls.- Lazy-read crash hardening:
find_uuid_for_data/2andrewrite_setting/1inGoogleDocsClient(both run fromactive_integration_uuid/0on every legacy-shape request) nowtry/rescue. Backend / Settings failure logsLogger.warningwith exception type and falls through cleanly instead of crashing the LV. Both rescues excludeException.message/1to avoid leaking provider strings or query bindings embedded in Ecto error structs. - Observability:
resolve_via_list_connections/1andlog_migration_activity/2now log exception type before swallowing — operators investigating "why is the resolver returning:resolver_failed" or "why is my activity feed empty after upgrade" have something to grep. @versionnow derives fromMix.Project.config()[:version]at compile time so the runtime function can't drift from the declared package version.- Test suite migration shim removed.
test_helper.exsnow runsEcto.Migrator.run(TestRepo, [{0, PhoenixKit.Migration}], :up, all: true, log: false)— the same call host apps use in production. The 180-line hand-rolledTest.Migration(creating tables that core already owns:phoenix_kit_settings,phoenix_kit_activities,phoenix_kit_doc_*) is gone. Same pattern asphoenix_kit_ai. - Tests dependent on the strict-UUID
add_connection/3return shape ({:ok, %{uuid: _}}) are tagged@tag :requires_unreleased_coreand excluded by default; opt in viamix test --include requires_unreleased_coreonce the matching core version is published. Standalonemix testagainst Hex~> 1.7now exits clean.
Fixed
mix precommitfailures inherited from the strict-UUID flip —find_uuid_by_provider_name/1(undefined in Hex~> 1.7) caused a hardcall_to_missingdialyzer error;@impl PhoenixKit.Moduleonmigrate_legacy/0warned because the published behaviour doesn't declare the callback. Both addressed viafunction_exported?/3runtime gating;mix precommit(compile + format + credo + dialyzer) now exits clean.
Tests
- New integration coverage in
test/integration/active_integration_test.exs(271 lines):active_integration_uuid/0modern-shape passthrough, legacy"google:name"exact match, bare"google"first-row fallback, unresolvable target → setting cleared;get_credentials/0/connection_status/0/authenticated_request/3:not_configuredgates;migrate_legacy/0combined entry point —{:ok, summary}shape, credentials migration converts OAuth → integration row, credentials short-circuit on existing row, reference sweep rewrites string → uuid, idempotency, legacy oauth key wiped after success. - Test stub additions:
StubIntegrations.list_connections/1andseed_connection!/2(used bymigrate_legacy_connection/1's fallback path);connected!/1now also seeds a sentinel uuid indocument_creator_settings.google_connectionso existing tests that only callconnected!()keep working under the new resolver.
Hex: https://hex.pm/packages/phoenix_kit_document_creator/0.2.9
Docs: https://hexdocs.pm/phoenix_kit_document_creator/0.2.9
0.2.7 - 2026-04-22
Added
GoogleDocsClient.DriveWalkermodule — paginatedlist_files/1/list_folders/1and recursivewalk_tree/2(BFS,pageSize: 1000,nextPageTokenlooping, batched'a' in parents or …queries chunked at 40 IDs per request). Both folder discovery and file listing now costO(ceil(N / 40))Drive calls per BFS level instead ofO(N)sequential list calls.Documents.register_existing_document/2andregister_existing_template/2— DB-only upsert for Drive files the caller has already created (e.g. consumers that organise files intodocuments/order-N/sub-M/). Validatesgoogle_doc_idviavalidate_file_id/1, validatestemplate_uuidviaforeign_key_constraint, usesmaybe_put/3so re-registration without optional fields preserves existing values. Opts::actor_uuid(activity log),:emit_pubsub(defaulttrue).Documents.pubsub_topic/0andDocuments.broadcast_files_changed/0— single source of truth for the"document_creator:files"topic; bulk callers can passemit_pubsub: falseand broadcast once at the end.create_document_from_template/3: new:parent_folder_idand:pathoptions for placing documents in consumer-managed subfolders.foreign_key_constraint(:template_uuid)onDocumentchangeset — invalid template UUIDs now return a changeset error instead of raising.- Catch-all
handle_info/2inGoogleOAuthSettingsLiveto prevent crashes on unexpected messages (Task supervisor signals, stray PubSub traffic).
Changed
sync_from_drive/0recursively walks both managed trees and upserts every Google Doc found (including those nested in subfolders) with its actual parentfolder_idand resolvedpath.classify_by_location/5accepts aMapSetof enumerated folder IDs so files in descendant subfolders stay:publishedinstead of being reclassified as:unfiled.- Reconcile drops the implicit "file must be in managed root" rule — any descendant of a managed folder is treated as
:published. list_folder_files/1andlist_subfolders/1onGoogleDocsClientnow delegate toDriveWalker— full pagination instead of the previous silent 100-item cap.- Narrowed
Documents.default_managed/2rescue from bare_to a targeted set (ArgumentError,KeyError,MatchError,BadMapError,DBConnection.ConnectionError,Postgrex.Error) so futureFunctionClauseError/RuntimeErrorbugs propagate instead of being silently swallowed.
Fixed
- Silent data loss past 100 items in
list_folder_files/1/list_subfolders/1— both now fully paginate. test_helper.exsno longer crashes on module load whenpsqlis missing fromPATH(sandboxes / minimal CI images); degrades to the connect-attempt branch instead.test_helper.exsPubSub supervisor bootstrap now raises on unexpected errors instead of silently ignoring them.
Hex: https://hex.pm/packages/phoenix_kit_document_creator/0.2.7
Docs: https://hexdocs.pm/phoenix_kit_document_creator/0.2.7
0.2.6
Added
- Trash tab in DocumentsLive with Active/Trash status toggle (auto-hidden when empty)
- Restore from trash —
restore_template/2,restore_document/2, andlist_trashed_*_from_db/0 - Pending spinner overlay on cards during async delete/restore (layout-stable)
phx-disable-withon New Template / New Document buttons
Changed
- Sort document/template lists by
inserted_at DESC(workaround; see AGENTS.md TODO fordrive_modified_at) - Remove delete confirmation popup — soft delete is recoverable from Trash
- Refactor delete flow into data-driven
action_spec/2shared with restore
Fixed
- PDF download: anchor now appended to DOM before
.click()(fixes Firefox) - Catch-all
handle_info/2to avoid crashes on unexpected messages
Hex: https://hex.pm/packages/phoenix_kit_document_creator/0.2.6
Docs: https://hexdocs.pm/phoenix_kit_document_creator/0.2.6
0.2.5 - 2026-04-12
Fixed
- Add routing anti-pattern warning to AGENTS.md
0.2.4 - 2026-04-09
- Fix 3 dialyzer errors
- Fix sync error swallowing
- Refactor create_document
- Remove dead code
- Graceful DB insert failure
0.2.3 - 2026-04-06
- Migrate Google OAuth to centralized Integrations system
- Remove duplicate OAuth code
- Declare required_integrations for Google
- Update dependencies