Packages

phoenix_kit

1.7.128
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

59 files changed
+1957 additions
-42 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.126">>}.
3 + {<<"version">>,<<"1.7.128">>}.
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,
  @@ -133,6 +133,7 @@
133 133 <<"lib/phoenix_kit_web/users/oauth.ex">>,
134 134 <<"lib/phoenix_kit_web/users/forgot_password.ex">>,
135 135 <<"lib/phoenix_kit_web/users/auth.ex">>,
136 + <<"lib/phoenix_kit_web/users/multi_session.ex">>,
136 137 <<"lib/phoenix_kit_web/users/confirmation.html.heex">>,
137 138 <<"lib/phoenix_kit_web/users/confirmation_instructions.html.heex">>,
138 139 <<"lib/phoenix_kit_web/users/magic_link.html.heex">>,
  @@ -286,6 +287,9 @@
286 287 <<"lib/phoenix_kit_web/live/modules/jobs/index.html.heex">>,
287 288 <<"lib/phoenix_kit_web/live/modules/jobs/index.ex">>,
288 289 <<"lib/phoenix_kit_web/live/modules/languages.ex">>,
290 + <<"lib/phoenix_kit_web/live/modules/notifications">>,
291 + <<"lib/phoenix_kit_web/live/modules/notifications/index.html.heex">>,
292 + <<"lib/phoenix_kit_web/live/modules/notifications/index.ex">>,
289 293 <<"lib/phoenix_kit_web/live/modules/languages.html.heex">>,
290 294 <<"lib/phoenix_kit_web/live/settings.ex">>,
291 295 <<"lib/phoenix_kit_web/live/dashboard.html.heex">>,
  @@ -314,6 +318,7 @@
314 318 <<"lib/phoenix_kit_web/projects_gettext_manifest.ex">>,
315 319 <<"lib/phoenix_kit_web/controllers">>,
316 320 <<"lib/phoenix_kit_web/controllers/error_json.ex">>,
321 + <<"lib/phoenix_kit_web/controllers/pdf_viewer_controller.ex">>,
317 322 <<"lib/phoenix_kit_web/controllers/error_html.ex">>,
318 323 <<"lib/phoenix_kit_web/controllers/file_controller.ex">>,
319 324 <<"lib/phoenix_kit_web/controllers/upload_controller.ex">>,
  @@ -486,6 +491,7 @@
486 491 <<"lib/phoenix_kit/migrations/postgres/v18.ex">>,
487 492 <<"lib/phoenix_kit/migrations/postgres/v47.ex">>,
488 493 <<"lib/phoenix_kit/migrations/postgres/v107.ex">>,
494 + <<"lib/phoenix_kit/migrations/postgres/v128.ex">>,
489 495 <<"lib/phoenix_kit/migrations/postgres/v68.ex">>,
490 496 <<"lib/phoenix_kit/migrations/postgres/v37.ex">>,
491 497 <<"lib/phoenix_kit/migrations/postgres/v114.ex">>,
  @@ -521,6 +527,7 @@
521 527 <<"lib/phoenix_kit/migrations/postgres/v38.ex">>,
522 528 <<"lib/phoenix_kit/migrations/postgres/v67.ex">>,
523 529 <<"lib/phoenix_kit/migrations/postgres/v90.ex">>,
530 + <<"lib/phoenix_kit/migrations/postgres/v127.ex">>,
524 531 <<"lib/phoenix_kit/migrations/postgres/v108.ex">>,
525 532 <<"lib/phoenix_kit/migrations/postgres/v48.ex">>,
526 533 <<"lib/phoenix_kit/migrations/postgres/v17.ex">>,
  @@ -535,6 +542,7 @@
535 542 <<"lib/phoenix_kit/migrations/postgres/v91.ex">>,
536 543 <<"lib/phoenix_kit/migrations/postgres/v66.ex">>,
537 544 <<"lib/phoenix_kit/migrations/postgres/v39.ex">>,
545 + <<"lib/phoenix_kit/migrations/postgres/v126.ex">>,
538 546 <<"lib/phoenix_kit/migrations/postgres/v14.ex">>,
539 547 <<"lib/phoenix_kit/migrations/postgres/v124.ex">>,
540 548 <<"lib/phoenix_kit/migrations/postgres/v64.ex">>,
  @@ -529,7 +529,35 @@ 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 - ### V125 - Project workflow statuses (entities-backed, cement-at-start) ⚡ LATEST
