trek 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.slim-lint.yml +0 -3
  3. data/AGENTS.md +8 -1
  4. data/CHANGELOG.md +75 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +20 -3
  7. data/Makefile +11 -9
  8. data/README.md +1 -1
  9. data/app/assets/icons/trek/audit.svg +1 -0
  10. data/app/assets/icons/trek/chevron-down.svg +1 -0
  11. data/app/assets/icons/trek/chevron-up.svg +1 -0
  12. data/app/assets/icons/trek/external.svg +1 -0
  13. data/app/assets/icons/trek/print.svg +1 -0
  14. data/app/assets/icons/trek/theme-auto.svg +1 -0
  15. data/app/assets/icons/trek/theme-dark.svg +1 -0
  16. data/app/assets/icons/trek/theme-light.svg +1 -0
  17. data/app/assets/stylesheets/trek/base.css +13 -0
  18. data/app/components/trek/button_component/button_component.css +9 -0
  19. data/app/components/trek/button_component.rb +13 -1
  20. data/app/components/trek/dashboard/analytics_component/analytics_component.html.slim +2 -0
  21. data/app/components/trek/dashboard/analytics_component.rb +1 -0
  22. data/app/components/trek/dashboard/chart_component/chart_component.css +192 -0
  23. data/app/components/trek/dashboard/chart_component/chart_component.html.slim +6 -0
  24. data/app/components/trek/dashboard/chart_component/chart_component.js +1 -0
  25. data/app/components/trek/dashboard/chart_component.rb +27 -0
  26. data/app/components/trek/dashboard/tile_component/tile_component.css +40 -0
  27. data/app/components/trek/dashboard/tile_component/tile_component.html.slim +14 -4
  28. data/app/components/trek/dashboard/tile_component.rb +20 -2
  29. data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
  30. data/app/components/trek/dialog_component/dialog_component.js +62 -2
  31. data/app/components/trek/dropdown_component/dropdown_component.css +66 -0
  32. data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
  33. data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
  34. data/app/components/trek/dropdown_component.rb +29 -0
  35. data/app/components/trek/form/actions_component/actions_component.css +6 -7
  36. data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +10 -12
  37. data/app/components/trek/form/content_editor_component/content_editor_component.js +4 -0
  38. data/app/components/trek/form/group_component/group_component.css +34 -0
  39. data/app/components/trek/header_component/header_component.css +10 -4
  40. data/app/components/trek/header_component/header_component.html.slim +17 -10
  41. data/app/components/trek/header_component/header_component.js +29 -5
  42. data/app/components/trek/header_component.rb +20 -3
  43. data/app/components/trek/layout_component/layout_component.css +20 -0
  44. data/app/components/trek/layout_component/layout_component.html.slim +16 -0
  45. data/app/components/trek/layout_component/layout_component.js +35 -6
  46. data/app/components/trek/menu_component/menu_component.css +6 -0
  47. data/app/components/trek/menu_component.rb +29 -4
  48. data/app/components/trek/toaster/toast_component.rb +1 -1
  49. data/app/controllers/concerns/trek/authorization.rb +12 -0
  50. data/app/formatters/prosemirror_to_html/global_id_to_links_formatter.rb +3 -1
  51. data/app/helpers/trek/page_path_helper.rb +4 -3
  52. data/app/helpers/trek/theme_helper.rb +16 -1
  53. data/app/models/concerns/trek/phonable.rb +15 -3
  54. data/app/models/concerns/trek/sectionable.rb +5 -0
  55. data/app/models/concerns/trek/users/validations.rb +1 -1
  56. data/app/models/prosemirror.rb +5 -2
  57. data/app/uploaders/audio_uploader.rb +11 -0
  58. data/app/uploaders/record_image_uploader.rb +3 -0
  59. data/app/views/trek/panels/images/_form.html.slim +1 -1
  60. data/app/views/trek/panels/images/create.turbo_stream.slim +3 -3
  61. data/app/views/trek/panels/images/new.html.slim +1 -1
  62. data/app/views/trek/panels/links/_form.html.slim +1 -1
  63. data/app/views/trek/panels/links/create.turbo_stream.slim +3 -3
  64. data/app/views/trek/panels/links/new.html.slim +1 -1
  65. data/app/views/trek/panels/prompts/_form.html.slim +1 -1
  66. data/app/views/trek/panels/prompts/create.turbo_stream.slim +3 -3
  67. data/app/views/trek/panels/prompts/new.html.slim +1 -1
  68. data/config/locales/trek.en.yml +3 -2
  69. data/config/locales/trek.fr.yml +1 -0
  70. data/docs/customize.md +18 -2
  71. data/docs/deploy.md +7 -2
  72. data/docs/getting-started.md +1 -1
  73. data/docs/index.md +1 -1
  74. data/docs/reference/components.md +15 -0
  75. data/docs/reference/concerns/phonable.md +27 -1
  76. data/docs/reference/environment-variables.md +2 -0
  77. data/docs/reference/forms.md +2 -0
  78. data/docs/reference/generators/install.md +4 -1
  79. data/docs/reference/icons.md +1 -1
  80. data/docs/reference/models/tag.md +1 -1
  81. data/docs/reference/models/user.md +4 -2
  82. data/docs/reference/uploaders.md +31 -6
  83. data/docs/what-is-trek.md +1 -1
  84. data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
  85. data/lib/generators/trek/install/appsignal_generator.rb +102 -0
  86. data/lib/generators/trek/install/model_specs_generator.rb +64 -0
  87. data/lib/generators/trek/install/postmark_generator.rb +3 -0
  88. data/lib/generators/trek/install/ruby_generator.rb +1 -1
  89. data/lib/generators/trek/install/scalingo_generator.rb +1 -1
  90. data/lib/generators/trek/install/uploaders_generator.rb +2 -0
  91. data/lib/generators/trek/install_generator.rb +5 -0
  92. data/lib/generators/trek/taxonomies_generator.rb +7 -0
  93. data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
  94. data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
  95. data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
  96. data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
  97. data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
  98. data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
  99. data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
  100. data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
  101. data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
  102. data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
  103. data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
  104. data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
  105. data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
  106. data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
  107. data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
  108. data/lib/generators/trek/templates/models/tag.rb +6 -1
  109. data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
  110. data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
  111. data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
  112. data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
  113. data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
  114. data/lib/generators/trek/templates/spec/models/user_spec.rb +76 -0
  115. data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
  116. data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
  117. data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
  118. data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
  119. data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
  120. data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
  121. data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
  122. data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
  123. data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
  124. data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
  125. data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +71 -0
  126. data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +45 -0
  127. data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -0
  128. data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
  129. data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
  130. data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +2 -2
  131. data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
  132. data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
  133. data/lib/tasks/trek/attachments.rake +18 -0
  134. data/lib/trek/cached_attachments.rb +53 -0
  135. data/lib/trek/saved_attachments_reload.rb +44 -0
  136. data/lib/trek/version.rb +1 -1
  137. data/lib/trek.rb +3 -0
  138. data/package.json +1 -1
  139. data/postcss.config.js +36 -0
  140. data/trek.gemspec +5 -0
  141. metadata +76 -3
  142. data/app/assets/icons/trek/theme.svg +0 -11
