defmodule PhoenixKit.Modules.Publishing.Web.New do @moduledoc """ LiveView for creating a new blog workspace (publishing group). """ use PhoenixKitWeb, :live_view use Gettext, backend: PhoenixKitWeb.Gettext alias PhoenixKit.Modules.Publishing alias PhoenixKit.Modules.Publishing.PubSub, as: PublishingPubSub alias PhoenixKit.Modules.Publishing.Storage alias PhoenixKit.Settings alias PhoenixKit.Utils.Routes # Preset types with their default item names @preset_types Publishing.preset_types() def mount(_params, _session, socket) do {initial_params, auto_slug?, last_generated_slug, auto_item_names?} = normalize_form_params(%{}, true, "", true) socket = socket |> assign(:project_title, Settings.get_setting("project_title", "PhoenixKit")) |> assign(:page_title, gettext("Create Publishing Group")) |> assign( :current_path, Routes.path("/admin/settings/publishing/new", locale: socket.assigns.current_locale_base) ) |> assign(:slug_autogenerated?, auto_slug?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:item_names_autogenerated?, auto_item_names?) |> assign(:form_params, initial_params) |> assign(:form, new_blog_form(initial_params)) |> assign(:preset_types, @preset_types) |> assign(:enabled_languages, Storage.enabled_language_codes()) |> assign(:endpoint_url, nil) {:ok, socket} end def handle_params(_params, uri, socket) do endpoint_url = extract_endpoint_url(uri) {:noreply, assign(socket, :endpoint_url, endpoint_url)} end def handle_event("update_new_group", %{"blog" => params}, socket) do {normalized_params, auto_slug?, last_generated_slug, auto_item_names?} = normalize_form_params( params, socket.assigns.slug_autogenerated?, socket.assigns.last_generated_slug, socket.assigns.item_names_autogenerated? ) {:noreply, socket |> assign(:slug_autogenerated?, auto_slug?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:item_names_autogenerated?, auto_item_names?) |> assign(:form_params, normalized_params) |> assign(:form, new_blog_form(normalized_params))} end def handle_event("manual_slug", %{"blog" => %{"slug" => value}}, socket) do current_params = socket.assigns.form_params name = Map.get(current_params, "name", "") auto_slug = if name == "", do: "", else: Publishing.slugify(name) trimmed_value = value |> to_string() |> String.trim() cond do # Empty slug - revert to auto-generated trimmed_value == "" -> updated = Map.put(current_params, "slug", auto_slug) {:noreply, socket |> assign(:slug_autogenerated?, true) |> assign(:last_generated_slug, auto_slug) |> assign(:form_params, updated) |> assign(:form, new_blog_form(updated))} # Manually entered slug matches auto-generated - keep autogenerated flag trimmed_value == auto_slug -> updated = Map.put(current_params, "slug", auto_slug) {:noreply, socket |> assign(:slug_autogenerated?, true) |> assign(:last_generated_slug, auto_slug) |> assign(:form_params, updated) |> assign(:form, new_blog_form(updated))} # Any other manual slug - just store it as-is (validation happens on submit) true -> updated = Map.put(current_params, "slug", trimmed_value) {:noreply, socket |> assign(:slug_autogenerated?, false) |> assign(:last_generated_slug, trimmed_value) |> assign(:form_params, updated) |> assign(:form, new_blog_form(updated)) |> clear_flash()} end end def handle_event("add_group", %{"blog" => params}, socket) do # Use raw params for validation - don't normalize/sanitize name = params |> Map.get("name", "") |> String.trim() mode = params |> Map.get("mode", "timestamp") slug = params |> Map.get("slug", "") |> String.trim() custom_type = params |> Map.get("custom_type", "") |> String.trim() type = if custom_type != "" do custom_type else params |> Map.get("type", "blogging") end item_singular = params |> Map.get("item_singular", "") |> String.trim() item_plural = params |> Map.get("item_plural", "") |> String.trim() # Still normalize for UI updates in case of error {normalized_params, auto_slug?, last_generated_slug, auto_item_names?} = normalize_form_params( params, socket.assigns.slug_autogenerated?, socket.assigns.last_generated_slug, socket.assigns.item_names_autogenerated? ) # Pass nil if slug is empty to trigger auto-generation slug_to_validate = if slug == "", do: nil, else: slug # Build options for add_blog opts = [mode: mode, slug: slug_to_validate, type: type] |> maybe_add_opt(:item_singular, item_singular) |> maybe_add_opt(:item_plural, item_plural) case Publishing.add_group(name, opts) do {:ok, group} -> # Broadcast group created for live dashboard updates PublishingPubSub.broadcast_group_created(group) {:noreply, socket |> put_flash(:info, gettext("Publishing group \"%{name}\" created", name: group["name"])) |> push_navigate( to: Routes.path("/admin/settings/publishing", locale: socket.assigns.current_locale_base) )} {:error, :already_exists} -> {:noreply, socket |> assign_form_state( normalized_params, auto_slug?, last_generated_slug, auto_item_names? ) |> put_flash(:error, gettext("That publishing group already exists"))} {:error, :invalid_name} -> {:noreply, socket |> assign_form_state( normalized_params, auto_slug?, last_generated_slug, auto_item_names? ) |> put_flash(:error, gettext("Please enter a valid name"))} {:error, :invalid_mode} -> {:noreply, socket |> assign_form_state( normalized_params, auto_slug?, last_generated_slug, auto_item_names? ) |> put_flash(:error, gettext("Invalid storage mode"))} {:error, :invalid_type} -> {:noreply, socket |> assign_form_state( normalized_params, auto_slug?, last_generated_slug, auto_item_names? ) |> put_flash(:error, gettext("Invalid content type"))} {:error, :invalid_slug} -> {:noreply, socket |> assign_form_state( normalized_params, auto_slug?, last_generated_slug, auto_item_names? ) |> put_flash( :error, gettext( "Invalid slug format. Please use only lowercase letters, numbers, and hyphens (e.g. my-blog-name)" ) )} {:error, _reason} -> {:noreply, socket |> assign_form_state( normalized_params, auto_slug?, last_generated_slug, auto_item_names? ) |> put_flash(:error, gettext("Failed to create publishing group"))} end end # When type changes via radio button click, reset item names to auto-fill mode def handle_event("type_changed", %{"type" => new_type}, socket) do current_params = socket.assigns.form_params {default_singular, default_plural} = get_default_item_names(new_type) updated_params = current_params |> Map.put("type", new_type) |> Map.put("item_singular", default_singular) |> Map.put("item_plural", default_plural) {:noreply, socket |> assign(:item_names_autogenerated?, true) |> assign(:form_params, updated_params) |> assign(:form, new_blog_form(updated_params))} end # Also handle when type comes through the form def handle_event("type_changed", %{"blog" => %{"type" => new_type}}, socket) do handle_event("type_changed", %{"type" => new_type}, socket) end # When user manually edits item names, disable auto-fill def handle_event("item_name_changed", _params, socket) do {:noreply, assign(socket, :item_names_autogenerated?, false)} end def handle_event("cancel", _params, socket) do {:noreply, push_navigate(socket, to: Routes.path("/admin/settings/publishing", locale: socket.assigns.current_locale_base) )} end # Helper to assign form state after an error defp assign_form_state(socket, params, auto_slug?, last_generated_slug, auto_item_names?) do socket |> assign(:slug_autogenerated?, auto_slug?) |> assign(:last_generated_slug, last_generated_slug) |> assign(:item_names_autogenerated?, auto_item_names?) |> assign(:form_params, params) |> assign(:form, new_blog_form(params)) end # Helper to add optional params only if non-empty defp maybe_add_opt(opts, _key, ""), do: opts defp maybe_add_opt(opts, key, value), do: Keyword.put(opts, key, value) defp new_blog_form(params) when is_map(params) do defaults = %{ "name" => "", "slug" => "", "mode" => "timestamp", "type" => "blogging", "item_singular" => "post", "item_plural" => "posts" } values = Map.merge(defaults, params) to_form(values, as: :blog) end # Normalize form params with auto-fill behavior for slug and item names defp normalize_form_params( params, slug_autogenerated?, last_generated_slug, item_names_autogenerated? ) do name = normalize_name(params) mode = normalize_mode(params) type = normalize_type(params) {slug_value, new_auto_slug?, new_last_generated} = normalize_slug(params, name, slug_autogenerated?, last_generated_slug) {item_singular, item_plural, new_auto_item_names?} = normalize_item_names(params, type, item_names_autogenerated?) result_params = %{ "name" => name, "slug" => slug_value, "mode" => mode, "type" => type, "item_singular" => item_singular, "item_plural" => item_plural } {result_params, new_auto_slug?, new_last_generated, new_auto_item_names?} end defp normalize_name(params) do params |> Map.get("name", "") |> to_string() |> String.trim() end defp normalize_mode(params) do params |> Map.get("mode", "timestamp") |> to_string() |> String.downcase() |> case do "slug" -> "slug" _ -> "timestamp" end end defp normalize_type(params) do custom_type = params |> Map.get("custom_type", "") |> to_string() |> String.trim() if custom_type != "" do String.downcase(custom_type) else params |> Map.get("type", "blogging") |> to_string() |> String.downcase() end end defp normalize_slug(params, name, slug_autogenerated?, last_generated_slug) do trimmed_slug = params |> Map.get("slug", "") |> to_string() |> String.trim() auto_slug = if name == "", do: "", else: Publishing.slugify(name) if slug_autogenerated? do {auto_slug, true, auto_slug} else slug_val = if trimmed_slug == "", do: auto_slug, else: trimmed_slug auto? = slug_val == auto_slug new_last = if auto?, do: slug_val, else: last_generated_slug {slug_val, auto?, new_last} end end defp normalize_item_names(params, type, item_names_autogenerated?) do {default_singular, default_plural} = get_default_item_names(type) item_singular_input = params |> Map.get("item_singular", "") |> to_string() |> String.trim() item_plural_input = params |> Map.get("item_plural", "") |> to_string() |> String.trim() if item_names_autogenerated? do {default_singular, default_plural, true} else singular = if item_singular_input == "", do: default_singular, else: item_singular_input plural = if item_plural_input == "", do: default_plural, else: item_plural_input auto? = singular == default_singular and plural == default_plural {singular, plural, auto?} end end # Get default item names for a content type defp get_default_item_names(type) do case Enum.find(@preset_types, fn p -> p.type == type end) do %{item_singular: singular, item_plural: plural} -> {singular, plural} nil -> {"item", "items"} end end defp extract_endpoint_url(uri) when is_binary(uri) do case URI.parse(uri) do %URI{scheme: scheme, host: host, port: port} when not is_nil(scheme) and not is_nil(host) -> port_string = if port in [80, 443], do: "", else: ":#{port}" "#{scheme}://#{host}#{port_string}" _ -> "" end end defp extract_endpoint_url(_), do: "" end