532 + ### V128 - Assignee on projects (and sub-projects) ⚡ LATEST
533 + - Adds `assigned_team_uuid` / `assigned_department_uuid` / `assigned_person_uuid`
534 + (FKs to the staff tables, `ON DELETE SET NULL`) to `phoenix_kit_projects`,
535 + with a `num_nonnulls(...) <= 1` single-assignee CHECK + a partial index per
536 + FK. Lets a whole project — or, since a sub-project is a project (V127), a
537 + sub-projectbe assigned to a Department/Team/Person like a task.
538 +
539 + ### V127 - Sub-projects as tasks
540 + - Adds `child_project_uuid` (FK `phoenix_kit_projects(uuid) ON DELETE RESTRICT`)
541 + to `phoenix_kit_project_assignments` — a sub-project is an assignment that
542 + points at a child project instead of a task template, so it lives in the
543 + parent's task timeline with dependencies + drag-reorder for free.
544 + - Drops `NOT NULL` on `task_uuid`; adds a `CHECK` that exactly one of
545 + `task_uuid` / `child_project_uuid` is set (XOR).
546 + - Partial UNIQUE index on `(child_project_uuid) WHERE NOT NULL` (a project is
547 + a child of at most one parent); it also serves child-link lookups, since an
548 + equality predicate implies `IS NOT NULL`.
549 +
550 + ### V126 - Standalone notifications
551 + - Drops NOT NULL on `phoenix_kit_notifications.activity_uuid` so a
552 + notification can exist without an originating activity (the unique
553 + `(activity_uuid, recipient_uuid)` index still holds — NULLs are
554 + distinct in Postgres).
555 + - Adds `metadata JSONB NOT NULL DEFAULT '{}'` so a standalone
556 + notification carries its own display content (`Render` reads the same
557 + `notification_text` / `notification_icon` / `notification_link` keys
558 + it honors on activity metadata).
559 +
560 + ### V125 - Project workflow statuses (entities-backed, cement-at-start)
533 561 - Adds `status_entity_uuid` (FK `phoenix_kit_entities(uuid) ON DELETE SET NULL`),
534 562 `current_status_slug`, and a generic `settings` JSONB (first key:
535 563 `use_status_translations`) to `phoenix_kit_projects` — the catalog list a
  @@ -1079,7 +1107,7 @@ defmodule PhoenixKit.Migrations.Postgres do
1079 1107 use Ecto.Migration
1080 1108
1081 1109 @initial_version 1
1082 - @current_version 125
1110 + @current_version 128
1083 1111 @default_prefix "public"
1084 1112
1085 1113 @doc false
  @@ -0,0 +1,55 @@
1 + defmodule PhoenixKit.Migrations.Postgres.V126 do
2 + @moduledoc """
3 + V126: Allow standalone notifications.
4 +
5 + Until now every `phoenix_kit_notifications` row had to point at an
6 + `activity_uuid` (NOT NULL) — a notification was strictly a per-user
7 + projection of an activity. This relaxes that so a notification can
8 + exist on its own:
9 +
10 + 1. Drops NOT NULL on `activity_uuid`. The unique
11 + `(activity_uuid, recipient_uuid)` index keeps working — Postgres
12 + treats NULLs as distinct by default, so a recipient can hold many
13 + standalone notifications.
14 + 2. Adds `metadata JSONB NOT NULL DEFAULT '{}'` so a standalone
15 + notification carries its own display content. `Render` reads the
16 + same `notification_text` / `notification_icon` / `notification_link`
17 + keys it already honors on activity metadata.
18 +
19 + Idempotent: `DROP NOT NULL` is a no-op when already nullable and the
20 + column add is `IF NOT EXISTS`.
21 + """
22 +
23 + use Ecto.Migration
24 +
25 + def up(opts) do
26 + prefix = Map.get(opts, :prefix, "public")
27 + p = prefix_str(prefix)
28 +
29 + execute("ALTER TABLE #{p}phoenix_kit_notifications ALTER COLUMN activity_uuid DROP NOT NULL")
30 +
31 + execute(
32 + "ALTER TABLE #{p}phoenix_kit_notifications ADD COLUMN IF NOT EXISTS metadata JSONB NOT NULL DEFAULT '{}'::jsonb"
33 + )
34 +
35 + execute("COMMENT ON TABLE #{p}phoenix_kit IS '126'")
36 + end
37 +
38 + def down(opts) do
39 + prefix = Map.get(opts, :prefix, "public")
40 + p = prefix_str(prefix)
41 +
42 + execute("ALTER TABLE #{p}phoenix_kit_notifications DROP COLUMN IF EXISTS metadata")
43 +
44 + # Reverting the feature means activity-less notifications can no longer
45 + # exist — drop them before restoring NOT NULL so the constraint holds.
46 + execute("DELETE FROM #{p}phoenix_kit_notifications WHERE activity_uuid IS NULL")
47 +
48 + execute("ALTER TABLE #{p}phoenix_kit_notifications ALTER COLUMN activity_uuid SET NOT NULL")
49 +
50 + execute("COMMENT ON TABLE #{p}phoenix_kit IS '125'")
51 + end
52 +
53 + defp prefix_str("public"), do: "public."
54 + defp prefix_str(prefix), do: "#{prefix}."
55 + end
  @@ -0,0 +1,191 @@