@@ -6,7 +6,7 @@ module Trek
6
6
  included do
7
7
  validates :password,
8
8
  length: { minimum: 8 },
9
- if: -> { new_record? || changes[:crypted_password] },
9
+ if: -> { new_record? || password_digest_changed? },
10
10
  allow_blank: true
11
11
  validates :email,
12
12
  presence: true,
@@ -10,9 +10,12 @@ class Prosemirror
10
10
  def format_content(parsed_content)
11
11
  return if parsed_content.blank?
12
12
 
13
- content = renderer.render(parsed_content)
13
+ # GlobalIDs must be resolved to URLs before rendering: the renderer's
14
+ # sanitizer strips href attributes with unknown protocols, so a gid://
15
+ # link would lose its href entirely.
16
+ content = ProsemirrorToHtml::GlobalIdToLinksFormatter.new(parsed_content.to_json).to_s
17
+ content = renderer.render(JSON.parse(content))
14
18
  content = ProsemirrorToHtml::RemoveEmptyParagraphsFormatter.new(content).to_s
15
- content = ProsemirrorToHtml::GlobalIdToLinksFormatter.new(content).to_s
16
19
  # rubocop:disable Rails/OutputSafety
17
20
  content.html_safe
18
21
  # rubocop:enable Rails/OutputSafety
@@ -0,0 +1,11 @@
1
+ class AudioUploader < Shrine
2
+ plugin :determine_mime_type
3
+ plugin :validation_helpers
4
+ # The admin's sound field accepts files up to 200 MB (`Trek::Form::SoundFieldComponent`), above
5
+ # the 20 MB the Shrine initializer allows images.
6
+ plugin :upload_endpoint, url: true, max_size: 200.megabytes
7
+
8
+ Attacher.validate do
9
+ validate_mime_type %w[audio/mpeg audio/mp4 audio/aac audio/ogg audio/wav audio/x-wav audio/webm audio/flac]
10
+ end
11
+ end
@@ -1,3 +1,6 @@
1
1
  class RecordImageUploader < ImageUploader
2
2
  plugin :activerecord
3
+ # Keeps a file attached during a save that reloads the record (closure_tree reorders siblings
4
+ # that way) pending, so it is still promoted out of the cache storage on commit.
5
+ plugin Trek::SavedAttachmentsReload
3
6
  end
@@ -24,7 +24,7 @@ h1.c-trek--layout-panel-title = t(".title")
24
24
  = render Trek::Form::ActionsComponent.new do |c|
25
25
  ruby:
26
26
  c.with_button(
27
- data: { action: "trek--layout#closePanel" },
27
+ data: { action: "trek--layout#closeStackedPanel" },
28
28
  text: t("admin.actions.cancel"),
29
29
  )
30
30
 
@@ -1,6 +1,6 @@
1
1
  - if @object.valid?
2
- = turbo_stream.append("trek--layout-panel")
3
- div(data-trek--layout-target="panelCloser")
2
+ = turbo_stream.append("trek--layout-stacked-panel")
3
+ div(data-trek--layout-target="stackedPanelCloser")
4
4
 
5
5
  = turbo_stream.append(@object.turbo_frame_id)
6
6
  div(
@@ -15,5 +15,5 @@
15
15
  text=@object.text
16
16
  )
17
17
  - else
18
- = turbo_stream.update("trek--layout-panel")
18
+ = turbo_stream.update("trek--layout-stacked-panel")
19
19
  = render "form"
