trek 2.3.0 → 2.3.2
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.
- checksums.yaml +4 -4
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +4 -1
- data/app/components/trek/dropdown_component/dropdown_component.css +4 -1
- data/app/components/trek/dropdown_component/dropdown_component.html.slim +1 -1
- data/app/components/trek/dropdown_component.rb +5 -0
- data/app/components/trek/header_component/header_component.css +5 -6
- data/app/components/trek/header_component/header_component.html.slim +1 -1
- data/app/components/trek/header_component.rb +4 -0
- data/app/controllers/concerns/trek/filters.rb +18 -1
- data/app/controllers/concerns/trek/scopes.rb +9 -2
- data/app/models/concerns/trek/users/validations.rb +15 -6
- data/docs/.vitepress/config.mjs +20 -1
- data/docs/.vitepress/theme/style.css +36 -8
- data/docs/changelog.md +9 -0
- data/docs/index.md +7 -7
- data/docs/public/apple-touch-icon.png +0 -0
- data/docs/public/favicon.ico +0 -0
- data/docs/public/favicon.png +0 -0
- data/docs/public/trek-mark.webp +0 -0
- data/docs/reference/controllers.md +9 -4
- data/docs/reference/models/user.md +2 -2
- data/lib/generators/trek/templates/controllers/admin/user_password_resets_controller.rb +7 -0
- data/lib/generators/trek/templates/spec/models/user_spec.rb +33 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +19 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +12 -0
- data/lib/generators/trek/templates/views/admin/user_password_resets/edit.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin/user_password_resets/new.html.slim +1 -1
- data/lib/generators/trek/templates/views/admin/user_sessions/new.html.slim +1 -1
- data/lib/trek/version.rb +1 -1
- data/lib/trek.rb +1 -0
- data/package.json +1 -1
- data/trek.gemspec +1 -0
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3749a9dc01b378419fbbaacdbf9e8251100d9bdd6281b308a9d44cf9dd18d182
|
|
4
|
+
data.tar.gz: d61b12a1f80494ebaf0c664bbfcfdf2d24b682632ef9c268117b4de23fb7e1b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9083e34869370a8b22b69a8c9aadb3f404e74f9d4b32097c48f51ad024d750aed71769a203112b78f19b74c7489ec46f512340bdd728dfebfef76f6bd37ec27
|
|
7
|
+
data.tar.gz: 9800e5d16a2639e2a51644ce67bb488bd1e4c34930867f08311b7df56992dd7dcf42b37a926d4097a5bd5f9a1a59bc476f5d2c70091b9eb2f54efc11ee14dcfc
|
data/AGENTS.md
CHANGED
|
@@ -60,6 +60,6 @@ Security: `bundle exec brakeman` · `bundle exec bundle-audit check --update`
|
|
|
60
60
|
- **Slim** linted via slim_lint; **JS/CSS** via ESLint + Stylelint + Prettier 2.8.8
|
|
61
61
|
- **i18n**: keep English + French locales in sync (`config/locales/trek.*.yml`); DeepL auto-translation via `DEEPL_AUTH_KEY`
|
|
62
62
|
- **Versioning**: version lives in both `lib/trek/version.rb` and `package.json` — keep in sync via `make release`
|
|
63
|
-
- **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** When the change requires action in the host app on upgrade, end the entry with a bold `**Upgrade note:**` label followed by what to do. Use the same label when the change lets host apps remove or simplify existing patches or custom implementations (e.g. "**Upgrade note:** you can now drop your custom …") — never as an unlabelled trailing sentence.
|
|
63
|
+
- **CHANGELOG.md**: Keep-a-Changelog format. **Every MR must add an `[Unreleased]` entry.** Keep entries short: one or two sentences on what changed for host apps, not how it was found or fixed — the MR description holds the details. When the change requires action in the host app on upgrade, end the entry with a bold `**Upgrade note:**` label followed by what to do. Use the same label when the change lets host apps remove or simplify existing patches or custom implementations (e.g. "**Upgrade note:** you can now drop your custom …") — never as an unlabelled trailing sentence.
|
|
64
64
|
- **Public code**: Trek ships publicly (gem, npm, docs), so nothing committed may mention a client or project name, nor a client's domain models or business logic — not in code, comments, specs, templates, docs, CHANGELOG or commit messages. Use neutral examples (`Page`, `Shop::Product`, an engine's `Blog::Post`). Client context belongs in GitLab issues and MR descriptions only.
|
|
65
65
|
- **CI**: GitLab CI (`.gitlab-ci.yml`), stages build → lint → security → test → deploy; docs deploy to Netlify; deps via Renovate
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.3.2] - 2026-10-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Security:** `Trek::Scopes#apply_scopes` called any name found in `params[:scopes]` that the relation answered to, not only the controller's `available_scopes`, which let a request run unintended methods on the listed records. Only the names in `available_scopes` are applied now; others are ignored. **Upgrade note:** update Trek as soon as possible. A controller passing `apply_scopes` a scope name it does not list in `available_scopes` must add it there.
|
|
8
|
+
- The generated admin sign-in, password reset request and new password pages rendered their actions with the default aspect of `Trek::Form::ActionsComponent`, whose sticky action bar (top margin, padding, background) is meant for edit forms: inside those small cards, the button and the « forgotten password » link sat in a padded bar. They now use the `seamless` aspect. **Upgrade note:** in apps generated before this version, pass `aspect: :seamless` to `Trek::Form::ActionsComponent.new` in `app/views/admin/user_sessions/new.html.slim` and `app/views/admin/user_password_resets/{new,edit}.html.slim`.
|
|
9
|
+
|
|
10
|
+
## [2.3.1] - 2026-10-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `Trek::Filters` only filters on the columns a controller lists in its new `available_filters` (empty by default), read through strong params (`filter_params`): other keys of `params[:filters]`, and values that aren't a single value, are ignored. Until now any key became a `where` condition, so a request could filter an index on columns it was never meant to expose. **Upgrade note:** in each controller including `Trek::Filters`, define `available_filters` returning the filtered columns (e.g. `def available_filters = %i[country_code]`), or its filters stop applying.
|
|
15
|
+
- `Trek::Users::Validations` stores emails stripped and in lower case (sign-in and password resets now ignore case), checks them with `email_validator`'s strict mode, and refuses a spaces-only password; the password reset form refuses an empty password instead of pretending to change it. **Upgrade note:** resolve users whose emails differ only by case (`User.group("lower(email)").having("count(*) > 1").count`), then downcase the rest (`User.find_each { |user| user.update_column(:email, user.email.strip.downcase) }`); in `Admin::UserPasswordResetsController#update`, redirect back with an error when `params[:password]` is blank (see Trek's template); re-run `rails g trek:install:model_specs` for the new specs.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- The header's theme switcher now hovers like the links next to it, and the whole pill is clickable, not just the icon. `Trek::DropdownComponent`'s trigger inherits its context's color, and a new `trigger_classnames:` option styles the trigger button. **Upgrade note:** if you reuse `Trek::DropdownComponent`, move trigger styling (padding, hover) from `classnames:` to `trigger_classnames:`.
|
|
20
|
+
|
|
3
21
|
## [2.3.0] - 2026-10-01
|
|
4
22
|
|
|
5
23
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
trek (2.3.
|
|
4
|
+
trek (2.3.2)
|
|
5
5
|
action_policy (~> 0.6)
|
|
6
6
|
actioncable
|
|
7
7
|
acts_as_list (~> 1.1)
|
|
@@ -10,6 +10,7 @@ PATH
|
|
|
10
10
|
closure_tree (~> 9.0)
|
|
11
11
|
deepl-rb (~> 3.0)
|
|
12
12
|
dry-initializer (~> 3.1)
|
|
13
|
+
email_validator (~> 2.2)
|
|
13
14
|
fastimage (~> 2.2)
|
|
14
15
|
http_accept_language (~> 2.1)
|
|
15
16
|
i18n-tasks (~> 1.0)
|
|
@@ -205,6 +206,8 @@ GEM
|
|
|
205
206
|
dry-inflector (~> 1.0)
|
|
206
207
|
dry-logic (~> 1.4)
|
|
207
208
|
zeitwerk (~> 2.6)
|
|
209
|
+
email_validator (2.2.4)
|
|
210
|
+
activemodel
|
|
208
211
|
erb (6.0.4)
|
|
209
212
|
erubi (1.13.1)
|
|
210
213
|
fastimage (2.4.0)
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
position: relative;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/* Inherits its context's color and font, so it reads as part of the surrounding text or link (and
|
|
9
|
+
follows its hover color). Style the trigger itself, not the root, with `trigger_classnames:`. */
|
|
8
10
|
.trigger {
|
|
9
11
|
align-items: center;
|
|
10
12
|
background: none;
|
|
11
13
|
border: none;
|
|
12
|
-
color:
|
|
14
|
+
color: inherit;
|
|
13
15
|
cursor: pointer;
|
|
14
16
|
display: inline-flex;
|
|
17
|
+
font: inherit;
|
|
15
18
|
gap: 0.4rem;
|
|
16
19
|
}
|
|
17
20
|
|
|
@@ -2,6 +2,7 @@ module Trek
|
|
|
2
2
|
class DropdownComponent < Trek::Component
|
|
3
3
|
option :label, optional: true # accessible name (aria-label on trigger + menu)
|
|
4
4
|
option :classnames, optional: true # extra classes on root
|
|
5
|
+
option :trigger_classnames, optional: true # extra classes on the trigger button, the clickable element
|
|
5
6
|
option :hide_chevron, default: proc { false }
|
|
6
7
|
|
|
7
8
|
renders_one :trigger # inner content of the trigger button (e.g. an icon)
|
|
@@ -11,6 +12,10 @@ module Trek
|
|
|
11
12
|
class_names_for(root_class, classnames)
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
def trigger_classes
|
|
16
|
+
class_names_for(class_for("trigger"), trigger_classnames)
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
def menu_id
|
|
15
20
|
@menu_id ||= "#{identifier}-menu-#{SecureRandom.hex(4)}"
|
|
16
21
|
end
|
|
@@ -66,13 +66,12 @@
|
|
|
66
66
|
text-overflow: ellipsis;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/*
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.nav-item-link:global(.c-trek--dropdown-root .c-trek--icon-root) {
|
|
69
|
+
/* The theme dropdown's trigger: a nav item button (the button itself, so the whole pill is clickable),
|
|
70
|
+
with a larger icon and less vertical padding. */
|
|
71
|
+
.nav-theme-trigger {
|
|
75
72
|
--icon-size: 2rem;
|
|
73
|
+
|
|
74
|
+
padding: 0.8rem 1.6rem;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/* Media queries */
|
|
@@ -35,7 +35,7 @@ div(class=classes data-controller=identifier \
|
|
|
35
35
|
li(class=class_for("nav-item"))
|
|
36
36
|
= render Trek::DropdownComponent.new( \
|
|
37
37
|
label: User.human_attribute_name(:theme),
|
|
38
|
-
|
|
38
|
+
trigger_classnames: theme_trigger_classes,
|
|
39
39
|
hide_chevron: true,
|
|
40
40
|
) do |dropdown|
|
|
41
41
|
- dropdown.with_trigger do
|
|
@@ -29,6 +29,10 @@ module Trek
|
|
|
29
29
|
) && preview_path.present?
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def theme_trigger_classes
|
|
33
|
+
class_names_for("nav-item-button", "nav-theme-trigger", map_args: true)
|
|
34
|
+
end
|
|
35
|
+
|
|
32
36
|
def theme_item_data(theme)
|
|
33
37
|
{ action: stimulus_action("click", "setTheme"), theme: }
|
|
34
38
|
end
|
|
@@ -25,8 +25,13 @@ module Trek
|
|
|
25
25
|
def apply_filters(scope, filters)
|
|
26
26
|
return scope if filters.blank?
|
|
27
27
|
|
|
28
|
+
# Only the controller's own filters: the keys come from the request and become column names
|
|
29
|
+
allowed = available_filters.map(&:to_s)
|
|
30
|
+
|
|
28
31
|
filters.each do |filter, value|
|
|
29
32
|
next if value.blank?
|
|
33
|
+
next unless allowed.include?(filter.to_s)
|
|
34
|
+
|
|
30
35
|
value = nil if value == "nil"
|
|
31
36
|
|
|
32
37
|
scope = scope.where(filter => value)
|
|
@@ -37,7 +42,19 @@ module Trek
|
|
|
37
42
|
|
|
38
43
|
def apply_filters!
|
|
39
44
|
@filters_applied = true
|
|
40
|
-
@objects = apply_filters(@objects,
|
|
45
|
+
@objects = apply_filters(@objects, filter_params)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The requested filters, permitted to the available ones with scalar values only; nil when
|
|
49
|
+
# params[:filters] isn't a hash
|
|
50
|
+
def filter_params
|
|
51
|
+
filters = params[:filters]
|
|
52
|
+
filters.permit(*available_filters) if filters.respond_to?(:permit)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The columns a request may filter on; override in the controller.
|
|
56
|
+
def available_filters
|
|
57
|
+
[]
|
|
41
58
|
end
|
|
42
59
|
|
|
43
60
|
private
|
|
@@ -19,8 +19,13 @@ module Trek
|
|
|
19
19
|
def apply_scopes(scope, filters)
|
|
20
20
|
return scope if filters.blank?
|
|
21
21
|
|
|
22
|
+
# Only the controller's own scopes: the names come from the request, and a relation also
|
|
23
|
+
# answers to destroy_all, delete_all…
|
|
24
|
+
allowed = available_scopes.map(&:to_s)
|
|
25
|
+
|
|
22
26
|
filters.each do |filter, value|
|
|
23
27
|
next if value.blank?
|
|
28
|
+
next unless allowed.include?(filter.to_s)
|
|
24
29
|
|
|
25
30
|
scope = scope.public_send(filter) if scope.respond_to?(filter)
|
|
26
31
|
end
|
|
@@ -31,7 +36,7 @@ module Trek
|
|
|
31
36
|
def apply_scopes!
|
|
32
37
|
@scopes_applied = true
|
|
33
38
|
@unscoped_objects = @objects
|
|
34
|
-
@objects = apply_scopes(@objects,
|
|
39
|
+
@objects = apply_scopes(@objects, scope_params)
|
|
35
40
|
scope_counts
|
|
36
41
|
end
|
|
37
42
|
|
|
@@ -65,8 +70,10 @@ module Trek
|
|
|
65
70
|
[]
|
|
66
71
|
end
|
|
67
72
|
|
|
73
|
+
# The requested scopes, permitted to the available ones; nil when params[:scopes] isn't a hash
|
|
68
74
|
def scope_params
|
|
69
|
-
params[:scopes]
|
|
75
|
+
scopes = params[:scopes]
|
|
76
|
+
scopes.permit(*available_scopes) if scopes.respond_to?(:permit)
|
|
70
77
|
end
|
|
71
78
|
|
|
72
79
|
private def ensure_scopes_applied
|
|
@@ -4,14 +4,23 @@ module Trek
|
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
6
6
|
included do
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# Also applies to lookups (`find_by(email:)`, `authenticate_by`), so signing in and asking
|
|
8
|
+
# for a password reset work whatever the case typed.
|
|
9
|
+
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
10
|
+
|
|
11
11
|
validates :email,
|
|
12
12
|
presence: true,
|
|
13
|
-
uniqueness:
|
|
14
|
-
|
|
13
|
+
uniqueness: { case_sensitive: false },
|
|
14
|
+
email: { mode: :strict }
|
|
15
|
+
# Only when a password was assigned: `password` is nil otherwise.
|
|
16
|
+
validates :password, length: { minimum: 8 }, allow_nil: true
|
|
17
|
+
validate :password_not_only_spaces
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def password_not_only_spaces
|
|
23
|
+
errors.add(:password, :blank) if password&.strip&.empty?
|
|
15
24
|
end
|
|
16
25
|
end
|
|
17
26
|
end
|
data/docs/.vitepress/config.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { defineConfig } from 'vitepress'
|
|
2
2
|
import llmstxt from 'vitepress-plugin-llms'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
// The released version: `make release` keeps package.json in sync with lib/trek/version.rb.
|
|
6
|
+
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
|
|
3
7
|
|
|
4
8
|
|
|
5
9
|
// https://vitepress.dev/reference/site-config
|
|
@@ -11,6 +15,13 @@ export default defineConfig({
|
|
|
11
15
|
description: "A modern content management & back-office system for Ruby on Rails",
|
|
12
16
|
lang: 'en-US',
|
|
13
17
|
lastUpdated: true,
|
|
18
|
+
// The header logo doubles as the favicon (PNG versions for older browsers and iOS home screens,
|
|
19
|
+
// favicon.ico for clients that only request /favicon.ico).
|
|
20
|
+
head: [
|
|
21
|
+
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
|
|
22
|
+
['link', { rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon.png' }],
|
|
23
|
+
['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }],
|
|
24
|
+
],
|
|
14
25
|
themeConfig: {
|
|
15
26
|
logo: { src: '/logo.svg', width: 24, height: 24 },
|
|
16
27
|
|
|
@@ -18,7 +29,15 @@ export default defineConfig({
|
|
|
18
29
|
nav: [
|
|
19
30
|
{ text: 'Home', link: '/' },
|
|
20
31
|
{ text: 'Guide', link: '/what-is-trek' },
|
|
21
|
-
{ text: 'Reference', link: '/reference/' }
|
|
32
|
+
{ text: 'Reference', link: '/reference/' },
|
|
33
|
+
{
|
|
34
|
+
text: `v${version}`,
|
|
35
|
+
items: [
|
|
36
|
+
{ text: 'Changelog', link: '/changelog' },
|
|
37
|
+
{ text: 'RubyGems', link: `https://rubygems.org/gems/trek/versions/${version}` },
|
|
38
|
+
{ text: 'npm', link: `https://www.npmjs.com/package/trek/v/${version}` }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
22
41
|
],
|
|
23
42
|
|
|
24
43
|
sidebar: {
|
|
@@ -75,15 +75,18 @@
|
|
|
75
75
|
* -------------------------------------------------------------------------- */
|
|
76
76
|
|
|
77
77
|
:root {
|
|
78
|
+
/* The violet end of the hero name gradient, a step darker on hover and when pressed */
|
|
79
|
+
--trek-button-brand: #7a4df0;
|
|
80
|
+
|
|
78
81
|
--vp-button-brand-border: transparent;
|
|
79
82
|
--vp-button-brand-text: var(--vp-c-white);
|
|
80
|
-
--vp-button-brand-bg: var(--
|
|
83
|
+
--vp-button-brand-bg: var(--trek-button-brand);
|
|
81
84
|
--vp-button-brand-hover-border: transparent;
|
|
82
85
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
|
83
|
-
--vp-button-brand-hover-bg: var(--
|
|
86
|
+
--vp-button-brand-hover-bg: color-mix(in srgb, var(--trek-button-brand) 88%, black);
|
|
84
87
|
--vp-button-brand-active-border: transparent;
|
|
85
88
|
--vp-button-brand-active-text: var(--vp-c-white);
|
|
86
|
-
--vp-button-brand-active-bg: var(--
|
|
89
|
+
--vp-button-brand-active-bg: color-mix(in srgb, var(--trek-button-brand) 76%, black);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|
|
@@ -91,19 +94,44 @@
|
|
|
91
94
|
* -------------------------------------------------------------------------- */
|
|
92
95
|
|
|
93
96
|
:root {
|
|
97
|
+
--trek-green: #63e0a6;
|
|
98
|
+
--trek-purple: #9b7bff;
|
|
99
|
+
|
|
94
100
|
--vp-home-hero-name-color: transparent;
|
|
95
|
-
|
|
101
|
+
/* Darker stops on the light background, the brand colors themselves on the dark one */
|
|
102
|
+
--vp-home-hero-name-background: linear-gradient(
|
|
96
103
|
120deg,
|
|
97
|
-
#
|
|
98
|
-
#
|
|
104
|
+
#1fa971 15%,
|
|
105
|
+
#7a4df0 100%
|
|
99
106
|
);
|
|
100
107
|
|
|
101
108
|
--vp-home-hero-image-background-image: linear-gradient(
|
|
102
109
|
-45deg,
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
var(--trek-purple) 50%,
|
|
111
|
+
var(--trek-green) 50%
|
|
105
112
|
);
|
|
106
113
|
--vp-home-hero-image-filter: blur(44px);
|
|
114
|
+
|
|
115
|
+
/* Feature icons (docs/index.md): the ink follows the text color, light or dark */
|
|
116
|
+
--trek-icon-ink: currentColor;
|
|
117
|
+
--trek-icon-accent: var(--trek-green);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Text selection in the brand violet rather than the browser's default blue */
|
|
121
|
+
::selection {
|
|
122
|
+
background-color: color-mix(in srgb, var(--trek-purple) 30%, transparent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.dark ::selection {
|
|
126
|
+
background-color: color-mix(in srgb, var(--trek-purple) 45%, transparent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.dark {
|
|
130
|
+
--vp-home-hero-name-background: linear-gradient(
|
|
131
|
+
120deg,
|
|
132
|
+
var(--trek-green) 15%,
|
|
133
|
+
var(--trek-purple) 100%
|
|
134
|
+
);
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
@media (min-width: 640px) {
|
data/docs/changelog.md
ADDED
data/docs/index.md
CHANGED
|
@@ -7,7 +7,7 @@ hero:
|
|
|
7
7
|
text: "Take the easy path"
|
|
8
8
|
tagline: "A modern content management & back-office system for Ruby on Rails"
|
|
9
9
|
image:
|
|
10
|
-
src: /
|
|
10
|
+
src: /trek-mark.webp
|
|
11
11
|
alt: Trek
|
|
12
12
|
actions:
|
|
13
13
|
- theme: brand
|
|
@@ -18,22 +18,22 @@ hero:
|
|
|
18
18
|
link: /getting-started
|
|
19
19
|
|
|
20
20
|
features:
|
|
21
|
-
- icon:
|
|
21
|
+
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M16 14A8 8 0 0 1 32 14V38H16ZM21 18A3 3 0 1 0 27 18A3 3 0 1 0 21 18ZM16 26V38H4A12 12 0 0 1 16 26ZM32 26A12 12 0 0 1 44 38H32Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M18 40H30A6 6 0 0 1 18 40Z"/></svg>'
|
|
22
22
|
title: Ready out of the box
|
|
23
23
|
details: One installer sets up authentication, authorization, file uploads, emails, i18n and a polished admin UI — with sensible conventions you can override later.
|
|
24
|
-
- icon:
|
|
24
|
+
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H30V30A12 12 0 0 1 18 42H6ZM12 14H24V18H12ZM12 22H20V26H12Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M34 6H42V32A4 4 0 0 1 34 32Z"/></svg>'
|
|
25
25
|
title: Modern content editing
|
|
26
26
|
details: A Tiptap-based rich text editor, hierarchical pages with versioning and SEO-friendly paths, plus reusable content fragments.
|
|
27
|
-
- icon:
|
|
27
|
+
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 18H30V30H42V42H6Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M6 18V6A12 12 0 0 1 18 18Z"/></svg>'
|
|
28
28
|
title: Scaffold-driven development
|
|
29
29
|
details: One command generates the model, admin panel, views, policy, routes and locales for any resource — just like Rails scaffolding, but for your back-office.
|
|
30
|
-
- icon:
|
|
30
|
+
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M22 22H5A17 17 0 0 1 22 5ZM22 26V43A17 17 0 0 1 5 26ZM26 26H43A17 17 0 0 1 26 43Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M26 22V5A17 17 0 0 1 43 22Z"/></svg>'
|
|
31
31
|
title: Multilingual by default
|
|
32
32
|
details: Content and admin UI translations powered by Mobility and i18n-tasks, with optional automatic translation via DeepL.
|
|
33
|
-
- icon:
|
|
33
|
+
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M14 22V16A10 10 0 0 1 34 16V22H28V16A4 4 0 0 0 20 16V22ZM8 22H24V42H16A8 8 0 0 1 8 34Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M24 22H40V34A8 8 0 0 1 32 42H24Z"/></svg>'
|
|
34
34
|
title: Roles & policies built in
|
|
35
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
|
-
- icon:
|
|
36
|
+
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="32" height="32" aria-hidden="true"><path fill-rule="evenodd" fill="var(--trek-icon-ink, currentColor)" d="M6 6H22V22H6ZM26 22V6A16 16 0 0 1 42 22ZM6 26H22V42A16 16 0 0 1 6 26Z"/><path fill="var(--trek-icon-accent, #63E0A6)" d="M26 26H42V42H26Z"/></svg>'
|
|
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.
|
|
39
39
|
---
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -26,9 +26,10 @@ end
|
|
|
26
26
|
|
|
27
27
|
### Filters
|
|
28
28
|
|
|
29
|
-
`Trek::Filters` lets users narrow an index by attribute values.
|
|
30
|
-
|
|
31
|
-
`"nil"` matches `NULL`)
|
|
29
|
+
`Trek::Filters` lets users narrow an index by attribute values. Declare the columns users may filter on via
|
|
30
|
+
`available_filters`, then call `apply_filters!` in your `index` action — it rewrites `@objects` from
|
|
31
|
+
`params[:filters]`, applying a `where(column => value)` per non-blank filter (a value of `"nil"` matches `NULL`). The filters are read
|
|
32
|
+
through strong params (`filter_params`): any other key, and any value that isn't a single value, is ignored:
|
|
32
33
|
|
|
33
34
|
```ruby
|
|
34
35
|
module Admin
|
|
@@ -44,6 +45,10 @@ module Admin
|
|
|
44
45
|
|
|
45
46
|
private
|
|
46
47
|
|
|
48
|
+
def available_filters
|
|
49
|
+
%i[country_code]
|
|
50
|
+
end
|
|
51
|
+
|
|
47
52
|
def countries
|
|
48
53
|
City.distinct.pluck(:country_code).index_with { |code| I18n.t("countries.#{code}") }
|
|
49
54
|
end
|
|
@@ -70,7 +75,7 @@ with the chosen filter:
|
|
|
70
75
|
### Scopes
|
|
71
76
|
|
|
72
77
|
`Trek::Scopes` filters by **named model scopes** rather than raw attributes. Declare the allowed scopes via
|
|
73
|
-
`available_scopes`, then call `apply_scopes!` in your `index
|
|
78
|
+
`available_scopes`, then call `apply_scopes!` in your `index`; any other name in `params[:scopes]` is ignored. It also exposes `scope_counts` and
|
|
74
79
|
`unscoped_objects` helpers (the per-scope match counts and the pre-scope collection) for building scope tabs:
|
|
75
80
|
|
|
76
81
|
```ruby
|
|
@@ -52,8 +52,8 @@ User.create(email: "new@example.com", role: :editor, send_invite: true)
|
|
|
52
52
|
|
|
53
53
|
`Trek::Users::Validations` enforces:
|
|
54
54
|
|
|
55
|
-
- `email` —
|
|
56
|
-
- `password` —
|
|
55
|
+
- `email` — stored stripped and in lower case (lookups too, so sign-in and password resets ignore case), present, unique whatever its case, and well-formed by [`email_validator`](https://github.com/K-and-R/email_validator)'s strict mode (no consecutive or leading/trailing dots, a domain with a TLD)
|
|
56
|
+
- `password` — when one is assigned: at least 8 characters, and not spaces only. A blank password is ignored by `has_secure_password` (the current one is kept); the password reset form refuses it
|
|
57
57
|
|
|
58
58
|
## Scopes
|
|
59
59
|
|
|
@@ -29,6 +29,13 @@ module Admin
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def update
|
|
32
|
+
# has_secure_password ignores a blank password: refuse it, rather than report a change that
|
|
33
|
+
# didn't happen.
|
|
34
|
+
if params[:password].blank?
|
|
35
|
+
@user.errors.add(:password, :blank)
|
|
36
|
+
return redirect_to edit_admin_user_password_reset_path(@token), alert: @user.errors.full_messages.join(", ")
|
|
37
|
+
end
|
|
38
|
+
|
|
32
39
|
@user.update!(params.permit(:password))
|
|
33
40
|
@user.user_sessions.destroy_all
|
|
34
41
|
redirect_to new_admin_user_session_path, notice: t(".success")
|
|
@@ -26,6 +26,27 @@ RSpec.describe User do
|
|
|
26
26
|
expect(user).to be_invalid
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
it "requires a unique email whatever its case" do
|
|
30
|
+
users.create(email: "ada@example.com")
|
|
31
|
+
user.email = "Ada@Example.com"
|
|
32
|
+
|
|
33
|
+
expect(user).to be_invalid
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
%w[ada..lovelace@example.com .ada@example.com ada.@example.com ada@example..com ada@example].each do |email|
|
|
37
|
+
it "refuses an email mail providers would reject: #{email}" do
|
|
38
|
+
user.email = email
|
|
39
|
+
|
|
40
|
+
expect(user).to be_invalid
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "refuses a password made of spaces only" do
|
|
45
|
+
user.password = " " * 8
|
|
46
|
+
|
|
47
|
+
expect(user).to be_invalid
|
|
48
|
+
end
|
|
49
|
+
|
|
29
50
|
it "requires a password of at least 8 characters on creation" do
|
|
30
51
|
user.password = "short"
|
|
31
52
|
|
|
@@ -47,6 +68,18 @@ RSpec.describe User do
|
|
|
47
68
|
end
|
|
48
69
|
end
|
|
49
70
|
|
|
71
|
+
describe "email" do
|
|
72
|
+
it "is stored in lower case, without surrounding spaces" do
|
|
73
|
+
expect(users.create(email: " Ada@Example.com ").email).to eq("ada@example.com")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "finds the user whatever its case" do
|
|
77
|
+
ada = users.create(email: "ada@example.com")
|
|
78
|
+
|
|
79
|
+
expect(described_class.find_by(email: "ADA@example.com")).to eq(ada)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
50
83
|
describe "roles" do
|
|
51
84
|
it "defaults to guest" do
|
|
52
85
|
expect(user.role).to eq("guest")
|
|
@@ -9,6 +9,12 @@ RSpec.describe "Admin password resets", type: :request do
|
|
|
9
9
|
expect(response).to have_http_status(:ok)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
it "keeps the reset request actions in the card, out of the sticky action bar" do
|
|
13
|
+
get new_admin_user_password_reset_path
|
|
14
|
+
|
|
15
|
+
expect(response.body).to include("c-trek--form--actions-aspect-seamless")
|
|
16
|
+
end
|
|
17
|
+
|
|
12
18
|
it "emails a reset link to a privileged user" do
|
|
13
19
|
expect { post admin_user_password_resets_path, params: { email: editor.email } }
|
|
14
20
|
.to have_enqueued_mail(Admin::PasswordsMailer, :reset)
|
|
@@ -31,6 +37,12 @@ RSpec.describe "Admin password resets", type: :request do
|
|
|
31
37
|
expect(response).to have_http_status(:ok)
|
|
32
38
|
end
|
|
33
39
|
|
|
40
|
+
it "keeps the new password actions in the card, out of the sticky action bar" do
|
|
41
|
+
get edit_admin_user_password_reset_path(editor.password_reset_token)
|
|
42
|
+
|
|
43
|
+
expect(response.body).to include("c-trek--form--actions-aspect-seamless")
|
|
44
|
+
end
|
|
45
|
+
|
|
34
46
|
it "renders the new password form from an invitation link" do
|
|
35
47
|
get edit_admin_user_password_reset_path(editor.generate_token_for(:invite))
|
|
36
48
|
|
|
@@ -56,6 +68,13 @@ RSpec.describe "Admin password resets", type: :request do
|
|
|
56
68
|
expect(editor.user_sessions).to be_empty
|
|
57
69
|
end
|
|
58
70
|
|
|
71
|
+
it "sends an empty password back to the form rather than pretending to change it" do
|
|
72
|
+
token = editor.password_reset_token
|
|
73
|
+
patch admin_user_password_reset_path(token), params: { password: "" }
|
|
74
|
+
|
|
75
|
+
expect(response).to redirect_to(edit_admin_user_password_reset_path(token))
|
|
76
|
+
end
|
|
77
|
+
|
|
59
78
|
it "sends a password shorter than 8 characters back to the form" do
|
|
60
79
|
token = editor.password_reset_token
|
|
61
80
|
patch admin_user_password_reset_path(token), params: { password: "short" }
|
|
@@ -9,6 +9,12 @@ RSpec.describe "Admin sign-in", type: :request do
|
|
|
9
9
|
expect(response).to have_http_status(:ok)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
it "keeps the sign-in actions in the card, out of the sticky action bar" do
|
|
13
|
+
get new_admin_user_session_path
|
|
14
|
+
|
|
15
|
+
expect(response.body).to include("c-trek--form--actions-aspect-seamless")
|
|
16
|
+
end
|
|
17
|
+
|
|
12
18
|
it "signs a privileged user in and opens the dashboard" do
|
|
13
19
|
post admin_user_session_path, params: { email: editor.email, password: users.password }
|
|
14
20
|
follow_redirect!
|
|
@@ -16,6 +22,12 @@ RSpec.describe "Admin sign-in", type: :request do
|
|
|
16
22
|
expect(response).to have_http_status(:ok)
|
|
17
23
|
end
|
|
18
24
|
|
|
25
|
+
it "signs in whatever the case of the email" do
|
|
26
|
+
post admin_user_session_path, params: { email: editor.email.upcase, password: users.password }
|
|
27
|
+
|
|
28
|
+
expect(response).to redirect_to(admin_root_url)
|
|
29
|
+
end
|
|
30
|
+
|
|
19
31
|
it "refuses a wrong password" do
|
|
20
32
|
post admin_user_session_path, params: { email: editor.email, password: "not the password" }
|
|
21
33
|
|
data/lib/trek/version.rb
CHANGED
data/lib/trek.rb
CHANGED
data/package.json
CHANGED
data/trek.gemspec
CHANGED
|
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_dependency "closure_tree", "~> 9.0"
|
|
36
36
|
spec.add_dependency "deepl-rb", "~> 3.0"
|
|
37
37
|
spec.add_dependency "dry-initializer", "~> 3.1"
|
|
38
|
+
spec.add_dependency "email_validator", "~> 2.2"
|
|
38
39
|
spec.add_dependency "fastimage", "~> 2.2"
|
|
39
40
|
spec.add_dependency "http_accept_language", "~> 2.1"
|
|
40
41
|
spec.add_dependency "i18n-tasks", "~> 1.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trek
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.3.
|
|
4
|
+
version: 2.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mohamed Bengrich
|
|
@@ -124,6 +124,20 @@ dependencies:
|
|
|
124
124
|
- - "~>"
|
|
125
125
|
- !ruby/object:Gem::Version
|
|
126
126
|
version: '3.1'
|
|
127
|
+
- !ruby/object:Gem::Dependency
|
|
128
|
+
name: email_validator
|
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - "~>"
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: '2.2'
|
|
134
|
+
type: :runtime
|
|
135
|
+
prerelease: false
|
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - "~>"
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '2.2'
|
|
127
141
|
- !ruby/object:Gem::Dependency
|
|
128
142
|
name: fastimage
|
|
129
143
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -781,11 +795,16 @@ files:
|
|
|
781
795
|
- docs/.vitepress/config.mjs
|
|
782
796
|
- docs/.vitepress/theme/index.js
|
|
783
797
|
- docs/.vitepress/theme/style.css
|
|
798
|
+
- docs/changelog.md
|
|
784
799
|
- docs/customize.md
|
|
785
800
|
- docs/deploy.md
|
|
786
801
|
- docs/getting-started.md
|
|
787
802
|
- docs/index.md
|
|
803
|
+
- docs/public/apple-touch-icon.png
|
|
804
|
+
- docs/public/favicon.ico
|
|
805
|
+
- docs/public/favicon.png
|
|
788
806
|
- docs/public/logo.svg
|
|
807
|
+
- docs/public/trek-mark.webp
|
|
789
808
|
- docs/reference/components.md
|
|
790
809
|
- docs/reference/concerns/contentable.md
|
|
791
810
|
- docs/reference/concerns/formattable.md
|