style: fix pre-existing formatting drift across codebase
This commit is contained in:
@@ -10,7 +10,8 @@ if config_env() == :prod do
|
|||||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1")
|
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1")
|
||||||
|
|
||||||
# Persist downloaded embedding model files alongside the database data dir.
|
# Persist downloaded embedding model files alongside the database data dir.
|
||||||
config :bumblebee, :cache_dir,
|
config :bumblebee,
|
||||||
System.get_env("BDS_MODEL_CACHE_DIR") ||
|
:cache_dir,
|
||||||
Path.join(Path.dirname(Path.expand(database_path)), "models")
|
System.get_env("BDS_MODEL_CACHE_DIR") ||
|
||||||
|
Path.join(Path.dirname(Path.expand(database_path)), "models")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ defmodule BDS.AI.ChatConversation do
|
|||||||
|
|
||||||
def changeset(conversation, attrs) do
|
def changeset(conversation, attrs) do
|
||||||
conversation
|
conversation
|
||||||
|> cast(attrs, [:id, :title, :model, :copilot_session_id, :surface_state, :created_at, :updated_at],
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[:id, :title, :model, :copilot_session_id, :surface_state, :created_at, :updated_at],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :title, :created_at, :updated_at])
|
|> validate_required([:id, :title, :created_at, :updated_at])
|
||||||
|
|||||||
@@ -263,8 +263,13 @@ defmodule BDS.Desktop.Overlay do
|
|||||||
def set_ai_suggestions_error(overlay, _error_message), do: overlay
|
def set_ai_suggestions_error(overlay, _error_message), do: overlay
|
||||||
|
|
||||||
defp normalize_ai_fields(fields) do
|
defp normalize_ai_fields(fields) do
|
||||||
Enum.map(fields, fn %{key: key, label: label, current_value: current,
|
Enum.map(fields, fn %{
|
||||||
suggested_value: suggested, locked: locked} = field ->
|
key: key,
|
||||||
|
label: label,
|
||||||
|
current_value: current,
|
||||||
|
suggested_value: suggested,
|
||||||
|
locked: locked
|
||||||
|
} = field ->
|
||||||
%{
|
%{
|
||||||
key: to_string(key),
|
key: to_string(key),
|
||||||
label: label,
|
label: label,
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
next_data = Map.put(socket.assigns.surface_data, surface_id, fields)
|
next_data = Map.put(socket.assigns.surface_data, surface_id, fields)
|
||||||
{:noreply, assign(socket, :surface_data, next_data) |> schedule_surface_state_persist() |> build_data()}
|
|
||||||
|
{:noreply,
|
||||||
|
assign(socket, :surface_data, next_data) |> schedule_surface_state_persist() |> build_data()}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event(
|
def handle_event(
|
||||||
@@ -227,8 +229,11 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do
|
|||||||
build_data(socket)
|
build_data(socket)
|
||||||
|
|
||||||
socket.assigns.offline_mode ->
|
socket.assigns.offline_mode ->
|
||||||
Notify.output(dgettext("ui", "Chat"),
|
Notify.output(
|
||||||
dgettext("ui", "Automatic AI actions stay gated by airplane mode."), "info")
|
dgettext("ui", "Chat"),
|
||||||
|
dgettext("ui", "Automatic AI actions stay gated by airplane mode."),
|
||||||
|
"info"
|
||||||
|
)
|
||||||
|
|
||||||
build_data(socket)
|
build_data(socket)
|
||||||
|
|
||||||
|
|||||||
@@ -35,14 +35,28 @@ defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|||||||
known_refs = MapSet.new(tasks, & &1.ref)
|
known_refs = MapSet.new(tasks, & &1.ref)
|
||||||
|
|
||||||
drain_tasks(
|
drain_tasks(
|
||||||
remaining, tasks, known_refs, project_id, post_id, language, translate_targets, parent
|
remaining,
|
||||||
|
tasks,
|
||||||
|
known_refs,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
)
|
)
|
||||||
|
|
||||||
send(parent, {:add_images_complete, length(paths)})
|
send(parent, {:add_images_complete, length(paths)})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp drain_tasks(
|
defp drain_tasks(
|
||||||
[], tasks, _known_refs, _project_id, _post_id, _language, _translate_targets, _parent
|
[],
|
||||||
|
tasks,
|
||||||
|
_known_refs,
|
||||||
|
_project_id,
|
||||||
|
_post_id,
|
||||||
|
_language,
|
||||||
|
_translate_targets,
|
||||||
|
_parent
|
||||||
) do
|
) do
|
||||||
Enum.each(tasks, fn task -> Task.await(task, :infinity) end)
|
Enum.each(tasks, fn task -> Task.await(task, :infinity) end)
|
||||||
end
|
end
|
||||||
@@ -65,7 +79,12 @@ defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|||||||
new_task =
|
new_task =
|
||||||
Task.async(fn ->
|
Task.async(fn ->
|
||||||
process_single_image(
|
process_single_image(
|
||||||
next_path, project_id, post_id, language, translate_targets, parent
|
next_path,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -81,8 +100,14 @@ defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
drain_tasks(
|
drain_tasks(
|
||||||
[next_path | rest], tasks, known_refs,
|
[next_path | rest],
|
||||||
project_id, post_id, language, translate_targets, parent
|
tasks,
|
||||||
|
known_refs,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -93,7 +118,12 @@ defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|||||||
new_task =
|
new_task =
|
||||||
Task.async(fn ->
|
Task.async(fn ->
|
||||||
process_single_image(
|
process_single_image(
|
||||||
next_path, project_id, post_id, language, translate_targets, parent
|
next_path,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -109,8 +139,14 @@ defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
drain_tasks(
|
drain_tasks(
|
||||||
[next_path | rest], tasks, known_refs,
|
[next_path | rest],
|
||||||
project_id, post_id, language, translate_targets, parent
|
tasks,
|
||||||
|
known_refs,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -124,16 +160,22 @@ defmodule BDS.Desktop.ShellLive.GalleryImport do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp process_single_image(
|
defp process_single_image(
|
||||||
path, project_id, post_id, language, translate_targets, parent
|
path,
|
||||||
|
project_id,
|
||||||
|
post_id,
|
||||||
|
language,
|
||||||
|
translate_targets,
|
||||||
|
parent
|
||||||
) do
|
) do
|
||||||
with {:ok, media} <- Media.import_media(%{project_id: project_id, source_path: path}),
|
with {:ok, media} <- Media.import_media(%{project_id: project_id, source_path: path}),
|
||||||
true <- String.starts_with?(media.mime_type || "", "image/"),
|
true <- String.starts_with?(media.mime_type || "", "image/"),
|
||||||
{:ok, result} <- AI.analyze_image(media.id, language: language),
|
{:ok, result} <- AI.analyze_image(media.id, language: language),
|
||||||
{:ok, _updated} <- Media.update_media(media.id, %{
|
{:ok, _updated} <-
|
||||||
title: result.title,
|
Media.update_media(media.id, %{
|
||||||
alt: result.alt,
|
title: result.title,
|
||||||
caption: result.caption
|
alt: result.alt,
|
||||||
}),
|
caption: result.caption
|
||||||
|
}),
|
||||||
{:ok, _link} <- Media.link_media_to_post(media.id, post_id) do
|
{:ok, _link} <- Media.link_media_to_post(media.id, post_id) do
|
||||||
translate_media_translations(media.id, translate_targets)
|
translate_media_translations(media.id, translate_targets)
|
||||||
title = result.title || media.original_name
|
title = result.title || media.original_name
|
||||||
|
|||||||
@@ -642,7 +642,10 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do
|
|||||||
defp maybe_update_tab_meta(socket, name) do
|
defp maybe_update_tab_meta(socket, name) do
|
||||||
title = name || dgettext("ui", "Untitled Import")
|
title = name || dgettext("ui", "Untitled Import")
|
||||||
|
|
||||||
Notify.tab_meta(:import, socket.assigns.definition_id, title,
|
Notify.tab_meta(
|
||||||
|
:import,
|
||||||
|
socket.assigns.definition_id,
|
||||||
|
title,
|
||||||
dgettext(
|
dgettext(
|
||||||
"ui",
|
"ui",
|
||||||
"Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported."
|
"Select a WordPress export file (WXR) and an uploads folder to analyze what would be imported."
|
||||||
|
|||||||
@@ -126,8 +126,12 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
Notify.dirty(:media, media.id, false)
|
Notify.dirty(:media, media.id, false)
|
||||||
|
|
||||||
Notify.tab_meta(:media, media.id, display_title(updated_media),
|
Notify.tab_meta(
|
||||||
updated_media.original_name || updated_media.mime_type || "")
|
:media,
|
||||||
|
media.id,
|
||||||
|
display_title(updated_media),
|
||||||
|
updated_media.original_name || updated_media.mime_type || ""
|
||||||
|
)
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
|
||||||
@@ -484,8 +488,12 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do
|
|||||||
|
|
||||||
Notify.dirty(:media, media.id, false)
|
Notify.dirty(:media, media.id, false)
|
||||||
|
|
||||||
Notify.tab_meta(:media, media.id, display_title(updated_media),
|
Notify.tab_meta(
|
||||||
updated_media.original_name || updated_media.mime_type || "")
|
:media,
|
||||||
|
media.id,
|
||||||
|
display_title(updated_media),
|
||||||
|
updated_media.original_name || updated_media.mime_type || ""
|
||||||
|
)
|
||||||
|
|
||||||
notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved"))
|
notify_output(socket, dgettext("ui", "Media"), dgettext("ui", "Media saved"))
|
||||||
socket
|
socket
|
||||||
|
|||||||
@@ -471,8 +471,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|> assign(:dirty?, false)
|
|> assign(:dirty?, false)
|
||||||
|> build_data()
|
|> build_data()
|
||||||
|
|
||||||
Notify.tab_meta(:post, post.id, record_title(record, refreshed_post),
|
Notify.tab_meta(
|
||||||
Atom.to_string(record_status(record)))
|
:post,
|
||||||
|
post.id,
|
||||||
|
record_title(record, refreshed_post),
|
||||||
|
Atom.to_string(record_status(record))
|
||||||
|
)
|
||||||
|
|
||||||
Notify.dirty(:post, post.id, false)
|
Notify.dirty(:post, post.id, false)
|
||||||
Notify.cancel_auto_save(:post, post.id)
|
Notify.cancel_auto_save(:post, post.id)
|
||||||
@@ -511,8 +515,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|> assign(:dirty?, false)
|
|> assign(:dirty?, false)
|
||||||
|> build_data()
|
|> build_data()
|
||||||
|
|
||||||
Notify.tab_meta(:post, post.id, record_title(record, refreshed_post),
|
Notify.tab_meta(
|
||||||
Atom.to_string(record_status(record)))
|
:post,
|
||||||
|
post.id,
|
||||||
|
record_title(record, refreshed_post),
|
||||||
|
Atom.to_string(record_status(record))
|
||||||
|
)
|
||||||
|
|
||||||
Notify.dirty(:post, post.id, false)
|
Notify.dirty(:post, post.id, false)
|
||||||
notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post published"))
|
notify_output(socket, dgettext("ui", "Post"), dgettext("ui", "Post published"))
|
||||||
@@ -546,9 +554,12 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
|> assign(:dirty?, false)
|
|> assign(:dirty?, false)
|
||||||
|> build_data()
|
|> build_data()
|
||||||
|
|
||||||
Notify.tab_meta(:post, post.id,
|
Notify.tab_meta(
|
||||||
|
:post,
|
||||||
|
post.id,
|
||||||
restored_post.title || restored_post.slug || restored_post.id,
|
restored_post.title || restored_post.slug || restored_post.id,
|
||||||
Atom.to_string(restored_post.status || :draft))
|
Atom.to_string(restored_post.status || :draft)
|
||||||
|
)
|
||||||
|
|
||||||
Notify.dirty(:post, post.id, false)
|
Notify.dirty(:post, post.id, false)
|
||||||
socket
|
socket
|
||||||
|
|||||||
@@ -271,10 +271,10 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attr :color, :string, default: nil
|
attr(:color, :string, default: nil)
|
||||||
attr :presets, :list, required: true
|
attr(:presets, :list, required: true)
|
||||||
attr :pick_event, :string, required: true
|
attr(:pick_event, :string, required: true)
|
||||||
attr :target, :any, required: true
|
attr(:target, :any, required: true)
|
||||||
|
|
||||||
defp colour_picker(assigns) do
|
defp colour_picker(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|||||||
@@ -584,7 +584,9 @@ defmodule BDS.Embeddings do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp duplicate_pairs_with_rebuild(project_id, entries, on_progress) do
|
defp duplicate_pairs_with_rebuild(project_id, entries, on_progress) do
|
||||||
case Index.duplicate_pairs(project_id, entries, @duplicate_threshold, on_progress: on_progress) do
|
case Index.duplicate_pairs(project_id, entries, @duplicate_threshold,
|
||||||
|
on_progress: on_progress
|
||||||
|
) do
|
||||||
{:ok, pairs} ->
|
{:ok, pairs} ->
|
||||||
{:ok, pairs}
|
{:ok, pairs}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,9 @@ defmodule BDS.Embeddings.Backends.Neural do
|
|||||||
# Place model params/tensors on the Apple GPU (Metal) when accelerating with
|
# Place model params/tensors on the Apple GPU (Metal) when accelerating with
|
||||||
# EMLX so the compiled inference pass actually runs on-device. EXLA manages
|
# EMLX so the compiled inference pass actually runs on-device. EXLA manages
|
||||||
# its own device placement, so nothing to do there.
|
# its own device placement, so nothing to do there.
|
||||||
defp maybe_set_default_backend(:emlx), do: Nx.global_default_backend({EMLX.Backend, device: :gpu})
|
defp maybe_set_default_backend(:emlx),
|
||||||
|
do: Nx.global_default_backend({EMLX.Backend, device: :gpu})
|
||||||
|
|
||||||
defp maybe_set_default_backend(:exla), do: :ok
|
defp maybe_set_default_backend(:exla), do: :ok
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
|||||||
@@ -58,7 +58,11 @@ defmodule BDS.Embeddings.Index do
|
|||||||
"""
|
"""
|
||||||
def neighbors(project_id, query_label, query_vector, limit)
|
def neighbors(project_id, query_label, query_vector, limit)
|
||||||
when is_binary(project_id) and is_integer(query_label) and is_binary(query_vector) do
|
when is_binary(project_id) and is_integer(query_label) and is_binary(query_vector) do
|
||||||
GenServer.call(__MODULE__, {:neighbors, project_id, query_label, query_vector, limit}, :infinity)
|
GenServer.call(
|
||||||
|
__MODULE__,
|
||||||
|
{:neighbors, project_id, query_label, query_vector, limit},
|
||||||
|
:infinity
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@@ -167,7 +171,10 @@ defmodule BDS.Embeddings.Index do
|
|||||||
|
|
||||||
defp build_entry(dimensions, entries) do
|
defp build_entry(dimensions, entries) do
|
||||||
count = length(entries)
|
count = length(entries)
|
||||||
{:ok, index} = HNSWLib.Index.new(@space, dimensions, count, m: @m, ef_construction: @ef_construction)
|
|
||||||
|
{:ok, index} =
|
||||||
|
HNSWLib.Index.new(@space, dimensions, count, m: @m, ef_construction: @ef_construction)
|
||||||
|
|
||||||
:ok = HNSWLib.Index.set_ef(index, @ef_search)
|
:ok = HNSWLib.Index.set_ef(index, @ef_search)
|
||||||
|
|
||||||
tensor =
|
tensor =
|
||||||
|
|||||||
@@ -82,9 +82,7 @@ defmodule BDS.Generation.Pagefind do
|
|||||||
# Returns nil when the page is not marked, so unmarked pages are excluded
|
# Returns nil when the page is not marked, so unmarked pages are excluded
|
||||||
# from the index entirely (matching Pagefind semantics).
|
# from the index entirely (matching Pagefind semantics).
|
||||||
defp body_text(content) do
|
defp body_text(content) do
|
||||||
case Regex.run(~r/<([a-zA-Z0-9]+)[^>]*\bdata-pagefind-body\b[^>]*>/, content,
|
case Regex.run(~r/<([a-zA-Z0-9]+)[^>]*\bdata-pagefind-body\b[^>]*>/, content, return: :index) do
|
||||||
return: :index
|
|
||||||
) do
|
|
||||||
[{open_start, open_len}, {tag_start, tag_len}] ->
|
[{open_start, open_len}, {tag_start, tag_len}] ->
|
||||||
tag = binary_part(content, tag_start, tag_len)
|
tag = binary_part(content, tag_start, tag_len)
|
||||||
region = scoped_region(content, tag, open_start + open_len)
|
region = scoped_region(content, tag, open_start + open_len)
|
||||||
@@ -117,7 +115,10 @@ defmodule BDS.Generation.Pagefind do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp event_kind(rest, pos, tag) do
|
defp event_kind(rest, pos, tag) do
|
||||||
if String.starts_with?(binary_part(rest, pos, min(2 + byte_size(tag), byte_size(rest) - pos)), "</") do
|
if String.starts_with?(
|
||||||
|
binary_part(rest, pos, min(2 + byte_size(tag), byte_size(rest) - pos)),
|
||||||
|
"</"
|
||||||
|
) do
|
||||||
:close
|
:close
|
||||||
else
|
else
|
||||||
:open
|
:open
|
||||||
|
|||||||
@@ -240,7 +240,11 @@ defmodule BDS.Media do
|
|||||||
|> Repo.insert_or_update!()
|
|> Repo.insert_or_update!()
|
||||||
end) do
|
end) do
|
||||||
{:ok, updated_translation} ->
|
{:ok, updated_translation} ->
|
||||||
log_sidecar_error(write_translation_sidecar(project, media, updated_translation), media.id)
|
log_sidecar_error(
|
||||||
|
write_translation_sidecar(project, media, updated_translation),
|
||||||
|
media.id
|
||||||
|
)
|
||||||
|
|
||||||
:ok = Search.sync_media(media.id)
|
:ok = Search.sync_media(media.id)
|
||||||
{:ok, updated_translation}
|
{:ok, updated_translation}
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,9 @@ defmodule BDS.Preview do
|
|||||||
case kind do
|
case kind do
|
||||||
:media ->
|
:media ->
|
||||||
serve_file(safe_join(server.data_dir, Path.join(["media", relative_path])),
|
serve_file(safe_join(server.data_dir, Path.join(["media", relative_path])),
|
||||||
server: server, query_params: query_params)
|
server: server,
|
||||||
|
query_params: query_params
|
||||||
|
)
|
||||||
|
|
||||||
:generated ->
|
:generated ->
|
||||||
case BDS.Preview.Router.render_route(server.project_id, request_path) do
|
case BDS.Preview.Router.render_route(server.project_id, request_path) do
|
||||||
@@ -187,7 +189,9 @@ defmodule BDS.Preview do
|
|||||||
|
|
||||||
:not_matched ->
|
:not_matched ->
|
||||||
serve_file(safe_join(Path.join(server.data_dir, "html"), relative_path),
|
serve_file(safe_join(Path.join(server.data_dir, "html"), relative_path),
|
||||||
server: server, query_params: query_params)
|
server: server,
|
||||||
|
query_params: query_params
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -240,7 +244,10 @@ defmodule BDS.Preview do
|
|||||||
|
|
||||||
defp draft_preview_payload(post, query_params) do
|
defp draft_preview_payload(post, query_params) do
|
||||||
requested_language = query_params |> Map.get("lang") |> normalize_requested_language()
|
requested_language = query_params |> Map.get("lang") |> normalize_requested_language()
|
||||||
effective_slug = post.template_slug || TemplateSelection.resolve_post_template_slug(post.project_id, post.tags, post.categories)
|
|
||||||
|
effective_slug =
|
||||||
|
post.template_slug ||
|
||||||
|
TemplateSelection.resolve_post_template_slug(post.project_id, post.tags, post.categories)
|
||||||
|
|
||||||
case draft_preview_translation(post.id, requested_language, post.language) do
|
case draft_preview_translation(post.id, requested_language, post.language) do
|
||||||
%Translation{} = translation ->
|
%Translation{} = translation ->
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ defmodule BDS.Preview.Router do
|
|||||||
defp render(project_id, {:home, page_number}, language, main_language, metadata) do
|
defp render(project_id, {:home, page_number}, language, main_language, metadata) do
|
||||||
posts = load_published_list_posts(project_id, metadata)
|
posts = load_published_list_posts(project_id, metadata)
|
||||||
posts = maybe_resolve_language(posts, language, main_language, project_id)
|
posts = maybe_resolve_language(posts, language, main_language, project_id)
|
||||||
|
|
||||||
render_list(project_id, posts, page_number, metadata, language, main_language, %{kind: "core"})
|
render_list(project_id, posts, page_number, metadata, language, main_language, %{kind: "core"})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -193,7 +194,13 @@ defmodule BDS.Preview.Router do
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render(project_id, {:day, year, month, day, page_number}, language, main_language, metadata) do
|
defp render(
|
||||||
|
project_id,
|
||||||
|
{:day, year, month, day, page_number},
|
||||||
|
language,
|
||||||
|
main_language,
|
||||||
|
metadata
|
||||||
|
) do
|
||||||
posts = load_published_posts_by_day(project_id, year, month, day)
|
posts = load_published_posts_by_day(project_id, year, month, day)
|
||||||
posts = maybe_resolve_language(posts, language, main_language, project_id)
|
posts = maybe_resolve_language(posts, language, main_language, project_id)
|
||||||
|
|
||||||
@@ -208,7 +215,8 @@ defmodule BDS.Preview.Router do
|
|||||||
## Post rendering
|
## Post rendering
|
||||||
|
|
||||||
defp render_post(project_id, post, language, main_language) do
|
defp render_post(project_id, post, language, main_language) do
|
||||||
{effective_record, body} = resolve_post_for_language(project_id, post, language, main_language)
|
{effective_record, body} =
|
||||||
|
resolve_post_for_language(project_id, post, language, main_language)
|
||||||
|
|
||||||
assigns = %{
|
assigns = %{
|
||||||
id: effective_record.id,
|
id: effective_record.id,
|
||||||
@@ -220,7 +228,9 @@ defmodule BDS.Preview.Router do
|
|||||||
_post_record: effective_record
|
_post_record: effective_record
|
||||||
}
|
}
|
||||||
|
|
||||||
effective_slug = post.template_slug || TemplateSelection.resolve_post_template_slug(project_id, post.tags, post.categories)
|
effective_slug =
|
||||||
|
post.template_slug ||
|
||||||
|
TemplateSelection.resolve_post_template_slug(project_id, post.tags, post.categories)
|
||||||
|
|
||||||
case Rendering.render_post_page(project_id, effective_slug, assigns) do
|
case Rendering.render_post_page(project_id, effective_slug, assigns) do
|
||||||
{:ok, rendered} -> {:ok, rendered}
|
{:ok, rendered} -> {:ok, rendered}
|
||||||
@@ -370,7 +380,8 @@ defmodule BDS.Preview.Router do
|
|||||||
|
|
||||||
defp archive_page_title(%{kind: "date", year: y, month: m, day: d})
|
defp archive_page_title(%{kind: "date", year: y, month: m, day: d})
|
||||||
when is_integer(y) and is_integer(m) and is_integer(d),
|
when is_integer(y) and is_integer(m) and is_integer(d),
|
||||||
do: "#{y}-#{String.pad_leading(Integer.to_string(m), 2, "0")}-#{String.pad_leading(Integer.to_string(d), 2, "0")}"
|
do:
|
||||||
|
"#{y}-#{String.pad_leading(Integer.to_string(m), 2, "0")}-#{String.pad_leading(Integer.to_string(d), 2, "0")}"
|
||||||
|
|
||||||
defp archive_page_title(%{kind: "date", year: y, month: m})
|
defp archive_page_title(%{kind: "date", year: y, month: m})
|
||||||
when is_integer(y) and is_integer(m),
|
when is_integer(y) and is_integer(m),
|
||||||
@@ -504,7 +515,8 @@ defmodule BDS.Preview.Router do
|
|||||||
if String.downcase(to_string(language)) == String.downcase(to_string(main_language)) do
|
if String.downcase(to_string(language)) == String.downcase(to_string(main_language)) do
|
||||||
posts
|
posts
|
||||||
else
|
else
|
||||||
translations = load_translations_for_language(project_id, Enum.map(posts, & &1.id), language)
|
translations =
|
||||||
|
load_translations_for_language(project_id, Enum.map(posts, & &1.id), language)
|
||||||
|
|
||||||
Enum.map(posts, fn post ->
|
Enum.map(posts, fn post ->
|
||||||
case Map.get(translations, post.id) do
|
case Map.get(translations, post.id) do
|
||||||
|
|||||||
@@ -104,8 +104,9 @@ defmodule BDS.Projects do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@spec project_data_dir(Project.t()) :: String.t()
|
@spec project_data_dir(Project.t()) :: String.t()
|
||||||
def project_data_dir(%Project{data_path: data_path}) when is_binary(data_path) and data_path != "",
|
def project_data_dir(%Project{data_path: data_path})
|
||||||
do: data_path
|
when is_binary(data_path) and data_path != "",
|
||||||
|
do: data_path
|
||||||
|
|
||||||
# A project without an explicit data_path resolves to its folder under the
|
# A project without an explicit data_path resolves to its folder under the
|
||||||
# per-user default content location — never priv/data inside the repo
|
# per-user default content location — never priv/data inside the repo
|
||||||
|
|||||||
@@ -43,12 +43,26 @@ defmodule BDS.Rendering.Filters do
|
|||||||
_language_prefix,
|
_language_prefix,
|
||||||
context
|
context
|
||||||
) do
|
) do
|
||||||
render_markdown(value, canonical_post_paths, canonical_media_paths, language, context, post_id)
|
render_markdown(
|
||||||
|
value,
|
||||||
|
canonical_post_paths,
|
||||||
|
canonical_media_paths,
|
||||||
|
language,
|
||||||
|
context,
|
||||||
|
post_id
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec render_markdown(term(), map() | nil, map() | nil, String.t(), Liquex.Context.t(), term()) ::
|
@spec render_markdown(term(), map() | nil, map() | nil, String.t(), Liquex.Context.t(), term()) ::
|
||||||
String.t()
|
String.t()
|
||||||
def render_markdown(value, canonical_post_paths, canonical_media_paths, language, context, post_id \\ nil) do
|
def render_markdown(
|
||||||
|
value,
|
||||||
|
canonical_post_paths,
|
||||||
|
canonical_media_paths,
|
||||||
|
language,
|
||||||
|
context,
|
||||||
|
post_id \\ nil
|
||||||
|
) do
|
||||||
value
|
value
|
||||||
|> to_string()
|
|> to_string()
|
||||||
|> replace_built_in_macros(language, context, post_id)
|
|> replace_built_in_macros(language, context, post_id)
|
||||||
@@ -161,7 +175,11 @@ defmodule BDS.Rendering.Filters do
|
|||||||
else
|
else
|
||||||
{:error, reason, line} ->
|
{:error, reason, line} ->
|
||||||
require Logger
|
require Logger
|
||||||
Logger.warning("Macro template parse failed (#{template_path}): #{reason} at line #{line}")
|
|
||||||
|
Logger.warning(
|
||||||
|
"Macro template parse failed (#{template_path}): #{reason} at line #{line}"
|
||||||
|
)
|
||||||
|
|
||||||
""
|
""
|
||||||
|
|
||||||
{:error, message} ->
|
{:error, message} ->
|
||||||
@@ -365,8 +383,7 @@ defmodule BDS.Rendering.Filters do
|
|||||||
"root_classes" => "macro-photo-archive",
|
"root_classes" => "macro-photo-archive",
|
||||||
"data_attrs" => [],
|
"data_attrs" => [],
|
||||||
"months" => months,
|
"months" => months,
|
||||||
"empty_label" =>
|
"empty_label" => BDS.Gettext.lgettext(language, "render", "No photos found")
|
||||||
BDS.Gettext.lgettext(language, "render", "No photos found")
|
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
@@ -391,8 +408,7 @@ defmodule BDS.Rendering.Filters do
|
|||||||
"width" => Map.get(params, "width", width),
|
"width" => Map.get(params, "width", width),
|
||||||
"height" => Map.get(params, "height", height),
|
"height" => Map.get(params, "height", height),
|
||||||
"aria_label" => "Tag cloud",
|
"aria_label" => "Tag cloud",
|
||||||
"empty_label" =>
|
"empty_label" => BDS.Gettext.lgettext(language, "render", "No tags")
|
||||||
BDS.Gettext.lgettext(language, "render", "No tags")
|
|
||||||
},
|
},
|
||||||
context
|
context
|
||||||
)
|
)
|
||||||
@@ -456,9 +472,18 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
defp group_by_media_month(media_records) do
|
defp group_by_media_month(media_records) do
|
||||||
month_names = %{
|
month_names = %{
|
||||||
1 => "January", 2 => "February", 3 => "March", 4 => "April",
|
1 => "January",
|
||||||
5 => "May", 6 => "June", 7 => "July", 8 => "August",
|
2 => "February",
|
||||||
9 => "September", 10 => "October", 11 => "November", 12 => "December"
|
3 => "March",
|
||||||
|
4 => "April",
|
||||||
|
5 => "May",
|
||||||
|
6 => "June",
|
||||||
|
7 => "July",
|
||||||
|
8 => "August",
|
||||||
|
9 => "September",
|
||||||
|
10 => "October",
|
||||||
|
11 => "November",
|
||||||
|
12 => "December"
|
||||||
}
|
}
|
||||||
|
|
||||||
media_records
|
media_records
|
||||||
@@ -555,8 +580,10 @@ defmodule BDS.Rendering.Filters do
|
|||||||
_ -> default
|
_ -> default
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_columns(value, _default, min, max) when is_integer(value),
|
defp normalize_columns(value, _default, min, max) when is_integer(value),
|
||||||
do: value |> max(min) |> min(max)
|
do: value |> max(min) |> min(max)
|
||||||
|
|
||||||
defp normalize_columns(_value, default, _min, _max), do: default
|
defp normalize_columns(_value, default, _min, _max), do: default
|
||||||
|
|
||||||
defp parse_integer(value) when is_binary(value) do
|
defp parse_integer(value) when is_binary(value) do
|
||||||
@@ -565,6 +592,7 @@ defmodule BDS.Rendering.Filters do
|
|||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp parse_integer(value) when is_integer(value), do: value
|
defp parse_integer(value) when is_integer(value), do: value
|
||||||
defp parse_integer(_value), do: nil
|
defp parse_integer(_value), do: nil
|
||||||
|
|
||||||
@@ -583,6 +611,7 @@ defmodule BDS.Rendering.Filters do
|
|||||||
case DateTime.from_iso8601("#{year}-#{pad(month + 1)}-01T00:00:00Z") do
|
case DateTime.from_iso8601("#{year}-#{pad(month + 1)}-01T00:00:00Z") do
|
||||||
{:ok, dt, _} ->
|
{:ok, dt, _} ->
|
||||||
dt |> DateTime.add(-1, :second) |> DateTime.to_date() |> Map.get(:day)
|
dt |> DateTime.add(-1, :second) |> DateTime.to_date() |> Map.get(:day)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
31
|
31
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -349,5 +349,4 @@ defmodule BDS.Rendering.ListArchive do
|
|||||||
do: RenderMetadata.calendar_initial_month(post)
|
do: RenderMetadata.calendar_initial_month(post)
|
||||||
|
|
||||||
defp calendar_initial_month_from_posts([]), do: nil
|
defp calendar_initial_month_from_posts([]), do: nil
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -63,8 +63,12 @@ defmodule BDS.Scripting do
|
|||||||
args,
|
args,
|
||||||
Keyword.put(opts, :timeout, timeout)
|
Keyword.put(opts, :timeout, timeout)
|
||||||
) do
|
) do
|
||||||
{:ok, nil} -> {:ok, ""}
|
{:ok, nil} ->
|
||||||
{:ok, value} -> {:ok, to_string(value)}
|
{:ok, ""}
|
||||||
|
|
||||||
|
{:ok, value} ->
|
||||||
|
{:ok, to_string(value)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
Logger.warning("execute_macro failed for project #{project_id}: #{inspect(reason)}")
|
Logger.warning("execute_macro failed for project #{project_id}: #{inspect(reason)}")
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
|
|||||||
@@ -111,8 +111,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
get_by_status: one_arg(fn status -> posts_by_status(project_id, status) end),
|
get_by_status: one_arg(fn status -> posts_by_status(project_id, status) end),
|
||||||
get_by_year_month: zero_or_one_arg(fn _args -> post_counts_by_year_month(project_id) end),
|
get_by_year_month: zero_or_one_arg(fn _args -> post_counts_by_year_month(project_id) end),
|
||||||
get_dashboard_stats: zero_or_one_arg(fn _args -> post_dashboard_stats(project_id) end),
|
get_dashboard_stats: zero_or_one_arg(fn _args -> post_dashboard_stats(project_id) end),
|
||||||
get_linked_by:
|
get_linked_by: one_arg(fn post_id -> linked_posts_for(project_id, post_id, :incoming) end),
|
||||||
one_arg(fn post_id -> linked_posts_for(project_id, post_id, :incoming) end),
|
|
||||||
get_links_to: one_arg(fn post_id -> linked_posts_for(project_id, post_id, :outgoing) end),
|
get_links_to: one_arg(fn post_id -> linked_posts_for(project_id, post_id, :outgoing) end),
|
||||||
get_preview_url:
|
get_preview_url:
|
||||||
two_arg(fn post_id, options -> preview_url(project_id, post_id, options) end),
|
two_arg(fn post_id, options -> preview_url(project_id, post_id, options) end),
|
||||||
@@ -157,8 +156,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
end),
|
end),
|
||||||
filter: one_arg(fn filters -> filter_media(project_id, filters) end),
|
filter: one_arg(fn filters -> filter_media(project_id, filters) end),
|
||||||
import: one_arg(fn attrs -> import_media(project_id, attrs) end),
|
import: one_arg(fn attrs -> import_media(project_id, attrs) end),
|
||||||
get_by_year_month:
|
get_by_year_month: zero_or_one_arg(fn _args -> media_counts_by_year_month(project_id) end),
|
||||||
zero_or_one_arg(fn _args -> media_counts_by_year_month(project_id) end),
|
|
||||||
get_file_path: one_arg(fn media_id -> media_file_path(project_id, media_id) end),
|
get_file_path: one_arg(fn media_id -> media_file_path(project_id, media_id) end),
|
||||||
update: two_arg(fn media_id, attrs -> update_media(project_id, media_id, attrs) end),
|
update: two_arg(fn media_id, attrs -> update_media(project_id, media_id, attrs) end),
|
||||||
delete: one_arg(fn media_id -> delete_media(project_id, media_id) end),
|
delete: one_arg(fn media_id -> delete_media(project_id, media_id) end),
|
||||||
@@ -172,8 +170,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
two_arg(fn media_id, language ->
|
two_arg(fn media_id, language ->
|
||||||
load_media_translation(project_id, media_id, language)
|
load_media_translation(project_id, media_id, language)
|
||||||
end),
|
end),
|
||||||
get_translations:
|
get_translations: one_arg(fn media_id -> list_media_translations(project_id, media_id) end),
|
||||||
one_arg(fn media_id -> list_media_translations(project_id, media_id) end),
|
|
||||||
get_url: one_arg(fn media_id -> media_url(project_id, media_id) end),
|
get_url: one_arg(fn media_id -> media_url(project_id, media_id) end),
|
||||||
rebuild_from_files: zero_or_one_arg(fn _args -> rebuild_media_from_files(project_id) end),
|
rebuild_from_files: zero_or_one_arg(fn _args -> rebuild_media_from_files(project_id) end),
|
||||||
regenerate_missing_thumbnails:
|
regenerate_missing_thumbnails:
|
||||||
@@ -201,8 +198,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
get: one_arg(fn script_id -> load_script(project_id, script_id) end),
|
get: one_arg(fn script_id -> load_script(project_id, script_id) end),
|
||||||
get_all: zero_or_one_arg(fn _args -> list_scripts(project_id) end),
|
get_all: zero_or_one_arg(fn _args -> list_scripts(project_id) end),
|
||||||
publish: one_arg(fn script_id -> publish_script(project_id, script_id) end),
|
publish: one_arg(fn script_id -> publish_script(project_id, script_id) end),
|
||||||
rebuild_from_files:
|
rebuild_from_files: zero_or_one_arg(fn _args -> rebuild_scripts_from_files(project_id) end)
|
||||||
zero_or_one_arg(fn _args -> rebuild_scripts_from_files(project_id) end)
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -295,8 +291,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
find_similar: two_arg(fn post_id, limit -> find_similar(post_id, limit) end),
|
find_similar: two_arg(fn post_id, limit -> find_similar(post_id, limit) end),
|
||||||
compute_similarities:
|
compute_similarities:
|
||||||
two_arg(fn post_id, target_ids -> compute_similarities(post_id, target_ids) end),
|
two_arg(fn post_id, target_ids -> compute_similarities(post_id, target_ids) end),
|
||||||
suggest_tags:
|
suggest_tags: two_arg(fn post_id, exclude_tags -> suggest_tags(post_id, exclude_tags) end),
|
||||||
two_arg(fn post_id, exclude_tags -> suggest_tags(post_id, exclude_tags) end),
|
|
||||||
find_duplicates: zero_or_one_arg(fn _args -> find_duplicates(project_id) end),
|
find_duplicates: zero_or_one_arg(fn _args -> find_duplicates(project_id) end),
|
||||||
dismiss_pair: two_arg(fn post_id_a, post_id_b -> dismiss_pair(post_id_a, post_id_b) end),
|
dismiss_pair: two_arg(fn post_id_a, post_id_b -> dismiss_pair(post_id_a, post_id_b) end),
|
||||||
index_unindexed_posts: zero_or_one_arg(fn _args -> index_unindexed_posts(project_id) end)
|
index_unindexed_posts: zero_or_one_arg(fn _args -> index_unindexed_posts(project_id) end)
|
||||||
|
|||||||
@@ -284,14 +284,24 @@ defmodule BDS.Search do
|
|||||||
|
|
||||||
defp maybe_where_year(query, year) do
|
defp maybe_where_year(query, year) do
|
||||||
year_str = to_string(year)
|
year_str = to_string(year)
|
||||||
where(query, [p], fragment("strftime('%Y', datetime(? / 1000, 'unixepoch')) = ?", p.created_at, ^year_str))
|
|
||||||
|
where(
|
||||||
|
query,
|
||||||
|
[p],
|
||||||
|
fragment("strftime('%Y', datetime(? / 1000, 'unixepoch')) = ?", p.created_at, ^year_str)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_where_month(query, nil), do: query
|
defp maybe_where_month(query, nil), do: query
|
||||||
|
|
||||||
defp maybe_where_month(query, month) do
|
defp maybe_where_month(query, month) do
|
||||||
month_str = String.pad_leading(to_string(month), 2, "0")
|
month_str = String.pad_leading(to_string(month), 2, "0")
|
||||||
where(query, [p], fragment("strftime('%m', datetime(? / 1000, 'unixepoch')) = ?", p.created_at, ^month_str))
|
|
||||||
|
where(
|
||||||
|
query,
|
||||||
|
[p],
|
||||||
|
fragment("strftime('%m', datetime(? / 1000, 'unixepoch')) = ?", p.created_at, ^month_str)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_where_from(query, nil), do: query
|
defp maybe_where_from(query, nil), do: query
|
||||||
@@ -305,7 +315,10 @@ defmodule BDS.Search do
|
|||||||
defp maybe_where_tags(query, tags) do
|
defp maybe_where_tags(query, tags) do
|
||||||
tags_clause =
|
tags_clause =
|
||||||
Enum.reduce(tags, false, fn tag, acc ->
|
Enum.reduce(tags, false, fn tag, acc ->
|
||||||
dynamic([p], ^acc or fragment("EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)", p.tags, ^tag))
|
dynamic(
|
||||||
|
[p],
|
||||||
|
^acc or fragment("EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)", p.tags, ^tag)
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
where(query, [p], ^tags_clause)
|
where(query, [p], ^tags_clause)
|
||||||
@@ -316,7 +329,10 @@ defmodule BDS.Search do
|
|||||||
defp maybe_where_tags_media(query, tags) do
|
defp maybe_where_tags_media(query, tags) do
|
||||||
tags_clause =
|
tags_clause =
|
||||||
Enum.reduce(tags, false, fn tag, acc ->
|
Enum.reduce(tags, false, fn tag, acc ->
|
||||||
dynamic([m], ^acc or fragment("EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)", m.tags, ^tag))
|
dynamic(
|
||||||
|
[m],
|
||||||
|
^acc or fragment("EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)", m.tags, ^tag)
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
where(query, [m], ^tags_clause)
|
where(query, [m], ^tags_clause)
|
||||||
@@ -327,7 +343,15 @@ defmodule BDS.Search do
|
|||||||
defp maybe_where_categories(query, categories) do
|
defp maybe_where_categories(query, categories) do
|
||||||
categories_clause =
|
categories_clause =
|
||||||
Enum.reduce(categories, false, fn category, acc ->
|
Enum.reduce(categories, false, fn category, acc ->
|
||||||
dynamic([p], ^acc or fragment("EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)", p.categories, ^category))
|
dynamic(
|
||||||
|
[p],
|
||||||
|
^acc or
|
||||||
|
fragment(
|
||||||
|
"EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)",
|
||||||
|
p.categories,
|
||||||
|
^category
|
||||||
|
)
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
where(query, [p], ^categories_clause)
|
where(query, [p], ^categories_clause)
|
||||||
@@ -548,8 +572,6 @@ defmodule BDS.Search do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
defp post_index_fields(post, translations) do
|
defp post_index_fields(post, translations) do
|
||||||
post_language = normalize_language(post.language)
|
post_language = normalize_language(post.language)
|
||||||
|
|
||||||
@@ -656,8 +678,8 @@ defmodule BDS.Search do
|
|||||||
else
|
else
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from translation in MediaTranslation,
|
from translation in MediaTranslation,
|
||||||
where: translation.translation_for in ^media_ids,
|
where: translation.translation_for in ^media_ids,
|
||||||
select: {translation.translation_for, translation}
|
select: {translation.translation_for, translation}
|
||||||
)
|
)
|
||||||
|> Enum.group_by(fn {media_id, _} -> media_id end, fn {_, translation} -> translation end)
|
|> Enum.group_by(fn {media_id, _} -> media_id end, fn {_, translation} -> translation end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ defmodule BDS.Tags do
|
|||||||
{:error, changeset} -> Repo.rollback(changeset)
|
{:error, changeset} -> Repo.rollback(changeset)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Enum.map(affected_posts, & &1.id)
|
Enum.map(affected_posts, & &1.id)
|
||||||
end)
|
end)
|
||||||
|> case do
|
|> case do
|
||||||
|
|||||||
@@ -79,8 +79,14 @@ defmodule BDS.UI.Dashboard do
|
|||||||
from post in Post,
|
from post in Post,
|
||||||
where: post.project_id == ^project_id,
|
where: post.project_id == ^project_id,
|
||||||
group_by: [
|
group_by: [
|
||||||
fragment("CAST(strftime('%Y', datetime(? / 1000, 'unixepoch')) AS INTEGER)", post.created_at),
|
fragment(
|
||||||
fragment("CAST(strftime('%m', datetime(? / 1000, 'unixepoch')) AS INTEGER)", post.created_at)
|
"CAST(strftime('%Y', datetime(? / 1000, 'unixepoch')) AS INTEGER)",
|
||||||
|
post.created_at
|
||||||
|
),
|
||||||
|
fragment(
|
||||||
|
"CAST(strftime('%m', datetime(? / 1000, 'unixepoch')) AS INTEGER)",
|
||||||
|
post.created_at
|
||||||
|
)
|
||||||
],
|
],
|
||||||
select: %{
|
select: %{
|
||||||
year:
|
year:
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ defmodule BDS.CSM006NPlusOneTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Clear FTS and reindex
|
# Clear FTS and reindex
|
||||||
Repo.query!("DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)", [project.id])
|
Repo.query!(
|
||||||
|
"DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)",
|
||||||
|
[project.id]
|
||||||
|
)
|
||||||
|
|
||||||
# Reindex should succeed and produce correct FTS entries
|
# Reindex should succeed and produce correct FTS entries
|
||||||
assert :ok = BDS.Search.reindex_posts(project.id)
|
assert :ok = BDS.Search.reindex_posts(project.id)
|
||||||
@@ -82,7 +85,11 @@ defmodule BDS.CSM006NPlusOneTest do
|
|||||||
language: "en"
|
language: "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
Repo.query!("DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)", [project.id])
|
Repo.query!(
|
||||||
|
"DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)",
|
||||||
|
[project.id]
|
||||||
|
)
|
||||||
|
|
||||||
assert :ok = BDS.Search.reindex_posts(project.id)
|
assert :ok = BDS.Search.reindex_posts(project.id)
|
||||||
|
|
||||||
{:ok, results} = BDS.Search.search_posts(project.id, "unique-keyword-xyz")
|
{:ok, results} = BDS.Search.search_posts(project.id, "unique-keyword-xyz")
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "triggers no dashboard or git queries", %{project: _project} do
|
test "triggers no dashboard or git queries", %{project: _project} do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
render_click(view, "toggle_sidebar", %{})
|
count_queries(fn ->
|
||||||
end)
|
render_click(view, "toggle_sidebar", %{})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries for sidebar toggle, got #{query_count}"
|
"Expected 0 DB queries for sidebar toggle, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -37,12 +38,13 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "triggers no DB queries" do
|
test "triggers no DB queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
render_click(view, "toggle_panel", %{})
|
count_queries(fn ->
|
||||||
end)
|
render_click(view, "toggle_panel", %{})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries for panel toggle, got #{query_count}"
|
"Expected 0 DB queries for panel toggle, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,16 +52,17 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "triggers no DB queries" do
|
test "triggers no DB queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
render_click(view, "sync_layout", %{
|
count_queries(fn ->
|
||||||
"sidebar_width" => "300",
|
render_click(view, "sync_layout", %{
|
||||||
"sidebar_visible" => "true",
|
"sidebar_width" => "300",
|
||||||
"panel_visible" => "false"
|
"sidebar_visible" => "true",
|
||||||
})
|
"panel_visible" => "false"
|
||||||
end)
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries for sync_layout, got #{query_count}"
|
"Expected 0 DB queries for sync_layout, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,12 +70,13 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "triggers no DB queries" do
|
test "triggers no DB queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
render_click(view, "select_panel_tab", %{"tab" => "output"})
|
count_queries(fn ->
|
||||||
end)
|
render_click(view, "select_panel_tab", %{"tab" => "output"})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries for select_panel_tab, got #{query_count}"
|
"Expected 0 DB queries for select_panel_tab, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,14 +84,15 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "triggers sidebar query but not dashboard or git queries" do
|
test "triggers sidebar query but not dashboard or git queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
{query_count, query_sources} = count_queries_with_sources(fn ->
|
{query_count, query_sources} =
|
||||||
render_click(view, "select_view", %{"view" => "media"})
|
count_queries_with_sources(fn ->
|
||||||
end)
|
render_click(view, "select_view", %{"view" => "media"})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count > 0, "Expected at least 1 query for view change"
|
assert query_count > 0, "Expected at least 1 query for view change"
|
||||||
|
|
||||||
refute "dashboard" in query_sources or "projects" in query_sources,
|
refute "dashboard" in query_sources or "projects" in query_sources,
|
||||||
"View change should not query dashboard or projects, got: #{inspect(query_sources)}"
|
"View change should not query dashboard or projects, got: #{inspect(query_sources)}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -95,14 +100,15 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "do not trigger dashboard or git queries" do
|
test "do not trigger dashboard or git queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
{_count, query_sources} = count_queries_with_sources(fn ->
|
{_count, query_sources} =
|
||||||
render_click(view, "update_sidebar_search", %{
|
count_queries_with_sources(fn ->
|
||||||
"sidebar_filters" => %{"search" => "test"}
|
render_click(view, "update_sidebar_search", %{
|
||||||
})
|
"sidebar_filters" => %{"search" => "test"}
|
||||||
end)
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
refute "dashboard" in query_sources,
|
refute "dashboard" in query_sources,
|
||||||
"Sidebar search should not query dashboard, got: #{inspect(query_sources)}"
|
"Sidebar search should not query dashboard, got: #{inspect(query_sources)}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -110,12 +116,13 @@ defmodule BDS.CSM007ReloadShellTest do
|
|||||||
test "triggers only the settings write, no refresh queries" do
|
test "triggers only the settings write, no refresh queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
render_click(view, "toggle_offline_mode", %{})
|
count_queries(fn ->
|
||||||
end)
|
render_click(view, "toggle_offline_mode", %{})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 1,
|
assert query_count == 1,
|
||||||
"Expected exactly 1 DB query (settings write) for offline mode toggle, got #{query_count}"
|
"Expected exactly 1 DB query (settings write) for offline mode toggle, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ defmodule BDS.CSM008RenderPathTest do
|
|||||||
render_click(view, "select_tab", %{"type" => "post", "id" => post.id})
|
render_click(view, "select_tab", %{"type" => "post", "id" => post.id})
|
||||||
render_click(view, "select_panel_tab", %{"tab" => "post_links"})
|
render_click(view, "select_panel_tab", %{"tab" => "post_links"})
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
1..10 |> Enum.each(fn _ -> render(view) end)
|
count_queries(fn ->
|
||||||
end)
|
1..10 |> Enum.each(fn _ -> render(view) end)
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries on panel re-renders, got #{query_count}"
|
"Expected 0 DB queries on panel re-renders, got #{query_count}"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "git_log panel re-render uses cached assigns", %{post: post} do
|
test "git_log panel re-render uses cached assigns", %{post: post} do
|
||||||
@@ -51,12 +52,13 @@ defmodule BDS.CSM008RenderPathTest do
|
|||||||
render_click(view, "select_tab", %{"type" => "post", "id" => post.id})
|
render_click(view, "select_tab", %{"type" => "post", "id" => post.id})
|
||||||
render_click(view, "select_panel_tab", %{"tab" => "git_log"})
|
render_click(view, "select_panel_tab", %{"tab" => "git_log"})
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
1..10 |> Enum.each(fn _ -> render(view) end)
|
count_queries(fn ->
|
||||||
end)
|
1..10 |> Enum.each(fn _ -> render(view) end)
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries on git_log re-renders, got #{query_count}"
|
"Expected 0 DB queries on git_log re-renders, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -64,26 +66,28 @@ defmodule BDS.CSM008RenderPathTest do
|
|||||||
test "switching to output panel triggers no post/media queries" do
|
test "switching to output panel triggers no post/media queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
{query_count, query_sources} = count_queries_with_sources(fn ->
|
{query_count, query_sources} =
|
||||||
render_click(view, "select_panel_tab", %{"tab" => "output"})
|
count_queries_with_sources(fn ->
|
||||||
end)
|
render_click(view, "select_panel_tab", %{"tab" => "output"})
|
||||||
|
end)
|
||||||
|
|
||||||
refute "post_links" in query_sources,
|
refute "post_links" in query_sources,
|
||||||
"Switching to output should not query post_links, got: #{inspect(query_sources)}"
|
"Switching to output should not query post_links, got: #{inspect(query_sources)}"
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 queries for output panel tab, got #{query_count}"
|
"Expected 0 queries for output panel tab, got #{query_count}"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "switching to tasks panel triggers no post/media queries" do
|
test "switching to tasks panel triggers no post/media queries" do
|
||||||
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
|
||||||
|
|
||||||
{query_count, _query_sources} = count_queries_with_sources(fn ->
|
{query_count, _query_sources} =
|
||||||
render_click(view, "select_panel_tab", %{"tab" => "tasks"})
|
count_queries_with_sources(fn ->
|
||||||
end)
|
render_click(view, "select_panel_tab", %{"tab" => "tasks"})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 queries for tasks panel tab, got #{query_count}"
|
"Expected 0 queries for tasks panel tab, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -98,12 +102,13 @@ defmodule BDS.CSM008RenderPathTest do
|
|||||||
"subtitle" => "preset subtitle"
|
"subtitle" => "preset subtitle"
|
||||||
})
|
})
|
||||||
|
|
||||||
query_count = count_queries(fn ->
|
query_count =
|
||||||
render_click(view, "select_tab", %{"type" => "post", "id" => post.id})
|
count_queries(fn ->
|
||||||
end)
|
render_click(view, "select_tab", %{"type" => "post", "id" => post.id})
|
||||||
|
end)
|
||||||
|
|
||||||
assert query_count == 0,
|
assert query_count == 0,
|
||||||
"Expected 0 DB queries when tab meta is already complete, got #{query_count}"
|
"Expected 0 DB queries when tab meta is already complete, got #{query_count}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ defmodule BDS.CSM009ThumbnailErrorHandlingTest do
|
|||||||
assert {:error, _reason} = result
|
assert {:error, _reason} = result
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ensure_thumbnails returns :ok for non-image media", %{project: project, temp_dir: temp_dir} do
|
test "ensure_thumbnails returns :ok for non-image media", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "readme.txt")
|
source_path = Path.join(temp_dir, "readme.txt")
|
||||||
File.write!(source_path, "just text")
|
File.write!(source_path, "just text")
|
||||||
|
|
||||||
@@ -86,11 +89,15 @@ defmodule BDS.CSM009ThumbnailErrorHandlingTest do
|
|||||||
} do
|
} do
|
||||||
source_path = Path.join(temp_dir, "good.jpg")
|
source_path = Path.join(temp_dir, "good.jpg")
|
||||||
File.write!(source_path, tiny_jpeg_binary())
|
File.write!(source_path, tiny_jpeg_binary())
|
||||||
{:ok, good_media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
|
||||||
|
{:ok, good_media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
corrupt_path = Path.join(temp_dir, "bad.jpg")
|
corrupt_path = Path.join(temp_dir, "bad.jpg")
|
||||||
File.write!(corrupt_path, "corrupt data")
|
File.write!(corrupt_path, "corrupt data")
|
||||||
{:ok, bad_media} = BDS.Media.import_media(%{project_id: project.id, source_path: corrupt_path})
|
|
||||||
|
{:ok, bad_media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: corrupt_path})
|
||||||
|
|
||||||
Enum.each(Map.values(Thumbnails.thumbnail_paths(good_media)), fn path ->
|
Enum.each(Map.values(Thumbnails.thumbnail_paths(good_media)), fn path ->
|
||||||
File.rm(Path.join(temp_dir, path))
|
File.rm(Path.join(temp_dir, path))
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ defmodule BDS.CSM020NestedCaseTest do
|
|||||||
describe "Publishing.handle_call :update_job uses with" do
|
describe "Publishing.handle_call :update_job uses with" do
|
||||||
test "source code uses with instead of case" do
|
test "source code uses with instead of case" do
|
||||||
source = File.read!("lib/bds/publishing.ex")
|
source = File.read!("lib/bds/publishing.ex")
|
||||||
[func_source] = Regex.scan(~r/def handle_call\(\{:update_job.*?(?=\n def |\n @impl)/s, source)
|
|
||||||
|
[func_source] =
|
||||||
|
Regex.scan(~r/def handle_call\(\{:update_job.*?(?=\n def |\n @impl)/s, source)
|
||||||
|
|
||||||
assert func_source |> List.first() |> String.contains?("with"),
|
assert func_source |> List.first() |> String.contains?("with"),
|
||||||
"update_job handler should use with"
|
"update_job handler should use with"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ defmodule BDS.CSM031TryRescueTest do
|
|||||||
describe "source-level: no inline try/rescue around Liquex.render!" do
|
describe "source-level: no inline try/rescue around Liquex.render!" do
|
||||||
test "filters.ex has no try/rescue block in render_macro_source" do
|
test "filters.ex has no try/rescue block in render_macro_source" do
|
||||||
source = File.read!("lib/bds/rendering/filters.ex")
|
source = File.read!("lib/bds/rendering/filters.ex")
|
||||||
|
|
||||||
refute source =~ ~r/try do\s+.*Liquex\.render!/s,
|
refute source =~ ~r/try do\s+.*Liquex\.render!/s,
|
||||||
"render_macro_source should use safe_liquex_render helper, not inline try/rescue"
|
"render_macro_source should use safe_liquex_render helper, not inline try/rescue"
|
||||||
end
|
end
|
||||||
@@ -51,6 +52,7 @@ defmodule BDS.CSM031TryRescueTest do
|
|||||||
|
|
||||||
test "template_selection.ex uses FileSystem.try_read instead of read_template_file" do
|
test "template_selection.ex uses FileSystem.try_read instead of read_template_file" do
|
||||||
source = File.read!("lib/bds/rendering/template_selection.ex")
|
source = File.read!("lib/bds/rendering/template_selection.ex")
|
||||||
|
|
||||||
refute source =~ "read_template_file",
|
refute source =~ "read_template_file",
|
||||||
"should use FileSystem.try_read, not the raising read_template_file"
|
"should use FileSystem.try_read, not the raising read_template_file"
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ defmodule BDS.CSM032MapGetPatternMatchTest do
|
|||||||
describe "source-level: overlay.ex uses pattern matching for known structures" do
|
describe "source-level: overlay.ex uses pattern matching for known structures" do
|
||||||
test "delete_details uses pattern matching instead of Map.get" do
|
test "delete_details uses pattern matching instead of Map.get" do
|
||||||
source = File.read!("lib/bds/desktop/overlay.ex")
|
source = File.read!("lib/bds/desktop/overlay.ex")
|
||||||
|
|
||||||
refute source =~ "Map.get(delete_details,",
|
refute source =~ "Map.get(delete_details,",
|
||||||
"overlay.ex should pattern match delete_details instead of using Map.get"
|
"overlay.ex should pattern match delete_details instead of using Map.get"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "merge_details uses pattern matching instead of Map.get" do
|
test "merge_details uses pattern matching instead of Map.get" do
|
||||||
source = File.read!("lib/bds/desktop/overlay.ex")
|
source = File.read!("lib/bds/desktop/overlay.ex")
|
||||||
|
|
||||||
refute source =~ "Map.get(merge,",
|
refute source =~ "Map.get(merge,",
|
||||||
"overlay.ex should pattern match merge_details instead of using Map.get"
|
"overlay.ex should pattern match merge_details instead of using Map.get"
|
||||||
end
|
end
|
||||||
@@ -65,32 +67,40 @@ defmodule BDS.CSM032MapGetPatternMatchTest do
|
|||||||
describe "source-level: generation pipeline uses dot access for Post struct fields" do
|
describe "source-level: generation pipeline uses dot access for Post struct fields" do
|
||||||
test "outputs.ex uses post.language instead of Map.get(post, :language)" do
|
test "outputs.ex uses post.language instead of Map.get(post, :language)" do
|
||||||
source = File.read!("lib/bds/generation/outputs.ex")
|
source = File.read!("lib/bds/generation/outputs.ex")
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :language)",
|
refute source =~ "Map.get(post, :language)",
|
||||||
"outputs.ex should use post.language"
|
"outputs.ex should use post.language"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "data.ex uses dot access for Post struct fields in build_published_translation_variant" do
|
test "data.ex uses dot access for Post struct fields in build_published_translation_variant" do
|
||||||
source = File.read!("lib/bds/generation/data.ex")
|
source = File.read!("lib/bds/generation/data.ex")
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :author)",
|
refute source =~ "Map.get(post, :author)",
|
||||||
"data.ex should use post.author"
|
"data.ex should use post.author"
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :tags",
|
refute source =~ "Map.get(post, :tags",
|
||||||
"data.ex should use post.tags"
|
"data.ex should use post.tags"
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :categories",
|
refute source =~ "Map.get(post, :categories",
|
||||||
"data.ex should use post.categories"
|
"data.ex should use post.categories"
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :template_slug)",
|
refute source =~ "Map.get(post, :template_slug)",
|
||||||
"data.ex should use post.template_slug"
|
"data.ex should use post.template_slug"
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :do_not_translate",
|
refute source =~ "Map.get(post, :do_not_translate",
|
||||||
"data.ex should use post.do_not_translate"
|
"data.ex should use post.do_not_translate"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validation.ex uses post.file_path instead of Map.get(post, :file_path)" do
|
test "validation.ex uses post.file_path instead of Map.get(post, :file_path)" do
|
||||||
source = File.read!("lib/bds/generation/validation.ex")
|
source = File.read!("lib/bds/generation/validation.ex")
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :file_path)",
|
refute source =~ "Map.get(post, :file_path)",
|
||||||
"validation.ex should use post.file_path"
|
"validation.ex should use post.file_path"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sitemap.ex uses post.do_not_translate instead of Map.get" do
|
test "sitemap.ex uses post.do_not_translate instead of Map.get" do
|
||||||
source = File.read!("lib/bds/generation/sitemap.ex")
|
source = File.read!("lib/bds/generation/sitemap.ex")
|
||||||
|
|
||||||
refute source =~ "Map.get(post, :do_not_translate)",
|
refute source =~ "Map.get(post, :do_not_translate)",
|
||||||
"sitemap.ex should use post.do_not_translate"
|
"sitemap.ex should use post.do_not_translate"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -118,9 +118,7 @@ defmodule BDS.CSM033BatchInsertsTest do
|
|||||||
assert Enum.all?(posts, fn post -> post.id in indexed end)
|
assert Enum.all?(posts, fn post -> post.id in indexed end)
|
||||||
|
|
||||||
keys =
|
keys =
|
||||||
BDS.Repo.all(
|
BDS.Repo.all(from(k in BDS.Embeddings.Key, where: k.project_id == ^project.id))
|
||||||
from(k in BDS.Embeddings.Key, where: k.project_id == ^project.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert length(keys) == 5
|
assert length(keys) == 5
|
||||||
labels = Enum.map(keys, & &1.label) |> Enum.sort()
|
labels = Enum.map(keys, & &1.label) |> Enum.sort()
|
||||||
@@ -141,7 +139,8 @@ defmodule BDS.CSM033BatchInsertsTest do
|
|||||||
original_key =
|
original_key =
|
||||||
BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
|
BDS.Repo.get_by!(BDS.Embeddings.Key, project_id: project.id, post_id: post.id)
|
||||||
|
|
||||||
{:ok, _post} = BDS.Posts.update_post(post.id, %{content: "completely different content now"})
|
{:ok, _post} =
|
||||||
|
BDS.Posts.update_post(post.id, %{content: "completely different content now"})
|
||||||
|
|
||||||
{:ok, rebuilt_ids} = BDS.Embeddings.rebuild_project(project.id)
|
{:ok, rebuilt_ids} = BDS.Embeddings.rebuild_project(project.id)
|
||||||
assert post.id in rebuilt_ids
|
assert post.id in rebuilt_ids
|
||||||
@@ -175,9 +174,7 @@ defmodule BDS.CSM033BatchInsertsTest do
|
|||||||
assert repaired == [post_a.id]
|
assert repaired == [post_a.id]
|
||||||
|
|
||||||
keys =
|
keys =
|
||||||
BDS.Repo.all(
|
BDS.Repo.all(from(k in BDS.Embeddings.Key, where: k.project_id == ^project.id))
|
||||||
from(k in BDS.Embeddings.Key, where: k.project_id == ^project.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
assert length(keys) == 2
|
assert length(keys) == 2
|
||||||
end
|
end
|
||||||
@@ -206,5 +203,4 @@ defmodule BDS.CSM033BatchInsertsTest do
|
|||||||
assert key_before.vector == key_after.vector
|
assert key_before.vector == key_after.vector
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ defmodule BDS.CSM034FileReadBangTest do
|
|||||||
test "release_packaging.ex has no File.read! or File.write!" do
|
test "release_packaging.ex has no File.read! or File.write!" do
|
||||||
source = File.read!("lib/bds/release_packaging.ex")
|
source = File.read!("lib/bds/release_packaging.ex")
|
||||||
refute source =~ "File.read!", "release_packaging.ex should use File.read, not File.read!"
|
refute source =~ "File.read!", "release_packaging.ex should use File.read, not File.read!"
|
||||||
refute source =~ "File.write!", "release_packaging.ex should use File.write, not File.write!"
|
|
||||||
|
refute source =~ "File.write!",
|
||||||
|
"release_packaging.ex should use File.write, not File.write!"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ defmodule BDS.CSM035ProcessDictTest do
|
|||||||
match = Regex.run(~r/def sidebar_content\(assigns\).*?\n(.*?)\n/s, source)
|
match = Regex.run(~r/def sidebar_content\(assigns\).*?\n(.*?)\n/s, source)
|
||||||
assert match, "could not find sidebar_content/1"
|
assert match, "could not find sidebar_content/1"
|
||||||
[_, first_line | _] = match
|
[_, first_line | _] = match
|
||||||
assert first_line =~ "UILocale.put", "sidebar_content/1 must call UILocale.put on its first line"
|
|
||||||
|
assert first_line =~ "UILocale.put",
|
||||||
|
"sidebar_content/1 must call UILocale.put on its first line"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "MenuBar.mount/1 calls UILocale.put" do
|
test "MenuBar.mount/1 calls UILocale.put" do
|
||||||
@@ -57,7 +59,9 @@ defmodule BDS.CSM035ProcessDictTest do
|
|||||||
match = Regex.run(~r/def mount\(menu\).*?\n(.*?)\n/s, source)
|
match = Regex.run(~r/def mount\(menu\).*?\n(.*?)\n/s, source)
|
||||||
assert match, "could not find mount/1 in menu_bar.ex"
|
assert match, "could not find mount/1 in menu_bar.ex"
|
||||||
[_, first_line | _] = match
|
[_, first_line | _] = match
|
||||||
assert first_line =~ "UILocale.put", "MenuBar.mount/1 must call UILocale.put on its first line"
|
|
||||||
|
assert first_line =~ "UILocale.put",
|
||||||
|
"MenuBar.mount/1 must call UILocale.put on its first line"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ defmodule BDS.Desktop.AutomationTest do
|
|||||||
|
|
||||||
assert :ok = Automation.reload(session)
|
assert :ok = Automation.reload(session)
|
||||||
|
|
||||||
snapshot = await(session, &(&1.sidebar_visible == true and &1.sidebar_width >= resized_width - 2))
|
snapshot =
|
||||||
|
await(session, &(&1.sidebar_visible == true and &1.sidebar_width >= resized_width - 2))
|
||||||
|
|
||||||
assert snapshot.sidebar_visible == true
|
assert snapshot.sidebar_visible == true
|
||||||
assert snapshot.sidebar_width >= resized_width - 2
|
assert snapshot.sidebar_width >= resized_width - 2
|
||||||
assert snapshot.sidebar_width <= resized_width + 2
|
assert snapshot.sidebar_width <= resized_width + 2
|
||||||
|
|||||||
@@ -315,9 +315,15 @@ defmodule BDS.DesktopTest do
|
|||||||
assert conn.resp_body =~ ~s(data-testid="toggle-sidebar")
|
assert conn.resp_body =~ ~s(data-testid="toggle-sidebar")
|
||||||
assert conn.resp_body =~ ~s(data-testid="toggle-panel")
|
assert conn.resp_body =~ ~s(data-testid="toggle-panel")
|
||||||
assert conn.resp_body =~ ~s(data-testid="toggle-assistant")
|
assert conn.resp_body =~ ~s(data-testid="toggle-assistant")
|
||||||
assert conn.resp_body =~ ~s(class="activity-bar flex h-full shrink-0 flex-col items-center justify-between")
|
|
||||||
|
assert conn.resp_body =~
|
||||||
|
~s(class="activity-bar flex h-full shrink-0 flex-col items-center justify-between")
|
||||||
|
|
||||||
assert conn.resp_body =~ ~s(class="sidebar flex min-w-0 flex-1 overflow-hidden")
|
assert conn.resp_body =~ ~s(class="sidebar flex min-w-0 flex-1 overflow-hidden")
|
||||||
assert conn.resp_body =~ ~s(class="status-bar flex h-[22px] shrink-0 items-center justify-between gap-2")
|
|
||||||
|
assert conn.resp_body =~
|
||||||
|
~s(class="status-bar flex h-[22px] shrink-0 items-center justify-between gap-2")
|
||||||
|
|
||||||
assert conn.resp_body =~ ~s(data-phx-main)
|
assert conn.resp_body =~ ~s(data-phx-main)
|
||||||
assert conn.resp_body =~ ~s(href="/assets/app.css")
|
assert conn.resp_body =~ ~s(href="/assets/app.css")
|
||||||
assert conn.resp_body =~ ~s(src="/assets/app.js")
|
assert conn.resp_body =~ ~s(src="/assets/app.js")
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ defmodule BDS.ImageImportPipelineTest do
|
|||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
{:ok, project} = BDS.Projects.create_project(%{name: "Image Import Test", data_path: temp_dir})
|
{:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Image Import Test", data_path: temp_dir})
|
||||||
|
|
||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -40,7 +42,8 @@ defmodule BDS.ImageImportPipelineTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
{:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
{:ok,
|
||||||
|
["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "multi selection filters out empty lines" do
|
test "multi selection filters out empty lines" do
|
||||||
@@ -51,7 +54,8 @@ defmodule BDS.ImageImportPipelineTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
{:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
{:ok,
|
||||||
|
["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "multi selection with single file returns list with one element" do
|
test "multi selection with single file returns list with one element" do
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ defmodule BDS.ImportDefinitionsTest do
|
|||||||
result = ImportDefinitions.decode_analysis_result(malicious_json)
|
result = ImportDefinitions.decode_analysis_result(malicious_json)
|
||||||
|
|
||||||
assert is_map(result)
|
assert is_map(result)
|
||||||
assert Map.get(result, unknown_key_1) == "val" or Map.get(result, "csm001_fictive_#{unique_suffix}") == "val"
|
|
||||||
|
assert Map.get(result, unknown_key_1) == "val" or
|
||||||
|
Map.get(result, "csm001_fictive_#{unique_suffix}") == "val"
|
||||||
|
|
||||||
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key_1) end
|
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key_1) end
|
||||||
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key_2) end
|
assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_key_2) end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -83,9 +83,10 @@ defmodule BDS.MapUtilsTest do
|
|||||||
test "safe_atomize_keys does not create atoms for malicious payloads" do
|
test "safe_atomize_keys does not create atoms for malicious payloads" do
|
||||||
unique_suffix = :erlang.unique_integer()
|
unique_suffix = :erlang.unique_integer()
|
||||||
|
|
||||||
malicious = for i <- 1..500, into: %{} do
|
malicious =
|
||||||
{"csm001_malicious_#{i}_#{unique_suffix}", "val"}
|
for i <- 1..500, into: %{} do
|
||||||
end
|
{"csm001_malicious_#{i}_#{unique_suffix}", "val"}
|
||||||
|
end
|
||||||
|
|
||||||
result = MapUtils.safe_atomize_keys(malicious)
|
result = MapUtils.safe_atomize_keys(malicious)
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ defmodule BDS.PostsTest do
|
|||||||
File.write!(old_full, "stale content")
|
File.write!(old_full, "stale content")
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
BDS.Repo.update_all(
|
BDS.Repo.update_all(
|
||||||
from(p in BDS.Posts.Post, where: p.id == ^published.id),
|
from(p in BDS.Posts.Post, where: p.id == ^published.id),
|
||||||
set: [file_path: old_relative, content: "edited body"]
|
set: [file_path: old_relative, content: "edited body"]
|
||||||
|
|||||||
@@ -424,9 +424,10 @@ defmodule BDS.PreviewTest do
|
|||||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "on-demand rendering: published post route renders via template without generated files", %{
|
test "on-demand rendering: published post route renders via template without generated files",
|
||||||
project: project
|
%{
|
||||||
} do
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
main_language: "en",
|
main_language: "en",
|
||||||
@@ -458,9 +459,10 @@ defmodule BDS.PreviewTest do
|
|||||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "on-demand rendering: home page renders published posts as list without generated files", %{
|
test "on-demand rendering: home page renders published posts as list without generated files",
|
||||||
project: project
|
%{
|
||||||
} do
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
main_language: "en",
|
main_language: "en",
|
||||||
|
|||||||
@@ -96,7 +96,9 @@ defmodule BDS.Scripts.TransformsTest do
|
|||||||
data.content = data.content .. "D"
|
data.content = data.content .. "D"
|
||||||
return data
|
return data
|
||||||
end
|
end
|
||||||
""", enabled: false)
|
""",
|
||||||
|
enabled: false
|
||||||
|
)
|
||||||
|
|
||||||
transform(other.id, "Foreign", """
|
transform(other.id, "Foreign", """
|
||||||
function main(data, _ctx)
|
function main(data, _ctx)
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ defmodule BDS.TemplateLookupPriorityTest do
|
|||||||
|
|
||||||
describe "BundledDefaultTemplatesExistOutsideProjectData" do
|
describe "BundledDefaultTemplatesExistOutsideProjectData" do
|
||||||
test "single-post bundled template resolves with no Template rows", %{project: project} do
|
test "single-post bundled template resolves with no Template rows", %{project: project} do
|
||||||
assert [] = BDS.Repo.all(from t in BDS.Templates.Template, where: t.project_id == ^project.id)
|
assert [] =
|
||||||
|
BDS.Repo.all(from t in BDS.Templates.Template, where: t.project_id == ^project.id)
|
||||||
|
|
||||||
{:ok, source} = TemplateSelection.load_template_source(project.id, :post, nil)
|
{:ok, source} = TemplateSelection.load_template_source(project.id, :post, nil)
|
||||||
|
|
||||||
@@ -150,7 +151,8 @@ defmodule BDS.TemplateLookupPriorityTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "post-list bundled template resolves with no Template rows", %{project: project} do
|
test "post-list bundled template resolves with no Template rows", %{project: project} do
|
||||||
assert [] = BDS.Repo.all(from t in BDS.Templates.Template, where: t.project_id == ^project.id)
|
assert [] =
|
||||||
|
BDS.Repo.all(from t in BDS.Templates.Template, where: t.project_id == ^project.id)
|
||||||
|
|
||||||
{:ok, source} = TemplateSelection.load_template_source(project.id, :list, nil)
|
{:ok, source} = TemplateSelection.load_template_source(project.id, :list, nil)
|
||||||
|
|
||||||
@@ -158,7 +160,8 @@ defmodule BDS.TemplateLookupPriorityTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "not-found bundled template resolves with no Template rows", %{project: project} do
|
test "not-found bundled template resolves with no Template rows", %{project: project} do
|
||||||
assert [] = BDS.Repo.all(from t in BDS.Templates.Template, where: t.project_id == ^project.id)
|
assert [] =
|
||||||
|
BDS.Repo.all(from t in BDS.Templates.Template, where: t.project_id == ^project.id)
|
||||||
|
|
||||||
{:ok, source} = TemplateSelection.load_template_source(project.id, :not_found, nil)
|
{:ok, source} = TemplateSelection.load_template_source(project.id, :not_found, nil)
|
||||||
|
|
||||||
|
|||||||
@@ -175,25 +175,39 @@ defmodule BDS.UI.ShellTest do
|
|||||||
|
|
||||||
test "phase 3 templates use shared shell and form primitives for common layout" do
|
test "phase 3 templates use shared shell and form primitives for common layout" do
|
||||||
post_template =
|
post_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
media_template =
|
media_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
script_template =
|
script_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/script_editor_html/script_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
template_template =
|
template_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/template_editor_html/template_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
chat_template =
|
chat_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/chat_editor_html/chat_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
menu_template =
|
menu_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/menu_editor_html/menu_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/menu_editor_html/menu_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
settings_template =
|
settings_template =
|
||||||
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex")
|
File.read!(
|
||||||
|
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/settings_editor_html/settings_editor.html.heex"
|
||||||
|
)
|
||||||
|
|
||||||
assert post_template =~ "ui-editor-shell"
|
assert post_template =~ "ui-editor-shell"
|
||||||
assert post_template =~ "ui-editor-header"
|
assert post_template =~ "ui-editor-header"
|
||||||
@@ -248,8 +262,12 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assistant_css = File.read!("/Users/gb/Projects/bDS2/assets/css/assistant.css")
|
assistant_css = File.read!("/Users/gb/Projects/bDS2/assets/css/assistant.css")
|
||||||
menu_css = File.read!("/Users/gb/Projects/bDS2/assets/css/menu_editor.css")
|
menu_css = File.read!("/Users/gb/Projects/bDS2/assets/css/menu_editor.css")
|
||||||
|
|
||||||
refute editor_css =~ ".post-editor .editor-header,\n.scripts-view-shell.editor .editor-header,\n.templates-view-shell.editor .editor-header {\n display: flex;"
|
refute editor_css =~
|
||||||
refute editor_css =~ ".post-editor .editor-actions,\n.scripts-view-shell.editor .editor-actions,\n.templates-view-shell.editor .editor-actions {\n display: flex;"
|
".post-editor .editor-header,\n.scripts-view-shell.editor .editor-header,\n.templates-view-shell.editor .editor-header {\n display: flex;"
|
||||||
|
|
||||||
|
refute editor_css =~
|
||||||
|
".post-editor .editor-actions,\n.scripts-view-shell.editor .editor-actions,\n.templates-view-shell.editor .editor-actions {\n display: flex;"
|
||||||
|
|
||||||
refute editor_css =~ ".post-editor .quick-actions-menu {"
|
refute editor_css =~ ".post-editor .quick-actions-menu {"
|
||||||
refute media_css =~ "[data-testid=\"media-editor\"] .editor-header {"
|
refute media_css =~ "[data-testid=\"media-editor\"] .editor-header {"
|
||||||
refute media_css =~ "[data-testid=\"media-editor\"] .editor-actions {"
|
refute media_css =~ "[data-testid=\"media-editor\"] .editor-actions {"
|
||||||
@@ -287,9 +305,14 @@ defmodule BDS.UI.ShellTest do
|
|||||||
assert css =~ ".chat-panel .chat-input-container"
|
assert css =~ ".chat-panel .chat-input-container"
|
||||||
assert css =~ ".chat-model-selector-menu"
|
assert css =~ ".chat-model-selector-menu"
|
||||||
|
|
||||||
assert css =~ "@media (max-width: 720px) {\n .chat-panel-header {\n align-items: stretch;\n flex-direction: column;"
|
assert css =~
|
||||||
|
"@media (max-width: 720px) {\n .chat-panel-header {\n align-items: stretch;\n flex-direction: column;"
|
||||||
|
|
||||||
assert css =~ ".chat-model-selector-wrap {\n width: 100%;"
|
assert css =~ ".chat-model-selector-wrap {\n width: 100%;"
|
||||||
assert css =~ ".chat-panel .chat-model-selector-button.chat-model-selector-inline {\n justify-content: space-between;\n width: 100%;"
|
|
||||||
|
assert css =~
|
||||||
|
".chat-panel .chat-model-selector-button.chat-model-selector-inline {\n justify-content: space-between;\n width: 100%;"
|
||||||
|
|
||||||
assert css =~ ".chat-panel .chat-input-container {\n padding: 8px 12px;"
|
assert css =~ ".chat-panel .chat-input-container {\n padding: 8px 12px;"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user