@@ -1,2 +1,2 @@
1
- = turbo_frame_tag("trek--layout-panel")
1
+ = turbo_frame_tag("trek--layout-stacked-panel")
2
2
  = render "form"
@@ -28,7 +28,7 @@ h1.c-trek--layout-panel-title = t(".title")
28
28
  = render Trek::Form::ActionsComponent.new do |c|
29
29
  ruby:
30
30
  c.with_button(
31
- data: { action: "trek--layout#closePanel" },
31
+ data: { action: "trek--layout#closeStackedPanel" },
32
32
  text: t("admin.actions.cancel"),
33
33
  )
34
34
 
@@ -1,6 +1,6 @@
1
1
  - if @object.valid?
2
- = turbo_stream.append("trek--layout-panel")
3
- div(data-trek--layout-target="panelCloser")
2
+ = turbo_stream.append("trek--layout-stacked-panel")
3
+ div(data-trek--layout-target="stackedPanelCloser")
4
4
 
5
5
  = turbo_stream.append(@object.turbo_frame_id)
6
6
  div(
@@ -8,5 +8,5 @@
8
8
  data-params=@object.to_json
9
9
  )
10
10
  - else
11
- = turbo_stream.update("trek--layout-panel")
11
+ = turbo_stream.update("trek--layout-stacked-panel")
12
12
  = render "form"
@@ -1,2 +1,2 @@
1
- = turbo_frame_tag("trek--layout-panel")
1
+ = turbo_frame_tag("trek--layout-stacked-panel")
2
2
  = render "form"
@@ -26,7 +26,7 @@ h1.c-trek--layout-panel-title = t(".title")
26
26
  = render Trek::Form::ActionsComponent.new do |c|
27
27
  ruby:
28
28
  c.with_button(
29
- data: { action: "trek--layout#closePanel" },
29
+ data: { action: "trek--layout#closeStackedPanel" },
30
30
  text: t("admin.actions.cancel"),
31
31
  )
32
32
 
@@ -1,6 +1,6 @@
1
1
  - if @object.valid?
2
- = turbo_stream.append("trek--layout-panel")
3
- div(data-trek--layout-target="panelCloser")
2
+ = turbo_stream.append("trek--layout-stacked-panel")
3
+ div(data-trek--layout-target="stackedPanelCloser")
4
4
 
5
5
  = turbo_stream.append(@object.turbo_frame_id)
6
6
  div(
@@ -14,5 +14,5 @@
14
14
  title=@object.title
15
15
  )
16
16
  - else
17
- = turbo_stream.update("trek--layout-panel")
17
+ = turbo_stream.update("trek--layout-stacked-panel")
18
18
  = render "form"
@@ -1,2 +1,2 @@
1
- = turbo_frame_tag("trek--layout-panel")
1
+ = turbo_frame_tag("trek--layout-stacked-panel")
2
2
  = render "form"
@@ -69,8 +69,8 @@ en:
69
69
  self: the same tab
70
70
  prompt:
71
71
  targets:
72
- blank: un nouvel onglet
73
- self: le même onglet
72
+ blank: a new tab
73
+ self: the same tab
74
74
  contents: Contents
75
75
  dashboard: Dashboard
76
76
  dashboards:
@@ -264,3 +264,4 @@ en:
264
264
  notices:
265
265
  authentication_needed: Please log in
266
266
  rate_limited: Too many attempts, please try again later
267
+ unauthorized: You are not allowed to do that
@@ -273,3 +273,4 @@ fr:
273
273
  notices:
274
274
  authentication_needed: Veuillez vous connecter
275
275
  rate_limited: Trop de tentatives, veuillez réessayer plus tard
276
+ unauthorized: Vous n'êtes pas autorisé·e à faire cela
data/docs/customize.md CHANGED
@@ -27,7 +27,21 @@ Replace Trek's favicon with your own:
27
27
  Trek's UI components accept options where it matters:
28
28
 
29
29
  - **`Trek::Form::ActionsComponent`** — pick an `aspect` and add your own `classnames` to restyle form action bars.
30
- - **`Trek::DialogComponent`** — set the `title` and the buttons of modal dialogs.
30
+ - **`Trek::DialogComponent`** — set the `title` and the buttons of modal dialogs. The confirm dialog wired to `data-turbo-confirm` can also be customized per action from the confirming element's data attributes, with the delete wording as fallback:
31
+
32
+ ```slim
33
+ = render Trek::ButtonComponent.new(
34
+ text: "Run the prize draw",
35
+ to: admin_event_draw_path(event), method: :post,
36
+ data: {
37
+ turbo_confirm: "Run the prize draw now? Winners will be picked and emails sent.",
38
+ turbo_confirm_title: "Prize draw",
39
+ turbo_confirm_button: "Run the draw",
40
+ turbo_confirm_color: "grass",
41
+ },
42
+ )
43
+ ```
44
+
31
45
  - **`Trek::Form::ContentEditorComponent`** — toggle the `nodes`, `blocks` and `floating` options to slim down the editor where full rich text isn't needed:
32
46
 
