Packages

phoenix_kit

1.7.179
1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

221 Versions

Jump to

Compare versions

8 files changed
+239 additions
-4 deletions
unknownCHANGELOG.md
File is too large to be displayed (100 KB limit).
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/BeamLabEU/phoenix_kit">>}]}.
2 2 {<<"name">>,<<"phoenix_kit">>}.
3 - {<<"version">>,<<"1.7.178">>}.
3 + {<<"version">>,<<"1.7.179">>}.
4 4 {<<"description">>,
5 5 <<65,32,102,111,117,110,100,97,116,105,111,110,32,102,111,114,32,98,117,105,
6 6 108,100,105,110,103,32,69,108,105,120,105,114,32,80,104,111,101,110,105,
  @@ -519,6 +519,7 @@
519 519 <<"lib/phoenix_kit/migrations/postgres/v56.ex">>,
520 520 <<"lib/phoenix_kit/migrations/postgres/v09.ex">>,
521 521 <<"lib/phoenix_kit/migrations/postgres/v116.ex">>,
522 + <<"lib/phoenix_kit/migrations/postgres/v139.ex">>,
522 523 <<"lib/phoenix_kit/migrations/postgres/v26.ex">>,
523 524 <<"lib/phoenix_kit/migrations/postgres/v79.ex">>,
524 525 <<"lib/phoenix_kit/migrations/postgres/v38.ex">>,
  @@ -60,6 +60,7 @@ defmodule PhoenixKit.Install.JsIntegration do
60 60 |> add_js_sources_compiler()
61 61 |> seed_modules_js_file()
62 62 |> add_modules_script_tag_to_layout()
63 + |> add_viewport_param_to_app_js()
63 64 end
64 65
65 66 @doc """
  @@ -260,6 +261,170 @@ defmodule PhoenixKit.Install.JsIntegration do
260 261 end
261 262 end
262 263
264 + # Add `viewport_width` to the LiveSocket connect params (as a closure, so
265 + # reconnects re-read the width). PhoenixKit LiveViews with responsive layouts
266 + # (e.g. the dashboards builder) use it to pick the right tier server-side on
267 + # the FIRST render instead of detecting it with a hook round-trip behind a
268 + # loading state. They all degrade gracefully without it.
269 + defp add_viewport_param_to_app_js(igniter) do
270 + app_js_path = Path.join([File.cwd!(), "assets", "js", "app.js"])
271 +
272 + if File.exists?(app_js_path) do
273 + content = File.read!(app_js_path)
274 +
275 + case inject_viewport_param(content) do
276 + :already ->
277 + igniter
278 +
279 + {:ok, updated} ->
280 + File.write!(app_js_path, updated)
281 +
282 + Igniter.add_notice(
283 + igniter,
284 + "✅ Added viewport_width to the LiveSocket connect params in assets/js/app.js"
285 + )
286 +
287 + :manual ->
288 + viewport_manual_notice(igniter)
289 + end
290 + else
291 + viewport_manual_notice(igniter)
292 + end
293 + end
294 +
295 + # A params object further than this from `new LiveSocket(` is assumed to
296 + # belong to something else (another socket, a config literal) — manual notice.
297 + @viewport_params_window 500
298 +
299 + @doc false
300 + # Pure transform (public for tests): rewrite the common phx.new shape
301 + # `params: {_csrf_token: csrfToken}` into a closure carrying viewport_width.
302 + # Deliberately conservative — regex surgery on arbitrary host code must not
303 + # corrupt it, so anything fancier gets a manual notice instead:
304 + # * only the params object INSIDE the `new LiveSocket(` options is touched
305 + # (an earlier `new Socket("/socket", {params: …})` must not be rewritten
306 + # while the real LiveSocket params stay bare),
307 + # * objects containing comments are refused (appending after a trailing
308 + # `// …` would swallow the new code into the comment),
309 + # * already a closure / nested braces → no simple-object match → refused.
310 + def inject_viewport_param(content) do
311 + # The KEY form specifically — a prose mention in a comment ("add
312 + # viewport_width someday") must not make the installer skip the patch.
313 + if Regex.match?(~r/viewport_width\s*:/, content),
314 + do: :already,
315 + else: rewrite_livesocket_params(content)
316 + end
317 +
318 + defp rewrite_livesocket_params(content) do
319 + with {:ok, anchor_end} <- livesocket_anchor(content),
320 + rest = binary_part(content, anchor_end, byte_size(content) - anchor_end),
321 + {:ok, {start, len}, inner} <- top_level_params(rest) do
322 + trimmed = inner |> String.trim() |> String.trim_trailing(",")
323 + prefix = if trimmed == "", do: "", else: trimmed <> ", "
324 +
325 + rewritten =
326 + binary_part(rest, 0, start) <>
327 + "params: () => ({#{prefix}viewport_width: window.innerWidth})" <>
328 + binary_part(rest, start + len, byte_size(rest) - start - len)
329 +
330 + {:ok, binary_part(content, 0, anchor_end) <> rewritten}
331 + else
332 + _ -> :manual
333 + end
334 + end
335 +
336 + # The first `new LiveSocket(` that isn't on a commented line — a documentation
337 + # example (`// Example: new LiveSocket(...)`) must not anchor the patch away
338 + # from the real call below it.
339 + defp livesocket_anchor(content) do
340 + :binary.matches(content, "new LiveSocket(")
341 + |> Enum.find_value(:manual, fn {pos, len} ->
342 + if commented_at?(content, pos), do: nil, else: {:ok, pos + len}
343 + end)
344 + end
345 +
346 + defp commented_at?(content, pos) do
347 + before = binary_part(content, 0, pos)
348 + line_prefix = before |> String.split("\n") |> List.last()
349 +
350 + String.contains?(line_prefix, "//") or String.contains?(line_prefix, "/*") or
351 + line_prefix |> String.trim_leading() |> String.starts_with?("*") or
352 + inside_block_comment?(before)
353 + end
354 +
355 + # An unclosed /* before the position means we're inside a multi-line block
356 + # comment (whose lines need no leading *) — e.g. a commented-out example call
357 + # would otherwise anchor the patch away from the real call below it.
358 + defp inside_block_comment?(before) do
359 + length(:binary.matches(before, "/*")) > length(:binary.matches(before, "*/"))
360 + end
361 +
362 + # The first simple `params: {…}` at the TOP level of the LiveSocket options
363 + # object (brace depth exactly 1 relative to the call's `(`): a nested
364 + # `params:` inside a hook body (`this.pushEvent("load", {params: {page: 1}})`)
365 + # must never be rewritten. Comment-bearing objects are refused (appending
366 + # after a trailing `// …` would swallow the new code into the comment), as is
367 + # anything farther than the window (assumed to belong to something else).
368 + defp top_level_params(rest) do
369 + ~r/params:\s*\{([^{}]*)\}/
370 + |> Regex.scan(rest, return: :index)
371 + |> Enum.find_value(:manual, fn [{start, len}, {inner_start, inner_len}] ->
372 + inner = binary_part(rest, inner_start, inner_len)
373 +
374 + cond do
375 + start > @viewport_params_window -> :manual
376 + brace_depth(binary_part(rest, 0, start)) != 1 -> nil
377 + String.contains?(inner, "//") or String.contains?(inner, "/*") -> :manual
378 + true -> {:ok, {start, len}, inner}
379 + end
380 + end)
381 + |> case do
382 + {:ok, _, _} = ok -> ok
383 + _ -> :manual
384 + end
385 + end
386 +
387 + # Brace depth of the segment with string literals and comments blanked out
388 + # first — a `"}}}"` inside a hook option must not fake depth 1 at a nested
389 + # site (that produced a WRONG rewrite in review; imperfect blanking merely
390 + # skews depth away from 1, which fails closed to :manual).
391 + defp brace_depth(segment) do
392 + for <<char <- blank_strings_and_comments(segment)>>, reduce: 0 do
393 + depth ->
394 + case char do
395 + ?{ -> depth + 1
396 + ?} -> depth - 1
397 + _ -> depth
398 + end
399 + end
400 + end
401 +
402 + defp blank_strings_and_comments(segment) do
403 + segment
404 + |> String.replace(~r/\\./, "")
405 + |> String.replace(~r/"[^"]*"/, "\"\"")
406 + |> String.replace(~r/'[^']*'/, "''")
407 + |> String.replace(~r/`[^`]*`/s, "``")
408 + |> String.replace(~r{/\*.*?\*/}s, "")
409 + |> String.replace(~r{//[^\n]*}, "")
410 + end
411 +
412 + defp viewport_manual_notice(igniter) do
413 + Igniter.add_notice(
414 + igniter,
415 + """
416 + ℹ️ Optional: pass the viewport in your LiveSocket connect params so
417 + PhoenixKit's responsive LiveViews (e.g. dashboards) load straight into
418 + the right layout tier:
419 +
420 + const liveSocket = new LiveSocket("/live", Socket, {
421 + params: () => ({_csrf_token: csrfToken, viewport_width: window.innerWidth}),
422 + ...
423 + })
424 + """
425 + )
426 + end
427 +
263 428 defp add_hooks_manual_notice(igniter) do
264 429 Igniter.add_warning(
265 430 igniter,
  @@ -529,7 +529,13 @@ defmodule PhoenixKit.Migrations.Postgres do
529 529 - Replaces unique index with partial index (slug-mode only, WHERE slug IS NOT NULL)
530 530 - Adds unique index on `(group_uuid, post_date, post_time)` for timestamp-mode posts
531 531
532 - ### V138 - CRM v1 interaction tracker ⚡ LATEST
532 + ### V139 - Dashboard `config` column ⚡ LATEST
533 + - Adds a JSONB `config` column (`NOT NULL DEFAULT '{}'`) to
534 + `phoenix_kit_dashboards` for per-dashboard presentation settings, read and
535 + written whole like `layout`. Backs the dashboards plugin module.
536 + - Idempotent (`ADD COLUMN IF NOT EXISTS`); rollback drops the column.
537 +
538 + ### V138 - CRM v1 interaction tracker
533 539 - Adds five `phoenix_kit_crm_*` tables for the CRM module's first data model:
534 540 `contacts` (profile + **optional** `user_uuid` login link, partial-unique so
535 541 it's 1:1 only among linked rows), `companies`, `company_memberships` (M:N
  @@ -1184,7 +1190,7 @@ defmodule PhoenixKit.Migrations.Postgres do
1184 1190 use Ecto.Migration
1185 1191
1186 1192 @initial_version 1
1187 - @current_version 138
1193 + @current_version 139
1188 1194 @default_prefix "public"
1189 1195
1190 1196 @doc false
  @@ -0,0 +1,37 @@
1 + defmodule PhoenixKit.Migrations.Postgres.V139 do
2 + @moduledoc """
3 + V139: Dashboard-level `config` for the dashboards plugin.
4 +
5 + Adds a JSONB `config` column to `phoenix_kit_dashboards` for per-dashboard
6 + presentation settings — currently the layout mode (`"grid"` flow vs `"free"`
7 + pixel placement) and pixel-mode zoom. Read/written whole, like `layout`.
8 +
9 + All statements are idempotent (IF NOT EXISTS), safe to re-run.
10 + """
11 +
12 + use Ecto.Migration
13 +
14 + def up(opts) do
15 + prefix = Map.get(opts, :prefix, "public")
16 + p = prefix_str(prefix)
17 +
18 + execute("""
19 + ALTER TABLE #{p}phoenix_kit_dashboards
20 + ADD COLUMN IF NOT EXISTS config JSONB NOT NULL DEFAULT '{}'
21 + """)
22 +
23 + execute("COMMENT ON TABLE #{p}phoenix_kit IS '139'")
24 + end
25 +
26 + def down(opts) do
27 + prefix = Map.get(opts, :prefix, "public")
28 + p = prefix_str(prefix)
29 +
30 + execute("ALTER TABLE #{p}phoenix_kit_dashboards DROP COLUMN IF EXISTS config")
31 +
32 + execute("COMMENT ON TABLE #{p}phoenix_kit IS '138'")
33 + end
34 +
35 + defp prefix_str("public"), do: "public."
36 + defp prefix_str(prefix), do: "#{prefix}."
37 + end
Loading more files…