Packages

phoenix_kit

1.7.109
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

32 files changed
+2406 additions
-464 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.108">>}.
3 + {<<"version">>,<<"1.7.109">>}.
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,
  @@ -56,6 +56,7 @@
56 56 <<"lib/modules/storage/storage.ex">>,
57 57 <<"lib/modules/storage/tessera_adapter.ex">>,
58 58 <<"lib/modules/storage/variant_naming.ex">>,
59 + <<"lib/modules/storage/etcher_adapter.ex">>,
59 60 <<"lib/modules/storage/README.md">>,<<"lib/modules/storage/workers">>,
60 61 <<"lib/modules/storage/workers/process_file_job.ex">>,
61 62 <<"lib/modules/storage/workers/delete_orphaned_file_job.ex">>,
  @@ -209,6 +210,7 @@
209 210 <<"lib/phoenix_kit_web/components/dashboard/badge.ex">>,
210 211 <<"lib/phoenix_kit_web/components/dashboard/context_selector.ex">>,
211 212 <<"lib/phoenix_kit_web/components/user_dashboard_nav.ex">>,
213 + <<"lib/phoenix_kit_web/components/annotation_composer.ex">>,
212 214 <<"lib/phoenix_kit_web/components/media_browser">>,
213 215 <<"lib/phoenix_kit_web/components/media_browser/embed.ex">>,
214 216 <<"lib/phoenix_kit_web/components/layout_wrapper.ex">>,
  @@ -376,6 +378,9 @@
376 378 <<"lib/phoenix_kit/aws/sesv2.ex">>,
377 379 <<"lib/phoenix_kit/aws/credentials_verifier.ex">>,
378 380 <<"lib/phoenix_kit/aws/infrastructure_setup.ex">>,
381 + <<"lib/phoenix_kit/annotations">>,
382 + <<"lib/phoenix_kit/annotations/annotations.ex">>,
383 + <<"lib/phoenix_kit/annotations/annotation.ex">>,
379 384 <<"lib/phoenix_kit/module_registry.ex">>,<<"lib/phoenix_kit/admin">>,
380 385 <<"lib/phoenix_kit/admin/presence.ex">>,
381 386 <<"lib/phoenix_kit/admin/simple_presence.ex">>,
  @@ -459,6 +464,7 @@
459 464 <<"lib/phoenix_kit/migrations/postgres/v107.ex">>,
460 465 <<"lib/phoenix_kit/migrations/postgres/v68.ex">>,
461 466 <<"lib/phoenix_kit/migrations/postgres/v37.ex">>,
467 + <<"lib/phoenix_kit/migrations/postgres/v114.ex">>,
462 468 <<"lib/phoenix_kit/migrations/postgres/v54.ex">>,
463 469 <<"lib/phoenix_kit/migrations/postgres/v24.ex">>,
464 470 <<"lib/phoenix_kit/migrations/postgres/v33.ex">>,
  @@ -468,6 +474,7 @@
468 474 <<"lib/phoenix_kit/migrations/postgres/v42.ex">>,
469 475 <<"lib/phoenix_kit/migrations/postgres/v32.ex">>,
470 476 <<"lib/phoenix_kit/migrations/postgres/v25.ex">>,
477 + <<"lib/phoenix_kit/migrations/postgres/v115.ex">>,
471 478 <<"lib/phoenix_kit/migrations/postgres/v55.ex">>,
472 479 <<"lib/phoenix_kit/migrations/postgres/v78.ex">>,
473 480 <<"lib/phoenix_kit/migrations/postgres/v27.ex">>,
  @@ -483,6 +490,7 @@
483 490 <<"lib/phoenix_kit/migrations/postgres/v101.ex">>,
484 491 <<"lib/phoenix_kit/migrations/postgres/v56.ex">>,
485 492 <<"lib/phoenix_kit/migrations/postgres/v09.ex">>,
493 + <<"lib/phoenix_kit/migrations/postgres/v116.ex">>,
486 494 <<"lib/phoenix_kit/migrations/postgres/v26.ex">>,
487 495 <<"lib/phoenix_kit/migrations/postgres/v79.ex">>,
488 496 <<"lib/phoenix_kit/migrations/postgres/v38.ex">>,
  @@ -709,9 +717,19 @@