33
47
  ```ruby
@@ -62,9 +76,11 @@ Each scaffolded resource gets its own policy in `app/policies/admin`. Adjust the
62
76
 
63
77
  Trek's admin supports light, dark and auto themes, selectable per user. The design tokens are based on [Radix colors](https://www.radix-ui.com/colors); override the CSS custom properties in your own stylesheet to adapt the palette.
64
78
 
79
+ The auto theme follows the system setting (`prefers-color-scheme`). The `autoDarkTheme` plugin in the generated `postcss.config.js` copies every rule targeting `.dark-theme` into a `@media (prefers-color-scheme: dark)` block, with `.auto-theme` in its place, so dark overrides in your own stylesheets (`.dark-theme { --accent-9: … }`, `.dark-theme .card { … }`) apply to the auto theme too.
80
+
65
81
  ## Dependencies configuration
66
82
 
67
- The installer configures Shrine, Postmark, Sorcery, ActionPolicy, Mobility and friends with sensible defaults — through initializers generated **in your app**. You can edit them at any time:
83
+ The installer configures Shrine, Postmark, ActionPolicy, Mobility and friends with sensible defaults — through initializers generated **in your app**. You can edit them at any time:
68
84
 
69
85
  - `config/initializers/shrine.rb` — storage backends, upload limits
70
86
  - `config/credentials.yml.enc` — Postmark API token (`EDITOR="code --wait" bin/rails credentials:edit`)
data/docs/deploy.md CHANGED
@@ -20,14 +20,19 @@ Generate the Scalingo files (an `Aptfile` and a `Procfile` with the web process
20
20
  rails g trek:install:scalingo
21
21
  ```
22
22
 
23
- Then configure the Scalingo app's environment, pinning the buildpack to a release tag:
23
+ Then configure the Scalingo app's environment, pinning the buildpack to a release through its archive:
24
24
 
25
25
  ```sh
26
- scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack#v0.1.0
26
+ scalingo --app <app> env-set BUILDPACK_URL=https://github.com/etaminstudio/trek-buildpack/archive/refs/tags/v0.1.0.tar.gz
27
27
  scalingo --app <app> env-set 'DATABASE_URL=$SCALINGO_POSTGRESQL_URL'
28
28
  scalingo --app <app> env-set RAILS_MASTER_KEY=$(cat config/master.key)
29
29
  ```
30
30
 
31
+ > Pin through the archive, not a `#tag` suffix: Scalingo reads `#ref` on `BUILDPACK_URL` as a **branch**, so
32
+ > `…/trek-buildpack#v0.1.0` asks for a branch named `v0.1.0`, which does not exist, and the build fails with
33
+ > `fail to download and extract the custom buildpack: Invalid status code: 404 Not Found`. The `.tar.gz` form above is
34
+ > downloaded as is and stays pinned to the release.
35
+
31
36
  Make sure `config/database.yml` reads `DATABASE_URL` in production — the section `rails new` generates targets a local socket with a password variable instead:
32
37
 
33
38
  ```yml
@@ -68,7 +68,7 @@ The installer will:
68
68
  - create the `Fragment` model and include Trek's concerns
69
69
  - create the relevant policies
70
70
  - generate and run the relevant migrations
71
- - set up authentication (Sorcery), authorization (ActionPolicy), uploads (Shrine), emails (Postmark), i18n, linting, CI and more
71
+ - set up authentication (`has_secure_password` with database-backed sessions), authorization (ActionPolicy), uploads (Shrine), emails (Postmark), i18n, linting, CI and more
72
72
 
73
73
  ### Configure Postmark (optional)
74
74
 
data/docs/index.md CHANGED
@@ -32,7 +32,7 @@ features:
32
32
  details: Content and admin UI translations powered by Mobility and i18n-tasks, with optional automatic translation via DeepL.
33
33
  - icon: 🔐
34
34
  title: Roles & policies built in
35
- details: Sorcery authentication and ActionPolicy authorization with admin, editor, user, reader and guest roles, enforced across every panel.
35
+ details: Authentication on Rails' own has_secure_password and ActionPolicy authorization with admin, editor, user, reader and guest roles, enforced across every panel.
36
36
  - icon: 🧩
37
37
  title: Hotwire-native UI
38
38
  details: ViewComponents, Stimulus and Turbo under the hood, with light & dark themes, Radix colors and a customizable design system.
@@ -22,6 +22,21 @@ The main building blocks, all in the `Trek::` namespace:
22
22
  | `HeadingComponent` | headings |
23
23
  | `GateComponent` | authorization-based visibility gate |
24
24
  | `IconComponent` | SVG [icon](/reference/icons) renderer |
25
+ | `Dashboard::SectionComponent` / `Dashboard::TileComponent` | dashboard sections of shortcut tiles |
26
+
27
+ ## Dashboard tiles
28
+
29
+ A tile is a link by default. With `method:`, it issues another HTTP method through Turbo (as a `data-turbo-method` link), so a tile can create a record in one click rather than open a form; `data:` adds attributes to the link, such as a confirmation:
30
+
31
+ ```ruby
32
+ s.with_tile(
33
+ text: "New weekly draft",
34
+ icon: "trek/add",
35
+ href: [:draft_current_week, :admin, :newsletters],
36
+ method: :post,
37
+ data: { turbo_confirm: "Create this week's draft?" }
38
+ )
39
+ ```
25
40
 
26
41
  ## Form components
27
42
 
@@ -13,11 +13,37 @@ end
13
13
 
14
14
  For each declared attribute:
15
15
 
16
- - a validation (`phone: { possible: true, allow_blank: true }`)
16
+ - a validation (`phone: { possible: true, allow_blank: true }`, see [Strictness](#strictness))
17
17
  - normalization to E.164 international format on save
18
18
  - `formatted_{attr}` — national format (`"01 23 45 67 89"`)
19
19
  - `{attr}_with_country_code` — E.164 (`"+33123456789"`)
20
20
 
21
+ ## Strictness
22
+
23
+ `strictness:` picks how hard Phonelib looks at a number. It applies to every attribute of the call.
24
+
25
+ | value | what is accepted |
26
+ | --- | --- |
27
+ | `:possible` (default) | the digits **could** form a number for the country — right length and shape |
28
+ | `:valid` | the digits fall in a range the regulator has actually **assigned** |
29
+
30
+ ```ruby
31
+ class Organization < ApplicationRecord
32
+ include Trek::Phonable
33
+ phonable_attributes :phone, strictness: :valid
34
+ end
35
+ ```
36
+
37
+ The gap between the two is worth knowing before leaving the default in place. With
38
+ `Phonelib.default_country = "FR"`, `"0012345678"` is *possible*: under `:possible` it is accepted and
39
+ normalized to `"+12345678"` — the stray leading zero read as an international prefix, the number
40
+ silently rehomed to country code +1, and the original input gone, since normalization happens on
41
+ save. Under `:valid` the same input is rejected and the person is asked to correct it.
42
+
43
+ `:possible` is the right default for a number stored as a way to reach someone, where a false
44
+ rejection is worse than a number that never gets dialled. Choose `:valid` when the application
45
+ intends to actually call or text what it collects.
46
+
21
47
  ## Usage
22
48
 
23
49
  ```ruby
