Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/css/_phoenix_kit_sources.css
Original file line number Diff line number Diff line change
@@ -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";
2 changes: 1 addition & 1 deletion lib/phoenix_kit_document_creator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
126 changes: 115 additions & 11 deletions lib/phoenix_kit_document_creator/documents.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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, %{
Expand All @@ -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) ->
Expand Down Expand Up @@ -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}
Expand All @@ -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
# ===========================================================================
Expand Down Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion lib/phoenix_kit_document_creator/documents/composer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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{}, %{
Expand All @@ -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} ->
Expand Down
8 changes: 8 additions & 0 deletions lib/phoenix_kit_document_creator/errors.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Loading