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.
- checksums.yaml +4 -4
- data/.slim-lint.yml +0 -3
- data/AGENTS.md +8 -1
- data/CHANGELOG.md +75 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +20 -3
- data/Makefile +11 -9
- data/README.md +1 -1
- data/app/assets/icons/trek/audit.svg +1 -0
- data/app/assets/icons/trek/chevron-down.svg +1 -0
- data/app/assets/icons/trek/chevron-up.svg +1 -0
- data/app/assets/icons/trek/external.svg +1 -0
- data/app/assets/icons/trek/print.svg +1 -0
- data/app/assets/icons/trek/theme-auto.svg +1 -0
- data/app/assets/icons/trek/theme-dark.svg +1 -0
- data/app/assets/icons/trek/theme-light.svg +1 -0
- data/app/assets/stylesheets/trek/base.css +13 -0
- data/app/components/trek/button_component/button_component.css +9 -0
- data/app/components/trek/button_component.rb +13 -1
- data/app/components/trek/dashboard/analytics_component/analytics_component.html.slim +2 -0
- data/app/components/trek/dashboard/analytics_component.rb +1 -0
- data/app/components/trek/dashboard/chart_component/chart_component.css +192 -0
- data/app/components/trek/dashboard/chart_component/chart_component.html.slim +6 -0
- data/app/components/trek/dashboard/chart_component/chart_component.js +1 -0
- data/app/components/trek/dashboard/chart_component.rb +27 -0
- data/app/components/trek/dashboard/tile_component/tile_component.css +40 -0
- data/app/components/trek/dashboard/tile_component/tile_component.html.slim +14 -4
- data/app/components/trek/dashboard/tile_component.rb +20 -2
- data/app/components/trek/dialog_component/dialog_component.html.slim +6 -2
- data/app/components/trek/dialog_component/dialog_component.js +62 -2
- data/app/components/trek/dropdown_component/dropdown_component.css +66 -0
- data/app/components/trek/dropdown_component/dropdown_component.html.slim +31 -0
- data/app/components/trek/dropdown_component/dropdown_component.js +114 -0
- data/app/components/trek/dropdown_component.rb +29 -0
- data/app/components/trek/form/actions_component/actions_component.css +6 -7
- data/app/components/trek/form/content_editor_component/content_editor_component.html.slim +10 -12
- data/app/components/trek/form/content_editor_component/content_editor_component.js +4 -0
- data/app/components/trek/form/group_component/group_component.css +34 -0
- data/app/components/trek/header_component/header_component.css +10 -4
- data/app/components/trek/header_component/header_component.html.slim +17 -10
- data/app/components/trek/header_component/header_component.js +29 -5
- data/app/components/trek/header_component.rb +20 -3
- data/app/components/trek/layout_component/layout_component.css +20 -0
- data/app/components/trek/layout_component/layout_component.html.slim +16 -0
- data/app/components/trek/layout_component/layout_component.js +35 -6
- data/app/components/trek/menu_component/menu_component.css +6 -0
- data/app/components/trek/menu_component.rb +29 -4
- data/app/components/trek/toaster/toast_component.rb +1 -1
- data/app/controllers/concerns/trek/authorization.rb +12 -0
- data/app/formatters/prosemirror_to_html/global_id_to_links_formatter.rb +3 -1
- data/app/helpers/trek/page_path_helper.rb +4 -3
- data/app/helpers/trek/theme_helper.rb +16 -1
- data/app/models/concerns/trek/phonable.rb +15 -3
- data/app/models/concerns/trek/sectionable.rb +5 -0
- data/app/models/concerns/trek/users/validations.rb +1 -1
- data/app/models/prosemirror.rb +5 -2
- data/app/uploaders/audio_uploader.rb +11 -0
- data/app/uploaders/record_image_uploader.rb +3 -0
- data/app/views/trek/panels/images/_form.html.slim +1 -1
- data/app/views/trek/panels/images/create.turbo_stream.slim +3 -3
- data/app/views/trek/panels/images/new.html.slim +1 -1
- data/app/views/trek/panels/links/_form.html.slim +1 -1
- data/app/views/trek/panels/links/create.turbo_stream.slim +3 -3
- data/app/views/trek/panels/links/new.html.slim +1 -1
- data/app/views/trek/panels/prompts/_form.html.slim +1 -1
- data/app/views/trek/panels/prompts/create.turbo_stream.slim +3 -3
- data/app/views/trek/panels/prompts/new.html.slim +1 -1
- data/config/locales/trek.en.yml +3 -2
- data/config/locales/trek.fr.yml +1 -0
- data/docs/customize.md +18 -2
- data/docs/deploy.md +7 -2
- data/docs/getting-started.md +1 -1
- data/docs/index.md +1 -1
- data/docs/reference/components.md +15 -0
- data/docs/reference/concerns/phonable.md +27 -1
- data/docs/reference/environment-variables.md +2 -0
- data/docs/reference/forms.md +2 -0
- data/docs/reference/generators/install.md +4 -1
- data/docs/reference/icons.md +1 -1
- data/docs/reference/models/tag.md +1 -1
- data/docs/reference/models/user.md +4 -2
- data/docs/reference/uploaders.md +31 -6
- data/docs/what-is-trek.md +1 -1
- data/lib/generators/trek/install/admin/routes_generator.rb +7 -1
- data/lib/generators/trek/install/appsignal_generator.rb +102 -0
- data/lib/generators/trek/install/model_specs_generator.rb +64 -0
- data/lib/generators/trek/install/postmark_generator.rb +3 -0
- data/lib/generators/trek/install/ruby_generator.rb +1 -1
- data/lib/generators/trek/install/scalingo_generator.rb +1 -1
- data/lib/generators/trek/install/uploaders_generator.rb +2 -0
- data/lib/generators/trek/install_generator.rb +5 -0
- data/lib/generators/trek/taxonomies_generator.rb +7 -0
- data/lib/generators/trek/templates/.gitlab-ci.yml +1 -1
- data/lib/generators/trek/templates/config/appsignal.rb.tt +25 -0
- data/lib/generators/trek/templates/controllers/admin/uploads_controller.rb +4 -0
- data/lib/generators/trek/templates/controllers/admin/users_controller.rb +4 -1
- data/lib/generators/trek/templates/controllers/concerns/appsignal_context.rb +37 -0
- data/lib/generators/trek/templates/db/seeds/setup/fragments.rb +3 -0
- data/lib/generators/trek/templates/db/seeds/setup/users.rb +6 -0
- data/lib/generators/trek/templates/db/seeds/test/cases/page_tree.rb +5 -0
- data/lib/generators/trek/templates/initializers/appsignal_context.rb +5 -0
- data/lib/generators/trek/templates/locales/scaffold/model.en.yml.tt +1 -1
- data/lib/generators/trek/templates/migrations/fragments.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/menu_nodes.rb.erb +0 -1
- data/lib/generators/trek/templates/migrations/tag_categories.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/taggings.rb.erb +1 -1
- data/lib/generators/trek/templates/migrations/users.rb.erb +2 -1
- data/lib/generators/trek/templates/models/tag.rb +6 -1
- data/lib/generators/trek/templates/policies/admin/tag_policy.rb +1 -1
- data/lib/generators/trek/templates/policies/external_link_policy.rb +1 -1
- data/lib/generators/trek/templates/spec/fixtures/files/image.png +0 -0
- data/lib/generators/trek/templates/spec/fixtures/files/sound.mp3 +0 -0
- data/lib/generators/trek/templates/spec/models/page_spec.rb +24 -0
- data/lib/generators/trek/templates/spec/models/user_spec.rb +76 -0
- data/lib/generators/trek/templates/spec/policies/admin/external_link_policy_spec.rb +17 -0
- data/lib/generators/trek/templates/spec/policies/admin/fragment_policy_spec.rb +33 -0
- data/lib/generators/trek/templates/spec/policies/admin/page_policy_spec.rb +39 -0
- data/lib/generators/trek/templates/spec/policies/admin/preview_policy_spec.rb +9 -0
- data/lib/generators/trek/templates/spec/policies/admin/tag_category_policy_spec.rb +31 -0
- data/lib/generators/trek/templates/spec/policies/admin/tag_policy_spec.rb +31 -0
- data/lib/generators/trek/templates/spec/policies/admin/user_policy_spec.rb +58 -0
- data/lib/generators/trek/templates/spec/requests/admin/fragments_spec.rb +50 -0
- data/lib/generators/trek/templates/spec/requests/admin/pages_spec.rb +47 -0
- data/lib/generators/trek/templates/spec/requests/admin/uploads_spec.rb +22 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_password_resets_spec.rb +71 -0
- data/lib/generators/trek/templates/spec/requests/admin/user_sessions_spec.rb +45 -0
- data/lib/generators/trek/templates/spec/requests/admin/users_spec.rb +99 -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 +2 -2
- data/lib/generators/trek/templates/views/admin/users/theme.turbo_stream.slim +2 -0
- data/lib/generators/trek/upgrade/v2_generator.rb +3 -0
- data/lib/tasks/trek/attachments.rake +18 -0
- data/lib/trek/cached_attachments.rb +53 -0
- data/lib/trek/saved_attachments_reload.rb +44 -0
- data/lib/trek/version.rb +1 -1
- data/lib/trek.rb +3 -0
- data/package.json +1 -1
- data/postcss.config.js +36 -0
- data/trek.gemspec +5 -0
- metadata +76 -3
- data/app/assets/icons/trek/theme.svg +0 -11
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Trek
|
|
2
|
+
class DropdownComponent < Trek::Component
|
|
3
|
+
option :label, optional: true # accessible name (aria-label on trigger + menu)
|
|
4
|
+
option :classnames, optional: true # extra classes on root
|
|
5
|
+
option :hide_chevron, default: proc { false }
|
|
6
|
+
|
|
7
|
+
renders_one :trigger # inner content of the trigger button (e.g. an icon)
|
|
8
|
+
renders_many :items, "ItemComponent"
|
|
9
|
+
|
|
10
|
+
def classes
|
|
11
|
+
class_names_for(root_class, classnames)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def menu_id
|
|
15
|
+
@menu_id ||= "#{identifier}-menu-#{SecureRandom.hex(4)}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ItemComponent < Trek::Component
|
|
19
|
+
option :data, default: -> { {} }
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
attrs = { class: class_for("root"), role: "menuitem", tabindex: -1, data: }
|
|
23
|
+
tag.li(**attrs) do
|
|
24
|
+
content
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -2,14 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
.root {
|
|
4
4
|
align-items: center;
|
|
5
|
-
background-color: var(--accent-1);
|
|
6
|
-
bottom: 0;
|
|
7
5
|
display: flex;
|
|
8
6
|
max-width: 120rem;
|
|
9
|
-
padding: 4rem 0;
|
|
10
|
-
position: sticky;
|
|
11
|
-
width: 100%;
|
|
12
|
-
z-index: 2;
|
|
13
7
|
}
|
|
14
8
|
|
|
15
9
|
.buttons {
|
|
@@ -36,9 +30,14 @@
|
|
|
36
30
|
|
|
37
31
|
/* aspect */
|
|
38
32
|
.root.aspect-default {
|
|
33
|
+
background-color: var(--accent-1);
|
|
39
34
|
border-top: 1px solid var(--slate-6);
|
|
35
|
+
bottom: 0;
|
|
40
36
|
margin-top: 3.2rem;
|
|
41
|
-
padding
|
|
37
|
+
padding: 3.2rem 0 4rem;
|
|
38
|
+
position: sticky;
|
|
39
|
+
width: 100%;
|
|
40
|
+
z-index: 2;
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/* .root.aspect-seamless {
|
|
@@ -29,16 +29,14 @@ div(
|
|
|
29
29
|
/ data-action=stimulus_action("toggleCode")
|
|
30
30
|
/ ) code
|
|
31
31
|
|
|
32
|
-
/
|
|
32
|
+
/ The panel opening is handled by the hidden button's
|
|
33
|
+
openStackedPanelWhenLoaded action — a direct openPanel here would
|
|
34
|
+
pop the (possibly empty) primary panel alongside the stacked one.
|
|
33
35
|
button.link(
|
|
34
36
|
class=class_for("button")
|
|
35
37
|
type="button"
|
|
36
|
-
data-action=
|
|
37
|
-
stimulus_action("toggleLink"),
|
|
38
|
-
"trek--layout#openPanel",
|
|
39
|
-
].join(" ")
|
|
38
|
+
data-action=stimulus_action("toggleLink")
|
|
40
39
|
) = render Trek::IconComponent.new("trek/link", title: t(".link"))
|
|
41
|
-
/ slim-lint:enable ControlStatementSpacing
|
|
42
40
|
|
|
43
41
|
= link_to "",
|
|
44
42
|
new_link_path,
|
|
@@ -46,8 +44,8 @@ div(
|
|
|
46
44
|
hidden: true,
|
|
47
45
|
data: stimulus_target_hash("hiddenLinkButton").merge( \
|
|
48
46
|
'base-path': new_link_path,
|
|
49
|
-
action: "trek--layout#
|
|
50
|
-
'turbo-frame': "trek--layout-panel",
|
|
47
|
+
action: "trek--layout#openStackedPanelWhenLoaded",
|
|
48
|
+
'turbo-frame': "trek--layout-stacked-panel",
|
|
51
49
|
)
|
|
52
50
|
|
|
53
51
|
button(
|
|
@@ -239,8 +237,8 @@ div(
|
|
|
239
237
|
hidden: true,
|
|
240
238
|
data: stimulus_target_hash("hiddenPromptsButton").merge( \
|
|
241
239
|
'base-path': new_prompt_path,
|
|
242
|
-
action: "trek--layout#
|
|
243
|
-
'turbo-frame': "trek--layout-panel",
|
|
240
|
+
action: "trek--layout#openStackedPanelWhenLoaded",
|
|
241
|
+
'turbo-frame': "trek--layout-stacked-panel",
|
|
244
242
|
)
|
|
245
243
|
= link_to "",
|
|
246
244
|
new_image_path,
|
|
@@ -248,6 +246,6 @@ div(
|
|
|
248
246
|
hidden: true,
|
|
249
247
|
data: stimulus_target_hash("hiddenImageButton").merge( \
|
|
250
248
|
'base-path': new_image_path,
|
|
251
|
-
action: "trek--layout#
|
|
252
|
-
'turbo-frame': "trek--layout-panel",
|
|
249
|
+
action: "trek--layout#openStackedPanelWhenLoaded",
|
|
250
|
+
'turbo-frame': "trek--layout-stacked-panel",
|
|
253
251
|
)
|
|
@@ -41,6 +41,10 @@ export class Controller extends BaseController {
|
|
|
41
41
|
// }),
|
|
42
42
|
Link.configure({
|
|
43
43
|
openOnClick: false,
|
|
44
|
+
// Internal links are stored as GlobalIDs (resolved to URLs at render
|
|
45
|
+
// time); without this the gid:// scheme fails Link's URI validation
|
|
46
|
+
// and setLink silently does nothing.
|
|
47
|
+
protocols: ["gid"],
|
|
44
48
|
HTMLAttributes: {
|
|
45
49
|
"data-action": "trek--form--content-editor#selectLink",
|
|
46
50
|
target: null,
|
|
@@ -92,6 +92,40 @@
|
|
|
92
92
|
color: var(--slate-12);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
.root input[type="file"] {
|
|
96
|
+
color: var(--slate-12);
|
|
97
|
+
font-size: 1.4rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Mirrors Trek::ButtonComponent with size: :narrow — keep in sync with
|
|
101
|
+
button_component/button_component.css (the scoped class can't be reused here) */
|
|
102
|
+
.root input[type="file"]::file-selector-button {
|
|
103
|
+
appearance: none;
|
|
104
|
+
background-color: transparent;
|
|
105
|
+
border: 1px solid var(--slate-6);
|
|
106
|
+
border-radius: 0.8rem;
|
|
107
|
+
color: var(--slate-12);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
font-size: 1.6rem;
|
|
110
|
+
font-weight: var(--semibold);
|
|
111
|
+
line-height: 1.25;
|
|
112
|
+
margin-right: 1.6rem;
|
|
113
|
+
padding: 1rem 2.8rem;
|
|
114
|
+
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out,
|
|
115
|
+
color 0.2s ease-in-out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.root input[type="file"]::file-selector-button:hover {
|
|
119
|
+
background-color: var(--accent-3);
|
|
120
|
+
border-color: var(--accent-5);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.root input[type="file"]::file-selector-button:active {
|
|
124
|
+
background-color: var(--accent-4);
|
|
125
|
+
border-color: var(--accent-6);
|
|
126
|
+
color: var(--accent-12);
|
|
127
|
+
}
|
|
128
|
+
|
|
95
129
|
.root input:disabled,
|
|
96
130
|
.root select:disabled,
|
|
97
131
|
.root textarea:disabled {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
align-items: center;
|
|
41
41
|
border-radius: 0.8rem;
|
|
42
42
|
display: flex;
|
|
43
|
+
gap: 0.8rem;
|
|
43
44
|
padding: 1rem 1.6rem;
|
|
44
45
|
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
|
|
45
46
|
}
|
|
@@ -52,10 +53,6 @@
|
|
|
52
53
|
cursor: pointer;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
.nav-item-link .nav-item-icon {
|
|
56
|
-
margin-left: 0.8rem;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
56
|
.nav-item-link:hover,
|
|
60
57
|
.nav-item-button:hover {
|
|
61
58
|
background-color: var(--accent-3);
|
|
@@ -69,6 +66,15 @@
|
|
|
69
66
|
text-overflow: ellipsis;
|
|
70
67
|
}
|
|
71
68
|
|
|
69
|
+
/* Theme Dropdown */
|
|
70
|
+
.nav-item-link:global(.c-trek--dropdown-root) {
|
|
71
|
+
padding: 0.8rem 1.6rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.nav-item-link:global(.c-trek--dropdown-root .c-trek--icon-root) {
|
|
75
|
+
--icon-size: 2rem;
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
/* Media queries */
|
|
73
79
|
|
|
74
80
|
@media (max-width: 768px) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
div(class=classes data-controller=identifier
|
|
1
|
+
div(class=classes data-controller=identifier \
|
|
2
|
+
data=stimulus_value_hash("url", theme_admin_users_path))
|
|
2
3
|
= render Trek::BrandComponent.new
|
|
3
4
|
nav(class=class_for("nav"))
|
|
4
5
|
ul(class=class_for("nav-items"))
|
|
@@ -31,12 +32,18 @@ div(class=classes data-controller=identifier)
|
|
|
31
32
|
"trek/profile",
|
|
32
33
|
classnames: class_for("nav-item-icon"),
|
|
33
34
|
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
li(class=class_for("nav-item"))
|
|
36
|
+
= render Trek::DropdownComponent.new( \
|
|
37
|
+
label: User.human_attribute_name(:theme),
|
|
38
|
+
classnames: class_for("nav-item-link"),
|
|
39
|
+
hide_chevron: true,
|
|
40
|
+
) do |dropdown|
|
|
41
|
+
- dropdown.with_trigger do
|
|
42
|
+
= helpers.theme_icon(current_user.theme)
|
|
43
|
+
- %w[auto light dark].each do |theme|
|
|
44
|
+
= dropdown.with_item(data: theme_item_data(theme)) do
|
|
45
|
+
= render Trek::IconComponent.new( \
|
|
46
|
+
"trek/theme-#{theme}",
|
|
47
|
+
classnames: class_for("nav-item-icon"),
|
|
48
|
+
)
|
|
49
|
+
span = t("user.theme.#{theme}")
|
|
@@ -3,11 +3,35 @@ import "./header_component.css";
|
|
|
3
3
|
import { Controller as BaseController } from "@hotwired/stimulus";
|
|
4
4
|
|
|
5
5
|
export class Controller extends BaseController {
|
|
6
|
-
|
|
7
|
-
const dark = document.documentElement.classList.toggle("dark-theme");
|
|
8
|
-
document.documentElement.classList.toggle("light-theme");
|
|
6
|
+
static values = { url: String };
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
setTheme(event) {
|
|
9
|
+
const theme = event.currentTarget.dataset.theme;
|
|
10
|
+
if (!theme) return;
|
|
11
|
+
|
|
12
|
+
const root = document.documentElement;
|
|
13
|
+
root.classList.remove("auto-theme", "light-theme", "dark-theme");
|
|
14
|
+
root.classList.add(`${theme}-theme`);
|
|
15
|
+
|
|
16
|
+
this.persist(theme);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async persist(theme) {
|
|
20
|
+
if (!this.hasUrlValue) return;
|
|
21
|
+
|
|
22
|
+
const token = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
23
|
+
const response = await fetch(this.urlValue, {
|
|
24
|
+
method: "PATCH",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"X-CSRF-Token": token,
|
|
28
|
+
Accept: "text/vnd.turbo-stream.html",
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ theme }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// A manual fetch is not processed by Turbo automatically; render the
|
|
34
|
+
// returned stream ourselves to refresh the server-rendered theme icon.
|
|
35
|
+
if (response.ok) window.Turbo.renderStreamMessage(await response.text());
|
|
12
36
|
}
|
|
13
37
|
}
|
|
@@ -4,6 +4,7 @@ module Trek
|
|
|
4
4
|
class HeaderComponent < Trek::Component
|
|
5
5
|
extend Dry::Initializer
|
|
6
6
|
include ActionPolicy::Behaviour
|
|
7
|
+
include Trek::PagePathHelper
|
|
7
8
|
|
|
8
9
|
option :current_object, optional: true
|
|
9
10
|
option :current_user, optional: true
|
|
@@ -25,14 +26,32 @@ module Trek
|
|
|
25
26
|
current_object&.persisted? && (
|
|
26
27
|
allowed_to?(:preview?, current_object, namespace: Admin, strict_namespace: true) ||
|
|
27
28
|
(current_object_page? || current_object.respond_to?(:page))
|
|
28
|
-
)
|
|
29
|
+
) && preview_path.present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def theme_item_data(theme)
|
|
33
|
+
{ action: stimulus_action("click", "setTheme"), theme: }
|
|
29
34
|
end
|
|
30
35
|
|
|
31
36
|
def can_preview_home?
|
|
32
37
|
Admin::PreviewPolicy.new(user: current_user).apply(:home?)
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
# Where the current object shows on the website, or nil when the app has no front-office route
|
|
41
|
+
# for it (a freshly generated app has none): the link is then left out rather than raising.
|
|
35
42
|
def preview_path
|
|
43
|
+
return @preview_path if defined?(@preview_path)
|
|
44
|
+
|
|
45
|
+
@preview_path = website_path
|
|
46
|
+
rescue NoMethodError => e
|
|
47
|
+
raise unless e.receiver.equal?(self) && e.name.to_s.end_with?("_path", "_url")
|
|
48
|
+
|
|
49
|
+
@preview_path = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def website_path
|
|
36
55
|
return current_object unless defined?(Page) && defined?(PageVersion)
|
|
37
56
|
|
|
38
57
|
case current_object
|
|
@@ -50,8 +69,6 @@ module Trek
|
|
|
50
69
|
end
|
|
51
70
|
end
|
|
52
71
|
|
|
53
|
-
private
|
|
54
|
-
|
|
55
72
|
def current_object_page?
|
|
56
73
|
defined?(Page) && current_object.is_a?(Page)
|
|
57
74
|
end
|
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
transform: translateX(0);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/* Stacked panel: overlays the primary panel. */
|
|
38
|
+
.panel-stacked {
|
|
39
|
+
box-shadow: 0 0 16px var(--slate-a8);
|
|
40
|
+
z-index: 3;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Only when it actually stacks on an open primary panel, make it 1rem
|
|
44
|
+
narrower so a sliver of the panel underneath stays visible. */
|
|
45
|
+
.panel.open + .panel-stacked {
|
|
46
|
+
width: 41rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
.panel-close {
|
|
38
50
|
--icon-size: 1.6rem;
|
|
39
51
|
|
|
@@ -65,4 +77,12 @@
|
|
|
65
77
|
.content {
|
|
66
78
|
margin: 6rem 3rem;
|
|
67
79
|
}
|
|
80
|
+
|
|
81
|
+
.panel {
|
|
82
|
+
width: 100vw;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.panel.open + .panel-stacked {
|
|
86
|
+
width: calc(100vw - 1rem);
|
|
87
|
+
}
|
|
68
88
|
}
|
|
@@ -18,3 +18,19 @@ div(
|
|
|
18
18
|
data-action=stimulus_action("closePanel")
|
|
19
19
|
)
|
|
20
20
|
= render(Trek::IconComponent.new("trek/close"))
|
|
21
|
+
/ Stacked panel: opens on top of the primary panel, so a panel-hosted form
|
|
22
|
+
(e.g. a content editor in a section panel) survives sub-panels like the
|
|
23
|
+
link/image/prompt inserters.
|
|
24
|
+
aside(
|
|
25
|
+
class=[class_for("panel"), class_for("panel-stacked")]
|
|
26
|
+
data=stimulus_target_hash("stackedPanel")
|
|
27
|
+
)
|
|
28
|
+
= turbo_frame_tag( \
|
|
29
|
+
"#{identifier}-stacked-panel",
|
|
30
|
+
data: stimulus_target_hash("stackedPanelFrame"),
|
|
31
|
+
)
|
|
32
|
+
button(
|
|
33
|
+
class=class_for("panel-close")
|
|
34
|
+
data-action=stimulus_action("closeStackedPanel")
|
|
35
|
+
)
|
|
36
|
+
= render(Trek::IconComponent.new("trek/close"))
|
|
@@ -5,7 +5,14 @@ import { useClickOutside } from "stimulus-use";
|
|
|
5
5
|
|
|
6
6
|
export class Controller extends BaseController {
|
|
7
7
|
static classes = ["open"];
|
|
8
|
-
static targets = [
|
|
8
|
+
static targets = [
|
|
9
|
+
"panel",
|
|
10
|
+
"panelFrame",
|
|
11
|
+
"panelCloser",
|
|
12
|
+
"stackedPanel",
|
|
13
|
+
"stackedPanelFrame",
|
|
14
|
+
"stackedPanelCloser",
|
|
15
|
+
];
|
|
9
16
|
|
|
10
17
|
connect() {
|
|
11
18
|
useClickOutside(this, { element: this.panelTarget });
|
|
@@ -17,31 +24,53 @@ export class Controller extends BaseController {
|
|
|
17
24
|
this.closePanel();
|
|
18
25
|
}
|
|
19
26
|
|
|
27
|
+
stackedPanelCloserTargetConnected() {
|
|
28
|
+
this.closeStackedPanel();
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
openPanelWhenLoaded() {
|
|
21
32
|
this.panelFrameTarget.loaded.then(() => this.openPanel());
|
|
22
33
|
}
|
|
23
34
|
|
|
35
|
+
openStackedPanelWhenLoaded() {
|
|
36
|
+
this.stackedPanelFrameTarget.loaded.then(() => this.openStackedPanel());
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
openPanel() {
|
|
25
40
|
this.panelTarget.classList.add(this.openClass);
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
openStackedPanel() {
|
|
44
|
+
this.stackedPanelTarget.classList.add(this.openClass);
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
closePanel() {
|
|
48
|
+
this.closeStackedPanel();
|
|
29
49
|
this.panelTarget.classList.remove(this.openClass);
|
|
30
|
-
this.emptyFrame();
|
|
50
|
+
this.emptyFrame(this.panelFrameTarget);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
closeStackedPanel() {
|
|
54
|
+
this.stackedPanelTarget.classList.remove(this.openClass);
|
|
55
|
+
this.emptyFrame(this.stackedPanelFrameTarget);
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
clickOutside(e) {
|
|
34
59
|
if (
|
|
35
60
|
e.target.dataset.action &&
|
|
36
|
-
e.target.dataset.action.includes("trek--layout#openPanel")
|
|
61
|
+
(e.target.dataset.action.includes("trek--layout#openPanel") ||
|
|
62
|
+
e.target.dataset.action.includes("trek--layout#openStackedPanel"))
|
|
37
63
|
)
|
|
38
64
|
return;
|
|
65
|
+
// Clicks inside the stacked panel are outside the primary panel element,
|
|
66
|
+
// but must not close anything.
|
|
67
|
+
if (this.stackedPanelTarget.contains(e.target)) return;
|
|
39
68
|
this.closePanel();
|
|
40
69
|
}
|
|
41
70
|
|
|
42
|
-
emptyFrame() {
|
|
43
|
-
while (
|
|
44
|
-
|
|
71
|
+
emptyFrame(frame) {
|
|
72
|
+
while (frame.firstChild) {
|
|
73
|
+
frame.removeChild(frame.firstChild);
|
|
45
74
|
}
|
|
46
75
|
}
|
|
47
76
|
}
|
|
@@ -62,13 +62,16 @@ module Trek
|
|
|
62
62
|
# against it, a boolean forces the state. By default the item is active when its href
|
|
63
63
|
# matches the current controller.
|
|
64
64
|
option :active, optional: true
|
|
65
|
+
option :target, optional: true
|
|
65
66
|
|
|
66
67
|
def call
|
|
67
68
|
tag.li(
|
|
68
69
|
link_to(
|
|
69
|
-
|
|
70
|
+
rendered_text,
|
|
70
71
|
href,
|
|
71
|
-
class: class_for("link")
|
|
72
|
+
class: class_for("link"),
|
|
73
|
+
target:,
|
|
74
|
+
rel:
|
|
72
75
|
),
|
|
73
76
|
class: class_names_for(
|
|
74
77
|
root_class,
|
|
@@ -83,6 +86,24 @@ module Trek
|
|
|
83
86
|
|
|
84
87
|
private
|
|
85
88
|
|
|
89
|
+
def rendered_text
|
|
90
|
+
safe_join([text, icon].compact)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def icon
|
|
94
|
+
return unless new_tab?
|
|
95
|
+
|
|
96
|
+
render IconComponent.new("trek/external", classnames: class_for("icon"))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rel
|
|
100
|
+
"noopener" if new_tab?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def new_tab?
|
|
104
|
+
target == "_blank"
|
|
105
|
+
end
|
|
106
|
+
|
|
86
107
|
def active?
|
|
87
108
|
case active
|
|
88
109
|
when nil then href_path == controller_path
|
|
@@ -93,8 +114,12 @@ module Trek
|
|
|
93
114
|
end
|
|
94
115
|
|
|
95
116
|
def href_path
|
|
96
|
-
Array.wrap(href).
|
|
97
|
-
part.respond_to?(:model_name) ? part.model_name.collection : part
|
|
117
|
+
Array.wrap(href).inject([]) do |parts, part|
|
|
118
|
+
segments = part.respond_to?(:model_name) ? part.model_name.collection.split("/") : [part.to_s]
|
|
119
|
+
# A namespaced model's collection already contains the namespace symbols the
|
|
120
|
+
# route helper needs before it — drop the overlap instead of doubling it.
|
|
121
|
+
overlap = (segments.length - 1).downto(0).find { |n| parts.last(n) == segments.first(n) }
|
|
122
|
+
parts + segments.drop(overlap)
|
|
98
123
|
end.join("/").sub("root", "dashboards")
|
|
99
124
|
end
|
|
100
125
|
end
|
|
@@ -4,6 +4,8 @@ module Trek
|
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
6
|
helper_method :policy_for, :policy_permitted_attribute?
|
|
7
|
+
|
|
8
|
+
rescue_from ActionPolicy::Unauthorized, with: :deny_access
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
private
|
|
@@ -11,5 +13,15 @@ module Trek
|
|
|
11
13
|
def policy_permitted_attribute?(name)
|
|
12
14
|
policy_for(record: @object).permitted_attribute?(name)
|
|
13
15
|
end
|
|
16
|
+
|
|
17
|
+
# A signed-in user opening what their role doesn't allow (an editor on the users panel…) goes back
|
|
18
|
+
# where they came from, or to the dashboard, with an alert, rather than to an error page. Other
|
|
19
|
+
# formats (Turbo Streams, JSON) get a plain 403.
|
|
20
|
+
def deny_access
|
|
21
|
+
respond_to do |format|
|
|
22
|
+
format.html { redirect_back_or_to admin_root_path, alert: t("notices.unauthorized") }
|
|
23
|
+
format.any { head :forbidden }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
14
26
|
end
|
|
15
27
|
end
|
|
@@ -2,7 +2,9 @@ module ProsemirrorToHtml
|
|
|
2
2
|
class GlobalIdToLinksFormatter
|
|
3
3
|
include Rails.application.routes.url_helpers
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# The app segment allows hyphens and dots: GlobalID derives it from the
|
|
6
|
+
# Rails application name (e.g. "au-poste").
|
|
7
|
+
GLOBAL_ID_REGEX = %r{(gid://[\w.-]+/[\w:]+/[\w-]+)}
|
|
6
8
|
|
|
7
9
|
def initialize(str)
|
|
8
10
|
@str = str
|
|
@@ -13,10 +13,11 @@ module Trek
|
|
|
13
13
|
raise "#{object} is not a page" unless object.is_a?(Page)
|
|
14
14
|
|
|
15
15
|
if object.pageable.present?
|
|
16
|
-
|
|
16
|
+
# Same key as Rails' polymorphic routes: `shop_product` for Shop::Product, `post` for an isolated engine's Blog::Post
|
|
17
|
+
route_key = object.pageable.model_name.singular_route_key
|
|
17
18
|
id = object.slug.presence || object.to_param
|
|
18
19
|
|
|
19
|
-
id ? send(:"#{
|
|
20
|
+
id ? send(:"#{route_key}_#{suffix}", id:, **options) : nil
|
|
20
21
|
else
|
|
21
22
|
begin
|
|
22
23
|
key = object.key.presence
|
|
@@ -27,7 +28,7 @@ module Trek
|
|
|
27
28
|
send(:"localized_root_#{suffix}", locale:, **options)
|
|
28
29
|
end
|
|
29
30
|
else
|
|
30
|
-
send(:"#{key}
|
|
31
|
+
send(:"#{key}_#{suffix}", **options)
|
|
31
32
|
end
|
|
32
33
|
rescue NoMethodError
|
|
33
34
|
I18n.with_locale(locale) do
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
module Trek
|
|
2
2
|
module ThemeHelper
|
|
3
|
+
# DOM id of the header's theme indicator, targeted by the theme turbo_stream.
|
|
4
|
+
THEME_ICON_ID = "header-theme-icon".freeze
|
|
5
|
+
|
|
3
6
|
def class_for_theme
|
|
4
7
|
if authenticated?
|
|
5
8
|
"#{Current.user.theme}-theme"
|
|
6
9
|
else
|
|
7
|
-
"
|
|
10
|
+
"auto-theme"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Renders the theme indicator used in the header trigger. Wrapped in a
|
|
15
|
+
# stable-id element so the +theme+ turbo_stream can replace it after a
|
|
16
|
+
# theme change. Used both on initial render and in the stream response.
|
|
17
|
+
def theme_icon(theme)
|
|
18
|
+
tag.span(id: THEME_ICON_ID) do
|
|
19
|
+
render Trek::IconComponent.new(
|
|
20
|
+
"trek/theme-#{theme}",
|
|
21
|
+
classnames: "c-trek--header-nav-item-icon"
|
|
22
|
+
)
|
|
8
23
|
end
|
|
9
24
|
end
|
|
10
25
|
end
|
|
@@ -10,14 +10,26 @@ module Trek
|
|
|
10
10
|
module Phonable
|
|
11
11
|
extend ActiveSupport::Concern
|
|
12
12
|
|
|
13
|
+
STRICTNESSES = %i[possible valid].freeze
|
|
14
|
+
|
|
13
15
|
class_methods do
|
|
14
|
-
|
|
16
|
+
# `strictness:` picks how hard Phonelib looks. `:possible`, the default, only asks whether the
|
|
17
|
+
# digits could form a number for the country; `:valid` asks whether they fall in a range the
|
|
18
|
+
# regulator has actually assigned. The gap is not academic: with a French default country,
|
|
19
|
+
# "0012345678" is possible, so it is accepted and normalized to "+12345678" — the stray leading
|
|
20
|
+
# zero read as an international prefix, the number silently rehomed to country code +1. An app
|
|
21
|
+
# that intends to call the numbers it collects wants `:valid`.
|
|
22
|
+
def phonable_attributes(*attrs, strictness: :possible)
|
|
23
|
+
unless STRICTNESSES.include?(strictness)
|
|
24
|
+
raise ArgumentError, "unknown strictness #{strictness.inspect}, expected one of #{STRICTNESSES.inspect}"
|
|
25
|
+
end
|
|
26
|
+
|
|
15
27
|
@__phonable_attributes ||= []
|
|
16
28
|
@__phonable_attributes += attrs
|
|
17
29
|
|
|
18
30
|
attrs.each do |attr|
|
|
19
31
|
validates attr,
|
|
20
|
-
phone: { possible:
|
|
32
|
+
phone: { possible: strictness == :possible, allow_blank: true }
|
|
21
33
|
|
|
22
34
|
define_method :"formatted_#{attr}" do
|
|
23
35
|
return if send(attr).blank?
|
|
@@ -42,7 +54,7 @@ module Trek
|
|
|
42
54
|
|
|
43
55
|
def normalize_phone
|
|
44
56
|
self.class.instance_variable_get(:@__phonable_attributes).each do |attr|
|
|
45
|
-
send(:"#{attr}=", Phonelib.parse(
|
|
57
|
+
send(:"#{attr}=", Phonelib.parse(send(attr)).full_e164.presence)
|
|
46
58
|
end
|
|
47
59
|
end
|
|
48
60
|
end
|
|
@@ -9,6 +9,11 @@ module Trek
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
included do
|
|
12
|
+
# Sections are the host app's own `Section` model, which Trek doesn't generate: without one, a
|
|
13
|
+
# record has no sections, and declaring the association anyway would make `destroy` raise
|
|
14
|
+
# (`dependent: :destroy`). `const_defined?` also sees a model Zeitwerk hasn't loaded yet.
|
|
15
|
+
next unless Object.const_defined?(:Section)
|
|
16
|
+
|
|
12
17
|
has_many :sections,
|
|
13
18
|
-> { ordered },
|
|
14
19
|
as: :sectionable,
|