@@ -4,6 +4,8 @@
4
4
  | --- | --- |
5
5
  | `ADMIN_EMAIL` / `ADMIN_PASSWORD` | credentials used by [`trek:admin:user`](/reference/generators/admin-user) |
6
6
  | `POSTMARK_API_TOKEN` | Postmark email delivery (auto-injected into credentials by the [installer](/reference/generators/install)) |
7
+ | `APPSIGNAL_PUSH_API_KEY` | AppSignal error tracking and performance monitoring (auto-injected into credentials by the [installer](/reference/generators/install)) |
8
+ | `APPSIGNAL_APP_ENV` | environment name reported to AppSignal (optional, defaults to `RAILS_ENV`) |
7
9
  | `DEEPL_AUTH_KEY` | automatic locale translation in the [scaffold generator](/reference/generators/scaffold) |
8
10
  | `FEATURE_STORAGE` | `local` (development) or `s3` (production) |
9
11
  | `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_BASE_URL` | S3 storage configuration for [uploads](/reference/uploaders) |
@@ -13,6 +13,8 @@ Trek ships `Trek::FormBuilder`, an extension of [`ViewComponent::Form`](https://
13
13
 
14
14
  Each helper renders the matching component from the [`Trek::Form` namespace](/reference/components#form-components).
15
15
 
16
+ Standard helpers are styled too: a plain `f.file_field` inside an `f.group` gets its native `::file-selector-button` styled like a narrow `Trek::ButtonComponent` (following the current `data-accent-color`), keeping the browser's built-in "file chosen" feedback with no JS — handy for one-off uploads like a spreadsheet import.
17
+
16
18
  ```slim
17
19
  = form_with model: [:admin, @book] do |f|
18
20
  = f.group :title
@@ -21,7 +21,7 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
21
21
 
22
22
  | Area | What gets set up |
23
23
  | --- | --- |
24
- | Authentication | Sorcery, the `UserSession` form model, login/logout & password reset panels |
24
+ | Authentication | bcrypt and `has_secure_password` on `User`, the `UserSession` model (one per signed-in browser, found through a signed `session_id` cookie), the `Trek::Authentication` controller concern, login/logout & password reset panels |
25
25
  | Authorization | ActionPolicy and the base policies |
26
26
  | Models | `User`, `Page`, `PagePath`, `PageVersion`, `Fragment`, `MenuNode`, `ExternalLink`, `Current` — with migrations |
27
27
  | Attachments | Shrine configuration and the uploaders |
@@ -32,6 +32,8 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
32
32
  | Tooling | esbuild, PostCSS, EditorConfig, Procfiles, Makefile, health check endpoint |
33
33
  | Linting | Standard, ESLint, Stylelint, Prettier, Slim Lint, Lefthook git hooks |
34
34
  | Security | Brakeman, bundler-audit, ruby_audit |
35
+ | Monitoring | AppSignal error tracking and performance monitoring — `config/appsignal.rb`, the push API key in the credentials, and the `AppsignalContext` concern tagging transactions per area and user |
36
+ | Specs | RSpec, and specs for the models Trek installs (`trek:install:model_specs`): `spec/models/page_spec.rb` and `spec/models/user_spec.rb`, with their test data as [Oaken](https://github.com/kaspth/oaken) seed scripts — defaults in `db/seeds/setup/`, scenarios in `db/seeds/test/cases/` loaded with `seed "cases/…"`. They check Trek's behaviour in your app — slugs and paths, user validations and roles — so a customization that breaks it fails your suite; edit or remove the ones you deliberately change. Trek's own suite runs the same files |
35
37
  | N+1 detection | Prosopite (development/test), scanning every request — see [Controllers › N+1 detection](/reference/controllers#n-1-detection) |
36
38
  | CI | GitLab CI configuration |
37
39
  | Deployment | Scalingo files (`trek:install:scalingo`, run on demand) — see [Deploying to Scalingo](/deploy) |
@@ -41,4 +43,5 @@ The installer orchestrates dedicated sub-generators, all under the `trek:install
41
43
  Define these **before** running the installer to have them injected automatically:
42
44
 
43
45
  - `POSTMARK_API_TOKEN` — injected into the Rails credentials for email delivery
46
+ - `APPSIGNAL_PUSH_API_KEY` — injected into the Rails credentials for error tracking and performance monitoring
44
47
  - `DEEPL_AUTH_KEY` — enables automatic locale translation in the [scaffold generator](/reference/generators/scaffold)
@@ -6,6 +6,6 @@ Trek bundles ~40 SVG icons, rendered with `Trek::IconComponent`:
6
6
  render Trek::IconComponent.new(:search)
7
7
  ```
8
8
 
9
- Available keys include: `add`, `arrow-bar-left`, `arrow-bar-right`, `bold`, `check`, `chevron-left`, `chevron-right`, `chevron-double`, `close`, `cursor`, `danger`, `download`, `eraser`, `eye`, `grid`, `hr`, `italic`, `layout`, `link`, `list`, `logout`, `paragraph`, `pic`, `profile`, `quotation`, `rating`, `redo`, `undo`, `search`…
9
+ Available keys include: `add`, `arrow-bar-left`, `arrow-bar-right`, `bold`, `check`, `chevron-left`, `chevron-right`, `chevron-double`, `close`, `cursor`, `danger`, `download`, `eraser`, `external`, `eye`, `grid`, `hr`, `italic`, `layout`, `link`, `list`, `logout`, `paragraph`, `pic`, `profile`, `quotation`, `rating`, `redo`, `undo`, `search`…
10
10
 
11
11
  [MenuNode](/reference/models/menu-node) records can reference an icon via their `icon_key` to display it in the admin sidebar.
@@ -12,7 +12,7 @@ The taxonomy models, installed by the [taxonomies generator](/reference/generato
12
12
  | `position` | integer | ordering within the category |
13
13
  | `taggings_count` | integer | counter cache |
14
14
 
15
- - `belongs_to :category`, `has_many :taggings`, `has_many :taggables, through: :taggings`
15
+ - `belongs_to :category`, `has_many :taggings`, and `taggables`, the tagged records of any model
16
16
  - Scopes: `ordered`, `in_category(key)`, `in_categories(keys)`
17
17
 
18
18
  ## TagCategory
@@ -1,6 +1,6 @@
1
1
  # User
2
2
 
3
- The host application's `User` model, into which Trek injects authentication (Sorcery), roles, theme & locale preferences and the invitation workflow.
3
+ The host application's `User` model, into which Trek injects authentication (`has_secure_password` and database-backed sessions), roles, theme & locale preferences and the invitation workflow.
4
4
 
5
5
  ## Columns added by Trek
6
6
 
@@ -10,8 +10,10 @@ The host application's `User` model, into which Trek injects authentication (Sor
10
10
  | `role` | integer | enum, default `guest` |
11
11
  | `theme` | integer | enum, default `auto` |
12
12
  | `locale` | string | admin UI language |
13
+ | `password_digest` | string | `has_secure_password` (bcrypt), required |
14
+ | `last_activity_at` | datetime | updated after each signed-in admin request |
13
15
 
14
- Plus the Sorcery authentication columns (`email`, `crypted_password`, reset tokens…).
16
+ When the app has no `User` model yet, Trek creates the `users` table with its `email` column. Signed-in browsers are `UserSession` records (`user_agent`, `ip_address`) in a `user_sessions` table, found through a signed `session_id` cookie. Password reset (15 minutes) and invitation (1 week) links carry tokens from `generates_token_for`, so no column stores them.
15
17
 
16
18
  ## Roles
17
19
 
@@ -5,15 +5,40 @@ File uploads are handled by [Shrine](https://shrinerb.com), with Uppy on the fro
5
5
  - `ImageUploader` — base image uploader
6
6
  - `RecordImageUploader` — attach an image to any record: `include RecordImageUploader::Attachment(:image)`
7
7
  - `ModelImageUploader` — model-level image uploader (used by content editor images)
8
+ - `AudioUploader` — sounds for `f.sound_field` (MP3, AAC/M4A, Ogg, WAV, WebM, FLAC): `include AudioUploader::Attachment(:audio)` on a record with an `audio_data` jsonb column
8
9
 
9
10
  ## Storage
10
11
 
11
12
  Configured per environment in the generated Shrine initializer (`config/initializers/shrine.rb`):
12
13
 
13
- | Environment | Storage |
14
- | --- | --- |
15
- | production | S3 (via the [`S3_*` ENV variables](/reference/environment-variables)) |
16
- | development | local filesystem (`public/uploads/`) |
17
- | test | in-memory |
14
+ | Environment | Storage |
15
+ | ----------- | --------------------------------------------------------------------- |
16
+ | production | S3 (via the [`S3_*` ENV variables](/reference/environment-variables)) |
17
+ | development | local filesystem (`public/uploads/`) |
18
+ | test | in-memory |
18
19
 
19
- Image processing uses libvips, blurhash placeholders are generated for progressive loading, and direct uploads go through the Shrine `upload_endpoint` (20 MB max).
20
+ Image processing uses libvips, blurhash placeholders are generated for progressive loading, and direct uploads go through the Shrine `upload_endpoint`: `POST /admin/upload/image` (20 MB max) and `POST /admin/upload/audio` (200 MB max), handled by the generated `Admin::UploadsController`. The endpoint only checks the size; each uploader validates the file type when it is attached to a record.
21
+
22
+ ## Attachments and records reloaded mid-save
23
+
24
+ `RecordImageUploader` loads `Trek::SavedAttachmentsReload` on top of Shrine's `activerecord` plugin:
25
+
26
+ ```ruby
27
+ class RecordImageUploader < ImageUploader
28
+ plugin :activerecord
29
+ plugin Trek::SavedAttachmentsReload
30
+ end
31
+ ```
32
+
33
+ Shrine resets every attacher when a record is reloaded, which is right for an explicit reload but loses a pending attachment when something reloads the record _during_ the save — closure_tree does exactly that in its `after_save` callback when it reorders the siblings of a numerically ordered tree, as the generated `Page` is. The file would then never be promoted out of the `cache` storage. The plugin keeps, across such a reload, the attachers whose file was saved with the record; an attachment assigned but not saved is still discarded.
34
+
35
+ ## Files left in the cache storage
36
+
37
+ An uploaded file first lands in the temporary `cache` storage, and is promoted to the permanent `store` after the record is committed. A record whose promotion was skipped keeps pointing to the cache, and its file breaks as soon as the cache is cleaned up. To find those records, for every model and attachment, and promote the ones whose file still exists:
38
+
39
+ ```bash
40
+ DRY_RUN=1 bin/rails trek:attachments:promote_cached # only lists them
41
+ bin/rails trek:attachments:promote_cached
42
+ ```
43
+
44
+ Each line gives the status (`promoted`, `promotable` on a dry run, `missing` when the cached file is gone, `changed` when the record changed meanwhile), the record and attachment, and the file id. Missing files have to be uploaded again.
data/docs/what-is-trek.md CHANGED
@@ -10,7 +10,7 @@ Trek ships as a Rails engine (a gem plus an NPM package) that integrates into yo
10
10
  - **Pages** — hierarchical, translatable pages with version history, SEO metadata and stable, redirect-aware URL paths.
11
11
  - **Fragments** — reusable chunks of content (a footer text, a hero tagline…) editable from the admin and usable anywhere in your views.
12
12
  - **A rich text editor** — based on [Tiptap](https://tiptap.dev), with image uploads (Uppy), link insertion and content stored as structured ProseMirror JSON.
13
- - **Authentication & authorization** — [Sorcery](https://github.com/Sorcery/sorcery)-based sign in, password resets, invitations, and [ActionPolicy](https://actionpolicy.evilmartians.io) policies with `admin`, `editor`, `user`, `reader` and `guest` roles.
13
+ - **Authentication & authorization** — sign in built on Rails' [`has_secure_password`](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) with database-backed sessions, password resets, invitations, and [ActionPolicy](https://actionpolicy.evilmartians.io) policies with `admin`, `editor`, `user`, `reader` and `guest` roles.
14
14
  - **Internationalization** — content translations with [Mobility](https://github.com/shioyama/mobility), locale management with [i18n-tasks](https://github.com/glebm/i18n-tasks), and optional automatic translation through DeepL.
15
15
  - **File uploads** — [Shrine](https://shrinerb.com)-backed attachments with S3 or local storage, image processing via libvips, and blurhash placeholders.
16
16
  - **Generators** — an installer that sets up everything above, and a scaffold generator that creates a full admin panel for any model in one command.
@@ -14,7 +14,13 @@ module Trek
14
14
  end
15
15
 
16
16
  def upload_image
17
- route 'post "upload", to: "uploads#image", namespace: :admin'
17
+ # The endpoint `Trek::Form::ImageFieldComponent` uploads to: /admin/upload/image
18
+ route 'post "upload/image", to: "uploads#image"', namespace: :admin
19
+ end
20
+
21
+ def upload_audio
22
+ # The endpoint `Trek::Form::SoundFieldComponent` uploads to: /admin/upload/audio
23
+ route 'post "upload/audio", to: "uploads#audio"', namespace: :admin
18
24
  end
19
25
 
20
26
  def lookbook
@@ -0,0 +1,102 @@
1
+ require "rails/generators/actions"
2
+ require "generators/trek/helpers"
3
+
4
+ module Trek
5
+ module Generators
6
+ module Install
7
+ class AppsignalGenerator < Rails::Generators::Base
8
+ include Trek::Generators::Helpers
9
+
10
+ desc "Install AppSignal (error tracking and performance monitoring)."
11
+
12
+ source_root File.expand_path("../templates", __dir__)
13
+
14
+ def add_gem
15
+ return if in_gemfile?("appsignal")
16
+
17
+ gem "appsignal", comment: "Error tracking and performance monitoring [https://appsignal.com]"
18
+ bundle_install
19
+ end
20
+
21
+ # Like Postmark's API token: the secret lives in the Rails credentials, not in
22
+ # config/appsignal.rb (where `appsignal install` would write it in plain text).
23
+ def inject_appsignal_to_credentials
24
+ return if credentials_encrypted_file.read&.include?("appsignal:")
25
+
26
+ inject_into_credentials(
27
+ <<~YAML
28
+ appsignal:
29
+ push_api_key: #{ENV.fetch("APPSIGNAL_PUSH_API_KEY", "")}
30
+ YAML
31
+ )
32
+ end
33
+
34
+ def create_config
35
+ template "config/appsignal.rb", "config/appsignal.rb"
36
+ end
37
+
38
+ def add_app_env_to_env_example
39
+ return unless File.exist?(".env.example")
40
+ return if File.read(".env.example").include?("APPSIGNAL_APP_ENV")
41
+
42
+ append_to_file ".env.example", "APPSIGNAL_APP_ENV=\n"
43
+ end
44
+
45
+ # A broken initializer or a missing credential raises before Rails can report
46
+ # anything — the error reporter does not exist yet — so the boot is wrapped.
47
+ def report_boot_errors
48
+ return unless File.exist?("config.ru")
49
+ return if File.read("config.ru").include?("Appsignal.send_error")
50
+
51
+ gsub_file "config.ru",
52
+ %r{^require_relative ["']config/environment["']\n},
53
+ boot_error_reporting
54
+ end
55
+
56
+ def create_context_concern
57
+ copy_file "controllers/concerns/appsignal_context.rb",
58
+ "app/controllers/concerns/appsignal_context.rb"
59
+ end
60
+
61
+ def include_context_in_application_controller
62
+ return if File.read("app/controllers/application_controller.rb").include?("AppsignalContext")
63
+
64
+ inject_into_class "app/controllers/application_controller.rb",
65
+ "ApplicationController",
66
+ " include AppsignalContext\n"
67
+ run "rubocop --autocorrect app/controllers/application_controller.rb", abort_on_failure: true
68
+ end
69
+
70
+ def include_context_in_trek_controllers
71
+ copy_file "initializers/appsignal_context.rb",
72
+ "config/initializers/appsignal_context.rb"
73
+ end
74
+
75
+ def final_instructions
76
+ say ""
77
+ say "AppSignal is configured for production. Check the setup with:"
78
+ say ""
79
+ say " bundle exec appsignal diagnose"
80
+ say ""
81
+ end
82
+
83
+ private
84
+
85
+ def boot_error_reporting
86
+ <<~RUBY
87
+ begin
88
+ require_relative "config/environment"
89
+ rescue Exception => error # rubocop:disable Lint/RescueException
90
+ # Errors raised while booting (a broken initializer, a missing credential)
91
+ # happen before the Rails error reporter exists — report them directly.
92
+ # The gem itself is loaded by Bundler.require, so a failure earlier than
93
+ # that (config/boot.rb, the Gemfile) has no Appsignal to report with.
94
+ Appsignal.send_error(error) if defined?(Appsignal)
95
+ raise
96
+ end
97
+ RUBY
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,64 @@
1
+ require "rails/generators/actions"
2
+ require "generators/trek/helpers"
3
+
4
+ module Trek
5
+ module Generators
6
+ module Install
7
+ class ModelSpecsGenerator < Rails::Generators::Base
8
+ include Trek::Generators::Helpers
9
+
10
+ desc "Add specs for the models Trek installs (Page, User…), with their Oaken seeds."
11
+
12
+ source_root File.expand_path("../templates", __dir__)
13
+
14
+ # In every group: `db/seeds.rb` calls Oaken, and `db:seed` runs in every environment.
15
+ def add_gem
16
+ return if in_gemfile?("oaken")
17
+
18
+ gem "oaken", "~> 1.0"
19
+ bundle_install
20
+ end
21
+
22
+ def load_oaken_in_seeds
23
+ seeds = File.join(destination_root, "db/seeds.rb")
24
+ return if File.exist?(seeds) && File.read(seeds).include?("Oaken.seed")
25
+
26
+ create_file "db/seeds.rb" unless File.exist?(seeds)
27
+ append_to_file "db/seeds.rb", <<~RUBY
28
+
29
+ # Oaken (https://github.com/kaspth/oaken) loads db/seeds/setup (defaults and helpers) first.
30
+ # Pass directories to seed them too, e.g. `Oaken.seed :data`; specs load their own cases with
31
+ # `seed "cases/…"`.
32
+ Oaken.seed
33
+ RUBY
34
+ end
35
+
36
+ def load_oaken_in_specs
37
+ rails_helper = File.join(destination_root, "spec/rails_helper.rb")
38
+ return unless File.exist?(rails_helper)
39
+ return if File.read(rails_helper).include?("oaken/rspec_setup")
40
+
41
+ # After rspec-rails, which Oaken's setup configures (transactional examples, seeding the
42
+ # database before the suite).
43
+ inject_into_file "spec/rails_helper.rb",
44
+ "require \"oaken/rspec_setup\"\n",
45
+ after: %r{^require ["']rspec/rails["'].*\n}
46
+ end
47
+
48
+ # Every file under the templates' db/seeds, spec/models and spec/requests is installed, so a new
49
+ # template needs no change here.
50
+ def copy_seeds
51
+ directory "db/seeds", "db/seeds"
52
+ end
53
+
54
+ def copy_specs
55
+ directory "spec/models", "spec/models"
56
+ directory "spec/requests", "spec/requests"
57
+ directory "spec/fixtures", "spec/fixtures"
58
+ # The tag policies' specs come with them, from `trek:taxonomies`.
59
+ directory "spec/policies", "spec/policies", exclude_pattern: /tag(_category)?_policy_spec/
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end