1 + defmodule PhoenixKit.Migrations.Postgres.V127 do
2 + @moduledoc """
3 + V127: Sub-projects as tasks (`phoenix_kit_project_assignments.child_project_uuid`).
4 +
5 + Lets a project be embedded inside another project as one of its task rows.
6 + A sub-project is an `Assignment` that points at a child `Project` instead of
7 + a reusable `Task` template — so it lives in the parent's task timeline and
8 + gets dependencies + drag-reorder for free (both are already assignment-level
9 + and project-scoped). The child project is the single source of truth; the
10 + parent's linking assignment carries denormalized rollup fields (status /
11 + progress_pct / estimated_duration / completed_at) synced by the context layer
12 + whenever the child changes, so every existing read site (schedule math,
13 + `recompute_project_completion`, dashboards, sorting) keeps working unchanged.
14 +
15 + Changes to `phoenix_kit_project_assignments`:
16 +
17 + * `child_project_uuid UUID` → FK `phoenix_kit_projects(uuid) ON DELETE RESTRICT`.
18 + `RESTRICT` (not `CASCADE`) so a stray child-project delete fails loudly
19 + instead of silently mutating the parent's task list — recursive teardown
20 + is orchestrated explicitly in `PhoenixKitProjects.Projects` inside a
21 + transaction so it can log activity and tear the subtree down in order.
22 + * `task_uuid` loses its `NOT NULL` — a sub-project assignment has no template.
23 + * `CHECK ((task_uuid IS NOT NULL) <> (child_project_uuid IS NOT NULL))` —
24 + exactly one of the two is set (XOR). Existing rows (task set, child NULL)
25 + satisfy it, so the constraint validates against current data without a
26 + backfill.
27 + * Partial UNIQUE index on `(child_project_uuid) WHERE child_project_uuid IS
28 + NOT NULL` — a project is a child of at most one parent assignment. This is
29 + also what forces template cloning to *deep-clone* child subtrees rather
30 + than point two parents at the same child. It also serves the "find the
31 + linking row for this child" lookups (parent breadcrumb, rollup sync): an
32 + equality predicate `child_project_uuid = $1` implies `IS NOT NULL`, so
33 + Postgres uses the partial index for it — no separate plain index needed.
34 +
35 + Idempotent: re-running is a no-op once the column/constraints/indexes exist.
36 + """
37 +
38 + use Ecto.Migration
39 +
40 + def up(opts) do
41 + prefix = Map.get(opts, :prefix, "public")
42 + p = prefix_str(prefix)
43 + schema = schema_for(prefix)
44 +
45 + add_child_project_uuid_column(p, schema)
46 + add_child_project_uuid_fk(p, schema)
47 + drop_task_uuid_not_null(p)
48 + add_task_xor_child_check(p, schema)
49 + create_child_project_unique_index(p, schema)
50 +
51 + execute("COMMENT ON TABLE #{p}phoenix_kit IS '127'")
52 + end
53 +
54 + def down(opts) do
55 + prefix = Map.get(opts, :prefix, "public")
56 + p = prefix_str(prefix)
57 +
58 + execute("DROP INDEX IF EXISTS #{p}phoenix_kit_project_assignments_child_project_unique")
59 +
60 + execute("""
61 + ALTER TABLE #{p}phoenix_kit_project_assignments
62 + DROP CONSTRAINT IF EXISTS phoenix_kit_project_assignments_task_xor_child
63 + """)
64 +
65 + execute("""
66 + ALTER TABLE #{p}phoenix_kit_project_assignments
67 + DROP CONSTRAINT IF EXISTS phoenix_kit_project_assignments_child_project_uuid_fkey
68 + """)
69 +
70 + # Drop the sub-project linking rows first — they're exactly the rows that
71 + # would violate the NOT NULL we restore below. Target them directly by
72 + # `child_project_uuid IS NOT NULL` (the column still exists here, dropped
73 + # just after): that's precisely the V127-created set, without leaning on
74 + # the XOR check we dropped two statements ago. The child projects survive
75 + # as standalone projects, but note this also cascade-deletes any
76 + # `phoenix_kit_project_dependencies` edges touching these assignments
77 + # (FK `ON DELETE CASCADE`), so a sub-project's dependency wiring is lost —
78 + # rollback is feature-removal, not a reversible round-trip.
79 + execute(
80 + "DELETE FROM #{p}phoenix_kit_project_assignments WHERE child_project_uuid IS NOT NULL"
81 + )
82 +
83 + execute(
84 + "ALTER TABLE #{p}phoenix_kit_project_assignments DROP COLUMN IF EXISTS child_project_uuid"
85 + )
86 +
87 + # Restore the pre-V127 NOT NULL on task_uuid. Safe now: the XOR check is
88 + # gone and the only rows that could have had a NULL task_uuid (the child
89 + # links) were just deleted, so every remaining row is a plain
90 + # template-backed assignment with task_uuid set.
91 + execute("ALTER TABLE #{p}phoenix_kit_project_assignments ALTER COLUMN task_uuid SET NOT NULL")
92 +
93 + execute("COMMENT ON TABLE #{p}phoenix_kit IS '126'")
94 + end
95 +
96 + defp add_child_project_uuid_column(p, schema) do
97 + execute("""
98 + DO $$
99 + BEGIN
100 + IF NOT EXISTS (
101 + SELECT FROM information_schema.columns
102 + WHERE table_schema = '#{schema}'
103 + AND table_name = 'phoenix_kit_project_assignments'
104 + AND column_name = 'child_project_uuid'
105 + ) THEN
106 + ALTER TABLE #{p}phoenix_kit_project_assignments
107 + ADD COLUMN child_project_uuid UUID;
108 + END IF;
109 + END $$;
110 + """)
111 + end
112 +
113 + # FK to the embedded child project. `ON DELETE RESTRICT` keeps the parent's
114 + # task list honest: you can't delete a project that is still embedded as a
115 + # sub-project — the context tears the subtree down explicitly instead.
116 + # Postgres has no `ADD CONSTRAINT IF NOT EXISTS`, so guard on the name.
117 + defp add_child_project_uuid_fk(p, schema) do
118 + execute("""
119 + DO $$
120 + BEGIN
121 + IF NOT EXISTS (
122 + SELECT FROM information_schema.table_constraints
123 + WHERE table_schema = '#{schema}'
124 + AND table_name = 'phoenix_kit_project_assignments'
125 + AND constraint_name = 'phoenix_kit_project_assignments_child_project_uuid_fkey'
126 + ) THEN
127 + ALTER TABLE #{p}phoenix_kit_project_assignments
128 + ADD CONSTRAINT phoenix_kit_project_assignments_child_project_uuid_fkey
129 + FOREIGN KEY (child_project_uuid)
130 + REFERENCES #{p}phoenix_kit_projects(uuid)
131 + ON DELETE RESTRICT;
132 + END IF;
133 + END $$;
134 + """)
135 + end
136 +
137 + # A sub-project assignment has no task template, so task_uuid must be
138 + # nullable. `DROP NOT NULL` is itself idempotent (a no-op when already
139 + # nullable), so no existence guard is needed.
140 + defp drop_task_uuid_not_null(p) do
141 + execute(
142 + "ALTER TABLE #{p}phoenix_kit_project_assignments ALTER COLUMN task_uuid DROP NOT NULL"
143 + )
144 + end
145 +
146 + # Exactly one of task_uuid / child_project_uuid is set. `<>` is boolean XOR
147 + # in Postgres. Existing rows (task set, child NULL) already satisfy it.
148 + defp add_task_xor_child_check(p, schema) do
149 + execute("""
150 + DO $$
151 + BEGIN
152 + IF NOT EXISTS (
153 + SELECT FROM information_schema.table_constraints
154 + WHERE table_schema = '#{schema}'
155 + AND table_name = 'phoenix_kit_project_assignments'
156 + AND constraint_name = 'phoenix_kit_project_assignments_task_xor_child'
157 + ) THEN
158 + ALTER TABLE #{p}phoenix_kit_project_assignments
159 + ADD CONSTRAINT phoenix_kit_project_assignments_task_xor_child
160 + CHECK ((task_uuid IS NOT NULL) <> (child_project_uuid IS NOT NULL));
161 + END IF;
162 + END $$;
163 + """)
164 + end
165 +
166 + # A project can be embedded as a sub-project in at most one parent. Partial
167 + # so the common task-backed rows (child NULL) don't collide.
168 + defp create_child_project_unique_index(p, schema) do
169 + execute("""
170 + DO $$
171 + BEGIN
172 + IF NOT EXISTS (
173 + SELECT FROM pg_indexes
174 + WHERE schemaname = '#{schema}'
175 + AND tablename = 'phoenix_kit_project_assignments'
176 + AND indexname = 'phoenix_kit_project_assignments_child_project_unique'
177 + ) THEN
178 + CREATE UNIQUE INDEX phoenix_kit_project_assignments_child_project_unique
179 + ON #{p}phoenix_kit_project_assignments (child_project_uuid)
180 + WHERE child_project_uuid IS NOT NULL;
181 + END IF;
182 + END $$;
183 + """)
184 + end
185 +
186 + defp schema_for("public"), do: "public"
187 + defp schema_for(prefix), do: prefix
188 +
189 + defp prefix_str("public"), do: "public."
190 + defp prefix_str(prefix), do: "#{prefix}."
191 + end
Loading more files…