diff --git a/assets/css/_phoenix_kit_sources.css b/assets/css/_phoenix_kit_sources.css index bcfaa20..e7ba1f7 100644 --- a/assets/css/_phoenix_kit_sources.css +++ b/assets/css/_phoenix_kit_sources.css @@ -1,3 +1,3 @@ /* Auto-generated by PhoenixKit — do not edit manually. Regenerated on each compilation from css_sources/0 callbacks. */ - +@source "../../deps/phoenix_kit_document_creator"; diff --git a/lib/phoenix_kit_document_creator.ex b/lib/phoenix_kit_document_creator.ex index ef456b7..ec72552 100644 --- a/lib/phoenix_kit_document_creator.ex +++ b/lib/phoenix_kit_document_creator.ex @@ -183,7 +183,7 @@ defmodule PhoenixKitDocumentCreator do label: "Categories", icon: "hero-folder", path: "document-creator/categories", - priority: 647, + priority: 651, level: :admin, permission: module_key(), parent: :admin_document_creator, diff --git a/lib/phoenix_kit_document_creator/documents.ex b/lib/phoenix_kit_document_creator/documents.ex index 05983a9..cb83488 100644 --- a/lib/phoenix_kit_document_creator/documents.ex +++ b/lib/phoenix_kit_document_creator/documents.ex @@ -195,6 +195,7 @@ defmodule PhoenixKitDocumentCreator.Documents do |> Enum.map(fn {"default_width_px", v} -> {"default_width_px", parse_integer(v)} {"max_count", v} -> {"max_count", parse_integer_or_nil(v)} + {"columns", v} -> {"columns", parse_columns(v)} {k, v} -> {k, v} end) |> Enum.reject(fn {_k, v} -> v == :skip end) @@ -225,6 +226,21 @@ defmodule PhoenixKitDocumentCreator.Documents do defp parse_integer_or_nil(_), do: :skip + @max_columns 4 + + # Parses a columns value, clamping to 1..@max_columns. Falls back to 1 on + # invalid input so callers always get a usable integer (never :skip). + defp parse_columns(v) when is_integer(v), do: max(1, min(v, @max_columns)) + + defp parse_columns(v) when is_binary(v) do + case Integer.parse(v) do + {n, _} -> max(1, min(n, @max_columns)) + _ -> 1 + end + end + + defp parse_columns(_), do: 1 + @doc "List templates from the local DB. Returns maps compatible with the LiveView." @spec list_templates_from_db() :: [map()] def list_templates_from_db do @@ -306,6 +322,7 @@ defmodule PhoenixKitDocumentCreator.Documents do |> maybe_put_field(record, :type_uuid, "type_uuid") |> maybe_put_field(record, :created_by_uuid, "created_by_uuid") |> maybe_put_field(record, :inserted_at, "inserted_at") + |> maybe_put_field(record, :data, "data") end defp maybe_put_field(map, record, field, key) do @@ -1630,7 +1647,7 @@ defmodule PhoenixKitDocumentCreator.Documents do """ @spec delete_document(String.t(), keyword()) :: :ok | {:error, term()} def delete_document(file_id, opts \\ []) when is_binary(file_id) do - case move_to_deleted_folder(file_id, :deleted_documents_folder_id) do + case move_to_deleted_folder(file_id, :deleted_documents_folder_id, opts[:actor_uuid]) do :ok -> log_activity(%{ action: "document.deleted", @@ -1657,7 +1674,7 @@ defmodule PhoenixKitDocumentCreator.Documents do """ @spec delete_template(String.t(), keyword()) :: :ok | {:error, term()} def delete_template(file_id, opts \\ []) when is_binary(file_id) do - case move_to_deleted_folder(file_id, :deleted_templates_folder_id) do + case move_to_deleted_folder(file_id, :deleted_templates_folder_id, opts[:actor_uuid]) do :ok -> log_activity(%{ action: "template.deleted", @@ -1675,7 +1692,7 @@ defmodule PhoenixKitDocumentCreator.Documents do end end - defp move_to_deleted_folder(file_id, folder_key) do + defp move_to_deleted_folder(file_id, folder_key, actor_uuid) do with {:ok, folder_id} <- resolve_deleted_folder_id(folder_key), :ok <- GoogleDocsClient.move_file(file_id, folder_id) do update_file_by_google_doc_id(file_id, %{ @@ -1684,10 +1701,45 @@ defmodule PhoenixKitDocumentCreator.Documents do path: deleted_folder_path(folder_key) }) + stamp_deleted_data(file_id, actor_uuid) + :ok end end + defp stamp_deleted_data(google_doc_id, actor_uuid) do + now = DateTime.utc_now() |> DateTime.truncate(:second) + deleted_entry = %{"at" => DateTime.to_iso8601(now), "by_uuid" => actor_uuid} + + from(t in Template, + where: t.google_doc_id == ^google_doc_id, + update: [ + set: [ + data: + fragment( + "COALESCE(data, '{}'::jsonb) || jsonb_build_object('deleted', ?::jsonb)", + ^deleted_entry + ) + ] + ] + ) + |> repo().update_all([]) + + from(d in Document, + where: d.google_doc_id == ^google_doc_id, + update: [ + set: [ + data: + fragment( + "COALESCE(data, '{}'::jsonb) || jsonb_build_object('deleted', ?::jsonb)", + ^deleted_entry + ) + ] + ] + ) + |> repo().update_all([]) + end + defp resolve_deleted_folder_id(folder_key) do case get_folder_ids() do %{^folder_key => id} when is_binary(id) -> @@ -1758,6 +1810,8 @@ defmodule PhoenixKitDocumentCreator.Documents do path: location.path }) + clear_deleted_data(file_id) + :ok else %{folder_id: nil} -> {:error, :live_folder_not_found} @@ -1766,6 +1820,20 @@ defmodule PhoenixKitDocumentCreator.Documents do end end + defp clear_deleted_data(google_doc_id) do + from(t in Template, + where: t.google_doc_id == ^google_doc_id, + update: [set: [data: fragment("COALESCE(data, '{}'::jsonb) - 'deleted'")]] + ) + |> repo().update_all([]) + + from(d in Document, + where: d.google_doc_id == ^google_doc_id, + update: [set: [data: fragment("COALESCE(data, '{}'::jsonb) - 'deleted'")]] + ) + |> repo().update_all([]) + end + # =========================================================================== # Variables # =========================================================================== @@ -1833,22 +1901,58 @@ defmodule PhoenixKitDocumentCreator.Documents do Fetches the current document text via the Google Docs client and extracts all `{{ image: name }}` / `{{ images: name }}` tags, returning a list of - `%{name: String.t(), kind: :image | :image_list}` maps sorted by name. + `%{name: String.t(), kind: :image | :image_list, config: map()}` maps sorted + by name. + + The `:config` map is sourced from the saved variable in `template.variables` + (if present); otherwise it falls back to `Variable.default_image_config(kind)`. Returns `{:error, :not_found}` if no template exists for the given UUID. """ @spec image_slots_for_template(UUIDv7.t()) :: - {:ok, [%{name: String.t(), kind: :image | :image_list}]} + {:ok, [%{name: String.t(), kind: :image | :image_list, config: map()}]} | {:error, :not_found | term()} def image_slots_for_template(template_uuid) do case repo().get(Template, template_uuid) do - nil -> - {:error, :not_found} + nil -> {:error, :not_found} + template -> fetch_slots_for_template(template) + end + end - template -> - with {:ok, text} <- docs_client().get_document_text(template.google_doc_id) do - {:ok, PhoenixKitDocumentCreator.Variable.extract_image_variables(text)} - end + defp fetch_slots_for_template(template) do + with {:ok, text} <- docs_client().get_document_text(template.google_doc_id) do + vars = template.variables || [] + + slots = + text + |> PhoenixKitDocumentCreator.Variable.extract_image_variables() + |> Enum.map(fn %{name: name, kind: kind} = slot -> + Map.put(slot, :config, resolve_slot_config(vars, name, kind)) + end) + + {:ok, slots} + end + end + + # Looks up the saved variable config for a slot by name. Falls back to the + # kind-specific default when no saved variable is found or it has no config. + defp resolve_slot_config(variables, name, kind) do + saved_config = + case Enum.find(variables, &(&1["name"] == name)) do + nil -> nil + var -> var["config"] + end + + # Stringify atom keys from the default so the merged map is uniformly + # string-keyed. Saved configs from the DB jsonb column are already + # string-keyed; the default has atom keys. + string_default = + PhoenixKitDocumentCreator.Variable.default_image_config(kind) + |> Map.new(fn {k, v} -> {to_string(k), v} end) + + case saved_config do + nil -> string_default + config when is_map(config) -> Map.merge(string_default, config) end end diff --git a/lib/phoenix_kit_document_creator/documents/composer.ex b/lib/phoenix_kit_document_creator/documents/composer.ex index 832027d..2bcc656 100644 --- a/lib/phoenix_kit_document_creator/documents/composer.ex +++ b/lib/phoenix_kit_document_creator/documents/composer.ex @@ -162,6 +162,8 @@ defmodule PhoenixKitDocumentCreator.Documents.Composer do defp insert_document_and_sections(gdoc_id, sorted_sections, created_by, name, opts, repo) do category_uuid = Keyword.get(opts, :category_uuid) + data = Keyword.get(opts, :data, %{}) + Multi.new() |> Multi.insert(:document, fn _ -> Document.changeset(%Document{}, %{ @@ -170,7 +172,8 @@ defmodule PhoenixKitDocumentCreator.Documents.Composer do # legacy column not used for composed docs; nullable in DB template_uuid: nil, created_by_uuid: created_by, - category_uuid: category_uuid + category_uuid: category_uuid, + data: data }) end) |> Multi.run(:sections, fn _, %{document: doc} -> diff --git a/lib/phoenix_kit_document_creator/errors.ex b/lib/phoenix_kit_document_creator/errors.ex index d1eec8b..97426ad 100644 --- a/lib/phoenix_kit_document_creator/errors.ex +++ b/lib/phoenix_kit_document_creator/errors.ex @@ -28,6 +28,7 @@ defmodule PhoenixKitDocumentCreator.Errors do | :create_folder_failed | :deleted_folder_not_found | :documents_folder_not_found + | :drive_file_not_found | :file_trashed | :folder_not_found | :folder_search_failed @@ -66,6 +67,13 @@ defmodule PhoenixKitDocumentCreator.Errors do def message(:create_folder_failed), do: gettext("Failed to create the Drive folder") def message(:deleted_folder_not_found), do: gettext("Deleted folder not found") def message(:documents_folder_not_found), do: gettext("Documents folder not found") + + def message(:drive_file_not_found), + do: + gettext( + "File is missing in Google Drive — it cannot be restored. You can permanently delete this record." + ) + def message(:file_trashed), do: gettext("File is in the Drive trash") def message(:folder_not_found), do: gettext("Folder not found") def message(:folder_search_failed), do: gettext("Failed to search Drive for the folder") diff --git a/lib/phoenix_kit_document_creator/google_docs_client.ex b/lib/phoenix_kit_document_creator/google_docs_client.ex index dad38e4..3acabc8 100644 --- a/lib/phoenix_kit_document_creator/google_docs_client.ex +++ b/lib/phoenix_kit_document_creator/google_docs_client.ex @@ -885,6 +885,88 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do # Google rejects with `INVALID_ARGUMENT` (`google.apps.docs.v1.Unit`), so every # `insertInlineImage` batch failed. @px_to_pt 0.75 + @image_gap_pt 8.0 + @default_content_width_pt 468.0 + @max_columns 4 + + @doc """ + Page content width in points = pageSize.width − marginLeft − marginRight. + Falls back to 468pt (US Letter with 1" margins) if anything is missing. + """ + def content_width_pt(document) when is_map(document) do + ds = Map.get(document, "documentStyle") || %{} + width = magnitude(get_in(ds, ["pageSize", "width"])) + margin_l = magnitude(Map.get(ds, "marginLeft")) || 72.0 + margin_r = magnitude(Map.get(ds, "marginRight")) || 72.0 + + case width do + nil -> @default_content_width_pt + w -> w - margin_l - margin_r + end + end + + defp magnitude(%{"magnitude" => m}) when is_number(m), do: m * 1.0 + defp magnitude(_), do: nil + + @doc """ + Per-image width in points for N columns sharing `content_width_pt`. + """ + def image_width_for_columns(content_width_pt, columns) + when is_number(content_width_pt) do + n = columns |> max(1) |> min(@max_columns) + (content_width_pt - @image_gap_pt * (n - 1)) / n + end + + @doc """ + Phase A — emits batchUpdate requests that delete the placeholder range and + create a Google Docs table at its start index. After a doc re-fetch, + `fill_table_cells/3` populates the table. + """ + def table_image_inserts(%{start_index: s, end_index: e}, media, opts) + when is_list(media) do + cols = (opts[:columns] || 1) |> max(1) |> min(@max_columns) + rows = (length(media) / cols) |> Float.ceil() |> trunc() |> max(1) + + [ + %{"deleteContentRange" => %{"range" => %{"startIndex" => s, "endIndex" => e}}}, + %{"insertTable" => %{"rows" => rows, "columns" => cols, "location" => %{"index" => s}}} + ] + end + + @doc """ + Phase B — emits insertInlineImage requests for each cell, last-first so + earlier inserts don't shift later indices. One image per cell; extra cells + beyond the media list are ignored. + """ + def fill_table_cells(cells, media, %{image_width_pt: w_pt}) + when is_list(cells) do + cells + |> Enum.zip(media) + |> Enum.reverse() + |> Enum.map(fn {%{insert_index: idx}, media_item} -> + uri = Map.get(media_item, :uri) || Map.get(media_item, "uri") + src_w = Map.get(media_item, :width_px) || Map.get(media_item, "width_px") + src_h = Map.get(media_item, :height_px) || Map.get(media_item, "height_px") + + # scale_height mixes units here: target is PT, src dims are PX. The + # resulting height is w_pt * (h_px / w_px), which is numerically in PT + # because the PX ratio cancels. Google Docs renders the correct aspect; + # absolute height value is not in PT when src dims are absent (falls back + # to w_pt, a square). + scaled_h_pt = scale_height(w_pt, src_w, src_h) || w_pt + + %{ + "insertInlineImage" => %{ + "location" => %{"index" => idx}, + "uri" => uri, + "objectSize" => %{ + "width" => %{"magnitude" => w_pt * 1.0, "unit" => "PT"}, + "height" => %{"magnitude" => scaled_h_pt * 1.0, "unit" => "PT"} + } + } + } + end) + end @doc """ Builds the list of `batchUpdate` request maps to substitute image tags. @@ -897,6 +979,11 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do """ @spec build_image_batch_requests([map()], map()) :: [map()] def build_image_batch_requests(ranges, fills) do + build_image_batch_requests(ranges, fills, @default_content_width_pt) + end + + @spec build_image_batch_requests([map()], map(), number()) :: [map()] + def build_image_batch_requests(ranges, fills, content_width_pt) do ranges |> Enum.sort_by(& &1.start_index, :desc) |> Enum.flat_map(fn %{name: name, start_index: s, end_index: e} -> @@ -906,7 +993,7 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do inserts = case fill.kind do :image -> single_image_inserts(fill, s) - :image_list -> list_image_inserts(fill, s) + :image_list -> list_image_inserts(fill, s, content_width_pt) end [delete | inserts] @@ -945,17 +1032,45 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do [image_request(media, w, index, fill, media[:uri])] end - defp list_image_inserts(%{media: []}, _index), do: [] + defp list_image_inserts(%{media: []}, _index, _content_width_pt), do: [] + + # Column-aware path: dispatch on columns count. + # columns >= 2 → insertTable only. The outer build_image_batch_requests/3 + # already emits the deleteContentRange for the placeholder; emitting + # another one here would produce a zero-width (invalid) delete that the + # Google Docs API rejects with INVALID_ARGUMENT. + # columns == 1 → inline inserts using content-width-based PT width. + defp list_image_inserts(fill, index, content_width_pt) do + cols = Map.get(fill, :columns, 1) + + if cols >= 2 do + rows = (length(fill.media) / cols) |> Float.ceil() |> trunc() |> max(1) - defp list_image_inserts(fill, index) do - %{media: media, default_width_px: w, separator: sep} = fill + [ + %{ + "insertTable" => %{ + "rows" => rows, + "columns" => cols, + "location" => %{"index" => index} + } + } + ] + else + w_pt = image_width_for_columns(content_width_pt, 1) + inline_image_inserts_pt(fill, index, w_pt) + end + end + + # Inline inserts using PT width directly (for image_list columns=1 path) + defp inline_image_inserts_pt(fill, index, w_pt) do + %{media: media, separator: sep} = fill reversed = Enum.reverse(media) last_idx = length(reversed) - 1 reversed |> Enum.with_index() |> Enum.flat_map(fn {m, i} -> - img = image_request(m, w, index, fill, m[:uri]) + img = insert_inline_image_request_pt(m, w_pt, index) if i < last_idx, do: [img, separator_request(sep, index)], else: [img] end) |> Enum.reject(&is_nil/1) @@ -1012,6 +1127,28 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do } end + # Like insert_inline_image_request/3 but takes width in PT directly (no px→pt conversion). + # Used for image_list slots where width comes from image_width_for_columns/2. + defp insert_inline_image_request_pt(media, width_pt, index) when is_map(media) do + uri = Map.get(media, :uri) || Map.get(media, "uri") + w_px = Map.get(media, :width_px) || Map.get(media, "width_px") + h_px = Map.get(media, :height_px) || Map.get(media, "height_px") + + # scale_height mixes units here: target is PT, src dims are PX; the ratio cancels + scaled_height_pt = scale_height(width_pt, w_px, h_px) || width_pt + + %{ + insertInlineImage: %{ + location: %{index: index}, + uri: uri, + objectSize: %{ + width: %{magnitude: width_pt * 1.0, unit: "PT"}, + height: %{magnitude: scaled_height_pt * 1.0, unit: "PT"} + } + } + } + end + defp scale_height(target_width, src_width, src_height) when src_width in [nil, 0], do: src_height || target_width @@ -1069,7 +1206,13 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do @doc "Move a file to a different folder in Google Drive." @spec move_file(String.t(), String.t()) :: - :ok | {:error, :invalid_file_id | :move_failed | :get_file_parents_failed | term()} + :ok + | {:error, + :invalid_file_id + | :move_failed + | :get_file_parents_failed + | :drive_file_not_found + | term()} def move_file(file_id, to_folder_id) do with {:ok, fid} <- validate_file_id(file_id), {:ok, _tid} <- validate_file_id(to_folder_id) do @@ -1091,6 +1234,9 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do {:ok, %{status: status}} when status in 200..299 -> :ok + {:ok, %{status: 404}} -> + {:error, :drive_file_not_found} + {:ok, %{body: body}} -> log_drive_error("move failed", body) {:error, :move_failed} @@ -1099,6 +1245,9 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do err end + {:ok, %{status: 404}} -> + {:error, :drive_file_not_found} + {:ok, %{body: body}} -> log_drive_error("get file parents failed", body) {:error, :get_file_parents_failed} @@ -1433,26 +1582,124 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do if all_image_fills == [] do :ok else - # Build a flat fills map for find_image_tag_ranges, then filter each result - # to its section's range before building the batch. fills_map = Map.new(all_image_fills, fn {name, fill, _} -> {name, fill} end) range_by_name = Map.new(all_image_fills, fn {name, _, range} -> {name, range} end) + content_width_pt = content_width_pt(doc2) - requests = + filtered_ranges = doc2 |> find_image_tag_ranges(Map.keys(fills_map)) |> Enum.filter(fn %{name: name, start_index: s} -> in_section_range?(range_by_name, name, s) end) - |> build_image_batch_requests(fills_map) - case maybe_batch(&batch_update/2, doc_id, requests) do - {:ok, _} -> :ok - {:error, _} = err -> err + # Partition into table slots (image_list + columns >= 2) and inline slots. + {table_ranges, inline_ranges} = + Enum.split_with(filtered_ranges, fn %{name: name} -> + fill = Map.fetch!(fills_map, name) + fill.kind == :image_list and Map.get(fill, :columns, 1) >= 2 + end) + + # Phase 1 batch: inline slot deletes+inserts + table slot delete+insertTable. + # Sort all requests descending by start_index so earlier inserts don't shift later ones. + phase1_requests = + (table_ranges ++ inline_ranges) + |> build_image_batch_requests(fills_map, content_width_pt) + + # Snapshot pre-existing table start_indices from doc2 before Phase 1 so + # Phase 2 can identify the newly inserted tables by set-difference. + pre_existing_table_starts = + doc2 |> collect_tables() |> MapSet.new(& &1["table"]["startIndex"]) + + with {:ok, _} <- maybe_batch(&batch_update/2, doc_id, phase1_requests) do + if table_ranges == [] do + :ok + else + do_fill_table_cells( + doc_id, + table_ranges, + fills_map, + content_width_pt, + pre_existing_table_starts + ) + end + end + end + end + + # Phase 2: re-fetch the doc after table creation, locate the new tables by + # set-difference against pre-existing table start_indices, and fill cells. + # + # Using "last K tables" would fail when the document has pre-existing tables + # at indices below the placeholder positions — Phase 1 inserts shift those + # pre-existing tables to higher indices, making them the "last K" instead of + # the newly created ones. + defp do_fill_table_cells( + doc_id, + table_ranges, + fills_map, + content_width_pt, + pre_existing_table_starts + ) do + with {:ok, %{body: doc3}} <- get_document(doc_id) do + table_slots_asc = Enum.sort_by(table_ranges, & &1.start_index, :asc) + + # Newly inserted tables are those whose start_index was not present in + # doc2, sorted ascending so they match table_slots_asc in document order. + new_tables = + doc3 + |> collect_tables() + |> Enum.reject(fn el -> + MapSet.member?(pre_existing_table_starts, el["table"]["startIndex"]) + end) + |> Enum.sort_by(fn el -> el["table"]["startIndex"] end, :asc) + + if length(new_tables) != length(table_slots_asc) do + Logger.warning( + "substitute_all_images: expected #{length(table_slots_asc)} new tables " <> + "but found #{length(new_tables)} in doc #{doc_id}; skipping Phase 2" + ) + + :ok + else + phase2_requests = + Enum.zip(table_slots_asc, new_tables) + |> Enum.flat_map(fn {%{name: name}, table_el} -> + fill = Map.fetch!(fills_map, name) + cols = Map.get(fill, :columns, 1) + image_width_pt = image_width_for_columns(content_width_pt, cols) + cells = extract_table_cells(table_el) + fill_table_cells(cells, fill.media, %{image_width_pt: image_width_pt}) + end) + + case maybe_batch(&batch_update/2, doc_id, phase2_requests) do + {:ok, _} -> :ok + {:error, _} = err -> err + end end end end + # Walk doc body content and collect all table elements in document order. + defp collect_tables(doc) do + (get_in(doc, ["body", "content"]) || []) + |> Enum.filter(&Map.has_key?(&1, "table")) + end + + # Extract cell insert indices from a table element returned by the Docs API. + # Each cell's first paragraph provides the startIndex; we insert at startIndex + 1 + # (one position inside the paragraph, before any existing content). + defp extract_table_cells(%{"table" => %{"tableRows" => rows}}) do + for row <- rows, + %{"tableCells" => cells} = row, + cell <- cells do + cell_start = get_in(cell, ["startIndex"]) || 0 + %{insert_index: cell_start + 1} + end + end + + defp extract_table_cells(_), do: [] + defp in_section_range?(range_by_name, name, s) do case Map.get(range_by_name, name) do nil -> false @@ -1554,6 +1801,7 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do fill = %{ kind: kind, + columns: normalize_columns(Map.get(params, "columns")), default_width_px: Map.get(params, "width_px") || 400, opacity: Map.get(params, "opacity") || 1.0, z_index: Map.get(params, "z_index") || 0, @@ -1565,6 +1813,17 @@ defmodule PhoenixKitDocumentCreator.GoogleDocsClient do end) end + defp normalize_columns(n) when is_integer(n), do: n |> max(1) |> min(@max_columns) + + defp normalize_columns(n) when is_binary(n) do + case Integer.parse(n) do + {i, _} -> normalize_columns(i) + :error -> 1 + end + end + + defp normalize_columns(_), do: 1 + defp build_media_items(%{"media" => media}) when is_list(media) do Enum.map(media, fn m -> %{uri: Map.get(m, "uri", ""), width_px: Map.get(m, "width_px"), height_px: nil} diff --git a/lib/phoenix_kit_document_creator/variable.ex b/lib/phoenix_kit_document_creator/variable.ex index 6f85baf..3b89130 100644 --- a/lib/phoenix_kit_document_creator/variable.ex +++ b/lib/phoenix_kit_document_creator/variable.ex @@ -160,5 +160,12 @@ defmodule PhoenixKitDocumentCreator.Variable do def default_image_config(:image), do: %{default_width_px: 400, opacity: 1.0, z_index: 0} def default_image_config(:image_list), - do: %{default_width_px: 400, opacity: 1.0, z_index: 0, separator: :newline, max_count: nil} + do: %{ + default_width_px: 400, + opacity: 1.0, + z_index: 0, + separator: :newline, + max_count: nil, + columns: 1 + } end diff --git a/lib/phoenix_kit_document_creator/web/categories_live.ex b/lib/phoenix_kit_document_creator/web/categories_live.ex index 7c3c5e6..9972d51 100644 --- a/lib/phoenix_kit_document_creator/web/categories_live.ex +++ b/lib/phoenix_kit_document_creator/web/categories_live.ex @@ -27,8 +27,10 @@ defmodule PhoenixKitDocumentCreator.Web.CategoriesLive do selected: nil, types: [], presets: [], - categories_trash: false, - types_trash: false + categories_status_mode: "active", + types_status_mode: "active", + trashed_categories_count: 0, + trashed_types_count: 0 )} end @@ -51,29 +53,39 @@ defmodule PhoenixKitDocumentCreator.Web.CategoriesLive do with_category(socket, uuid, fn category -> {:noreply, socket - |> assign(selected: category, types_trash: false) + |> assign(selected: category, types_status_mode: "active") |> reload_types()} end) end - def handle_event("toggle_categories_trash", _params, socket) do - trash = !socket.assigns.categories_trash - + def handle_event( + "switch_status", + %{"target" => "categories", "mode" => mode}, + socket + ) + when mode in ["active", "trashed"] do {:noreply, socket - |> assign(categories_trash: trash, selected: nil, types: []) + |> assign(categories_status_mode: mode, selected: nil, types: []) |> reload_categories()} end - def handle_event("toggle_types_trash", _params, socket) do - trash = !socket.assigns.types_trash - + def handle_event( + "switch_status", + %{"target" => "types", "mode" => mode}, + socket + ) + when mode in ["active", "trashed"] do {:noreply, socket - |> assign(types_trash: trash) + |> assign(types_status_mode: mode) |> reload_types()} end + def handle_event("switch_status", _params, socket) do + {:noreply, socket} + end + def handle_event("trash_category", %{"uuid" => uuid}, socket) do with_category(socket, uuid, fn category -> case Taxonomy.trash_category(category, Helpers.actor_opts(socket)) do @@ -273,29 +285,18 @@ defmodule PhoenixKitDocumentCreator.Web.CategoriesLive do <%!-- Active / Trash sub-tabs --%> -
{format_time(file["modifiedTime"])}
++ + {format_deleted_info(file["data"]["deleted"], @deleted_by_names)} +