709 717 {<<"optional">>,false},
710 718 {<<"requirement">>,<<"~> 0.2.11">>},
711 719 {<<"repository">>,<<"hexpm">>}],
720 + [{<<"name">>,<<"fresco">>},
721 + {<<"app">>,<<"fresco">>},
722 + {<<"optional">>,false},
723 + {<<"requirement">>,<<"~> 0.1">>},
724 + {<<"repository">>,<<"hexpm">>}],
712 725 [{<<"name">>,<<"tessera">>},
713 726 {<<"app">>,<<"tessera">>},
714 727 {<<"optional">>,false},
728 + {<<"requirement">>,<<"~> 0.2">>},
729 + {<<"repository">>,<<"hexpm">>}],
730 + [{<<"name">>,<<"etcher">>},
731 + {<<"app">>,<<"etcher">>},
732 + {<<"optional">>,false},
715 733 {<<"requirement">>,<<"~> 0.1">>},
716 734 {<<"repository">>,<<"hexpm">>}],
717 735 [{<<"name">>,<<"aws_regions">>},
  @@ -0,0 +1,82 @@
1 + defmodule PhoenixKit.Modules.Storage.EtcherAdapter do
2 + @moduledoc """
3 + `Etcher.Storage` adapter that writes annotation events from the
4 + MediaBrowser's Fresco viewer into `phoenix_kit_annotations`.
5 +
6 + Etcher's generic API is keyed by `target_type` + `target_uuid` so the
7 + library can annotate any kind of resource. In PhoenixKit the only
8 + target is a media File, so this adapter requires `target_type ==
9 + "file"` and maps `target_uuid` to `file_uuid`.
10 +
11 + ## Comment threads
12 +
13 + An annotation's discussion thread is **not** created at draw time —
14 + it's instantiated lazily when the user posts the first comment on the
15 + annotation. The comments are anchored to the **file**
16 + (`resource_type = "file"`, `resource_uuid = file_uuid`) with
17 + `metadata.annotation_uuid` carrying the back-reference, so they
18 + appear in the file's main thread alongside non-annotated discussion.
19 + No `comment_uuid` column on annotations is needed.
20 + """
21 +
22 + @behaviour Etcher.Storage
23 +
24 + alias PhoenixKit.Annotations
25 + alias PhoenixKit.Annotations.Annotation
26 +
27 + # Whitelist of annotation schema fields the adapter accepts from event
28 + # payloads, sourced from `Annotation.adapter_writable_fields/0` so the
29 + # set stays in sync with the schema's `@cast_fields`. Anything else
30 + # (Etcher routing keys, JS-side anchor coords, client-side metadata)
31 + # is silently dropped — `String.to_existing_atom` on unknown payload
32 + # keys used to crash the LV when Etcher's payload shape grew new
33 + # client-side keys like `anchor_x` / `anchor_y`. Stored as strings
34 + # here since the filter compares against `to_string(payload_key)`.
35 + @schema_keys Enum.map(Annotation.adapter_writable_fields(), &Atom.to_string/1)
36 +
37 + @impl Etcher.Storage
38 + def create(attrs) do
39 + with {:ok, file_uuid} <- target_uuid(attrs) do
40 + attrs
41 + |> filter_to_schema()
42 + |> Map.put(:file_uuid, file_uuid)
43 + |> Annotations.create()
44 + end
45 + end
46 +
47 + @impl Etcher.Storage
48 + def list_for(target_type, target_uuid)
49 +
50 + def list_for("file", file_uuid) when is_binary(file_uuid) do
51 + Annotations.list_for_file(file_uuid)
52 + end
53 +
54 + def list_for(_other, _uuid), do: []
55 +
56 + @impl Etcher.Storage
57 + def update(uuid, attrs) do
58 + attrs
59 + |> filter_to_schema()
60 + |> then(&Annotations.update(uuid, &1))
61 + end
62 +
63 + @impl Etcher.Storage
64 + def delete(uuid), do: Annotations.delete(uuid)
65 +
66 + # ---------------------------------------------------------------------------
67 +
68 + defp target_uuid(%{"target_type" => "file", "target_uuid" => uuid}) when is_binary(uuid),
69 + do: {:ok, uuid}
70 +
71 + defp target_uuid(%{target_type: "file", target_uuid: uuid}) when is_binary(uuid),
72 + do: {:ok, uuid}
73 +
74 + defp target_uuid(_attrs), do: {:error, :unsupported_target}
75 +
76 + defp filter_to_schema(attrs) do
77 + Enum.reduce(attrs, %{}, fn {k, v}, acc ->
78 + key = to_string(k)
79 + if key in @schema_keys, do: Map.put(acc, String.to_existing_atom(key), v), else: acc
80 + end)
81 + end
82 + end
  @@ -43,16 +43,19 @@ defmodule PhoenixKit.Modules.Storage.VariantGenerator do
43 43
44 44 ## Options
45 45
46 - - `:async` - Whether to generate variants asynchronously (default: true)
47 46 - `:dimensions` - List of specific dimensions to generate (default: all enabled)
48 47
48 + Variant generation is always synchronous from the caller's perspective.
49 + Internally `process_variants/2` parallelizes the per-dimension work via
50 + `Task.await_many/2` with a 10-minute cap. Callers that need fire-and-forget
51 + semantics should enqueue the work via Oban (see `process_file_job.ex`).
52 +
49 53 ## Returns
50 54
51 55 - `{:ok, variants}` - List of generated file instances
52 56 - `{:error, reason}` - Error if generation fails
53 57 """
54 58 def generate_variants(file, opts \\ []) do
55 - async = Keyword.get(opts, :async, true)
56 59 specific_dimensions = Keyword.get(opts, :dimensions, [])
57 60
58 61 if should_generate_variants?(file) do
  @@ -60,23 +63,13 @@ defmodule PhoenixKit.Modules.Storage.VariantGenerator do
60 63
61 64 case dimensions do
62 65 [] -> {:ok, []}
63 - _ -> run_variant_processing(file, dimensions, async)
66 + _ -> process_variants(file, dimensions)
64 67 end
65 68 else
66 69 {:ok, []}
67 70 end
68 71 end
69 72
70 - defp run_variant_processing(file, dimensions, true) do
71 - task = Task.async(fn -> process_variants(file, dimensions) end)
72 - # 30 second timeout
73 - Task.await(task, 30_000)
74 - end
75 -
76 - defp run_variant_processing(file, dimensions, false) do
77 - process_variants(file, dimensions)
78 - end
79 -
80 73 @doc """
81 74 Generates a specific variant for a file.
  @@ -0,0 +1,84 @@
1 + defmodule PhoenixKit.Annotations.Annotation do
2 + @moduledoc """
3 + Ecto schema for `phoenix_kit_annotations`.
4 +
5 + Stores user-drawn shapes (rectangle, circle, polygon, freehand) tied to
6 + a `PhoenixKit.Modules.Storage.File` via `file_uuid`. All geometry is
7 + in image-pixel coordinates; Fresco's coordinate adapter rescales for
8 + pan/zoom at render time.
9 +
10 + ## Comment thread linkage
11 +
12 + An annotation's discussion lives in `phoenix_kit_comments` anchored to
13 + the **file** (`resource_type = "file"`, `resource_uuid = file_uuid`)
14 + with `metadata.annotation_uuid` carrying the back-reference. This lets
15 + annotation-rooted comments appear in the file's main comments thread
16 + alongside non-annotated discussion. There is no `comment_uuid` column
17 + on annotations — the relationship is one-directional from the comment
18 + side, and a thread is created lazily when the first comment is posted.
19 + """
20 + use Ecto.Schema
21 + import Ecto.Changeset
22 +
23 + @primary_key {:uuid, UUIDv7, autogenerate: true}
24 + @foreign_key_type UUIDv7
25 +
26 + @kinds ~w(rectangle circle polygon freehand)
27 +
28 + @type t :: %__MODULE__{
29 + uuid: UUIDv7.t() | nil,
30 + file_uuid: UUIDv7.t(),
31 + creator_uuid: UUIDv7.t() | nil,
32 + kind: String.t(),
33 + geometry: map(),
34 + style: map() | nil,
35 + metadata: map() | nil,
36 + position: integer(),
37 + inserted_at: DateTime.t() | nil,
38 + updated_at: DateTime.t() | nil
39 + }
40 +
41 + schema "phoenix_kit_annotations" do
42 + field :file_uuid, UUIDv7
43 + field :creator_uuid, UUIDv7
44 + field :kind, :string
45 + field :geometry, :map
46 + field :style, :map
47 + field :metadata, :map
48 + field :position, :integer, default: 0
49 +
50 + timestamps(type: :utc_datetime)
51 + end
52 +
53 + @cast_fields ~w(file_uuid creator_uuid kind geometry style metadata position)a
54 + @required_fields ~w(file_uuid kind geometry)a
55 +
56 + # Fields the storage adapter is allowed to accept from event payloads.
57 + # `file_uuid` is set server-side from `target_uuid`, not by the client,
58 + # so it's excluded here — the adapter's `create/1` puts it on the
59 + # changeset after the whitelist filter.
60 + @adapter_writable_fields @cast_fields -- [:file_uuid]
61 +
62 + @doc false
63 + def changeset(annotation, attrs) do
64 + annotation
65 + |> cast(attrs, @cast_fields)
66 + |> validate_required(@required_fields)
67 + |> validate_inclusion(:kind, @kinds)
68 + |> foreign_key_constraint(:file_uuid)
69 + |> foreign_key_constraint(:creator_uuid)
70 + |> check_constraint(:kind, name: :phoenix_kit_annotations_kind_check)
71 + end
72 +
73 + @doc "List of allowed kind strings."
74 + def kinds, do: @kinds
75 +
76 + @doc """
77 + Fields the Etcher storage adapter is allowed to take from event
78 + payloads. Single source of truth so the adapter's whitelist doesn't
79 + drift from the schema's `@cast_fields`. `file_uuid` is excluded —
80 + the adapter sets it server-side from the Etcher `target_uuid`.
81 + """
82 + @spec adapter_writable_fields() :: [atom()]
83 + def adapter_writable_fields, do: @adapter_writable_fields
84 + end
Loading more files…