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
Current section
221 Versions
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…