mat_views 0.2.0 → 0.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/README.md +4 -4
- data/app/assets/images/mat_views/android-chrome-192x192.png +0 -0
- data/app/assets/images/mat_views/android-chrome-512x512.png +0 -0
- data/app/assets/images/mat_views/apple-touch-icon.png +0 -0
- data/app/assets/images/mat_views/favicon-16x16.png +0 -0
- data/app/assets/images/mat_views/favicon-32x32.png +0 -0
- data/app/assets/images/mat_views/favicon-48x48.png +0 -0
- data/app/assets/images/mat_views/favicon.ico +0 -0
- data/app/assets/images/mat_views/favicon.svg +18 -0
- data/app/assets/images/mat_views/logo.svg +18 -0
- data/app/assets/images/mat_views/mask-icon.svg +5 -0
- data/app/assets/stylesheets/mat_views/application.css +323 -12
- data/app/controllers/mat_views/admin/application_controller.rb +135 -0
- data/app/controllers/mat_views/admin/dashboard_controller.rb +32 -0
- data/app/controllers/mat_views/admin/mat_view_definitions_controller.rb +248 -0
- data/app/controllers/mat_views/admin/preferences_controller.rb +91 -0
- data/app/controllers/mat_views/admin/runs_controller.rb +74 -0
- data/app/helpers/mat_views/admin/ui_helper.rb +385 -0
- data/app/javascript/mat_views/application.js +8 -0
- data/app/javascript/mat_views/controllers/application.js +10 -0
- data/app/javascript/mat_views/controllers/details_controller.js +122 -0
- data/app/javascript/mat_views/controllers/drawer_controller.js +252 -0
- data/app/javascript/mat_views/controllers/filter_controller.js +90 -0
- data/app/javascript/mat_views/controllers/flash_controller.js +13 -0
- data/app/javascript/mat_views/controllers/index.js +10 -0
- data/app/javascript/mat_views/controllers/mv_confirm_controller.js +281 -0
- data/app/javascript/mat_views/controllers/submitter_controller.js +15 -0
- data/app/javascript/mat_views/controllers/tabs_controller.js +67 -0
- data/app/javascript/mat_views/controllers/timezone_controller.js +16 -0
- data/app/javascript/mat_views/controllers/tooltip_controller.js +328 -0
- data/app/javascript/mat_views/controllers/turbo_frame_lifecycle_controller.js +49 -0
- data/app/jobs/mat_views/application_job.rb +2 -2
- data/app/jobs/mat_views/create_view_job.rb +9 -8
- data/app/jobs/mat_views/delete_view_job.rb +8 -8
- data/app/jobs/mat_views/refresh_view_job.rb +8 -9
- data/app/models/concerns/mat_views_i18n.rb +139 -0
- data/app/models/mat_views/application_record.rb +1 -0
- data/app/models/mat_views/mat_view_definition.rb +12 -7
- data/app/models/mat_views/mat_view_run.rb +11 -13
- data/app/views/layouts/mat_views/_footer.html.erb +41 -0
- data/app/views/layouts/mat_views/_header.html.erb +25 -0
- data/app/views/layouts/mat_views/admin.html.erb +47 -0
- data/app/views/layouts/mat_views/turbo_frame.html.erb +3 -0
- data/app/views/mat_views/admin/dashboard/index.html.erb +33 -0
- data/app/views/mat_views/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
- data/app/views/mat_views/admin/mat_view_definitions/_table.html.erb +48 -0
- data/app/views/mat_views/admin/mat_view_definitions/empty.html.erb +1 -0
- data/app/views/mat_views/admin/mat_view_definitions/form.html.erb +79 -0
- data/app/views/mat_views/admin/mat_view_definitions/index.html.erb +10 -0
- data/app/views/mat_views/admin/mat_view_definitions/show.html.erb +40 -0
- data/app/views/mat_views/admin/preferences/show.html.erb +50 -0
- data/app/views/mat_views/admin/runs/_table.html.erb +61 -0
- data/app/views/mat_views/admin/runs/index.html.erb +38 -0
- data/app/views/mat_views/admin/runs/show.html.erb +64 -0
- data/app/views/mat_views/admin/ui/_card.html.erb +15 -0
- data/app/views/mat_views/admin/ui/_details.html.erb +10 -0
- data/app/views/mat_views/admin/ui/_flash.html.erb +6 -0
- data/app/views/mat_views/admin/ui/_table.html.erb +8 -0
- data/config/importmap.rb +9 -0
- data/config/locales/en-AU-ocker.yml +187 -0
- data/config/locales/en-AU.yml +187 -0
- data/config/locales/en-BB.yml +187 -0
- data/config/locales/en-BD.yml +187 -0
- data/config/locales/en-BE.yml +187 -0
- data/config/locales/en-BORK.yml +187 -0
- data/config/locales/en-BS.yml +187 -0
- data/config/locales/en-BZ.yml +187 -0
- data/config/locales/en-CA.yml +187 -0
- data/config/locales/en-CM.yml +187 -0
- data/config/locales/en-CY.yml +187 -0
- data/config/locales/en-EG.yml +187 -0
- data/config/locales/en-FJ.yml +187 -0
- data/config/locales/en-GB.yml +187 -0
- data/config/locales/en-GH.yml +187 -0
- data/config/locales/en-GI.yml +187 -0
- data/config/locales/en-GM.yml +187 -0
- data/config/locales/en-GY.yml +187 -0
- data/config/locales/en-HK.yml +187 -0
- data/config/locales/en-IE.yml +187 -0
- data/config/locales/en-IN.yml +187 -0
- data/config/locales/en-JM.yml +187 -0
- data/config/locales/en-KE.yml +187 -0
- data/config/locales/en-LK.yml +187 -0
- data/config/locales/en-LOL.yml +187 -0
- data/config/locales/en-LR.yml +187 -0
- data/config/locales/en-MS.yml +187 -0
- data/config/locales/en-MT.yml +187 -0
- data/config/locales/en-MW.yml +187 -0
- data/config/locales/en-MY.yml +187 -0
- data/config/locales/en-NG.yml +187 -0
- data/config/locales/en-NP.yml +187 -0
- data/config/locales/en-NZ.yml +187 -0
- data/config/locales/en-PG.yml +187 -0
- data/config/locales/en-PH.yml +187 -0
- data/config/locales/en-PK.yml +187 -0
- data/config/locales/en-RW.yml +187 -0
- data/config/locales/en-SCOT.yml +187 -0
- data/config/locales/en-SG.yml +187 -0
- data/config/locales/en-SHAKESPEARE.yml +187 -0
- data/config/locales/en-SL.yml +187 -0
- data/config/locales/en-SS.yml +187 -0
- data/config/locales/en-TH.yml +187 -0
- data/config/locales/en-TT.yml +187 -0
- data/config/locales/en-TZ.yml +187 -0
- data/config/locales/en-UG.yml +187 -0
- data/config/locales/en-US-pirate.yml +187 -0
- data/config/locales/en-US.yml +187 -0
- data/config/locales/en-YODA.yml +187 -0
- data/config/locales/en-ZA.yml +187 -0
- data/config/locales/en-ZW.yml +187 -0
- data/config/locales/en.yml +187 -0
- data/config/routes.rb +27 -3
- data/lib/generators/mat_views/install/templates/create_mat_view_definitions.rb +7 -7
- data/lib/generators/mat_views/install/templates/create_mat_view_runs.rb +5 -5
- data/lib/mat_views/admin/auth_bridge.rb +93 -0
- data/lib/mat_views/admin/default_auth.rb +61 -0
- data/lib/mat_views/configuration.rb +9 -0
- data/lib/mat_views/engine.rb +50 -2
- data/lib/mat_views/helpers/ui_test_ids.rb +43 -0
- data/lib/mat_views/services/base_service.rb +46 -38
- data/lib/mat_views/services/check_matview_exists.rb +76 -0
- data/lib/mat_views/services/concurrent_refresh.rb +9 -6
- data/lib/mat_views/services/create_view.rb +15 -15
- data/lib/mat_views/services/delete_view.rb +8 -11
- data/lib/mat_views/services/regular_refresh.rb +6 -5
- data/lib/mat_views/services/swap_refresh.rb +11 -9
- data/lib/mat_views/version.rb +1 -1
- data/lib/mat_views.rb +10 -4
- data/lib/tasks/helpers.rb +13 -13
- data/lib/tasks/mat_views_tasks.rake +15 -15
- metadata +130 -5
@@ -0,0 +1,385 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright Codevedas Inc. 2025-present
|
4
|
+
#
|
5
|
+
# This source code is licensed under the MIT license found in the
|
6
|
+
# LICENSE file in the root directory of this source tree.
|
7
|
+
|
8
|
+
module MatViews
|
9
|
+
module Admin
|
10
|
+
# MatViews::Admin::UiHelper
|
11
|
+
# -------------------------
|
12
|
+
# View helper methods for the MatViews admin UI.
|
13
|
+
#
|
14
|
+
# Responsibilities:
|
15
|
+
# - Provides consistent button, link, drawer, badge, and icon components.
|
16
|
+
# - Wraps standard Rails helpers (`link_to`, `button_to`, `button_tag`, etc.)
|
17
|
+
# with MatViews-specific styling and Stimulus integration.
|
18
|
+
# - Defines inline SVG icon snippets for use across the admin dashboard.
|
19
|
+
#
|
20
|
+
# Key Components:
|
21
|
+
# - Buttons: {#mv_button_link}, {#mv_button_to}, {#mv_drawer_link}, {#mv_drawer_action_button}
|
22
|
+
# - Links: {#mv_link_to}
|
23
|
+
# - Badges: {#mv_badge}
|
24
|
+
# - Icons: {#mv_icon} with private `svg_icon_*` methods
|
25
|
+
# - Translations: {#mv_t}
|
26
|
+
#
|
27
|
+
module UiHelper
|
28
|
+
# Builds CSS classes for a MatViews-styled button.
|
29
|
+
#
|
30
|
+
# @param variant [Symbol] one of `:primary`, `:secondary`, `:ghost`, `:negative`
|
31
|
+
# @param size [Symbol] one of `:sm`, `:md`, `:lg`
|
32
|
+
# @return [String] concatenated CSS class string
|
33
|
+
def mv_button_classes(variant, size)
|
34
|
+
[
|
35
|
+
'mv-btn',
|
36
|
+
"mv-btn--#{variant}",
|
37
|
+
"mv-btn--#{size}"
|
38
|
+
].join(' ')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Renders a styled link button.
|
42
|
+
#
|
43
|
+
# @param href [String] target URL
|
44
|
+
# @param opts [Hash] options for styling, data attributes, etc.
|
45
|
+
# @yield link body content
|
46
|
+
# @return [String] HTML-safe link tag
|
47
|
+
def mv_button_link(href, opts = {}, &)
|
48
|
+
link_to capture(&), href, **link_options(assign_test_id(opts))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Renders a button link that opens a drawer via Stimulus.
|
52
|
+
#
|
53
|
+
# @param drawer_url [String] URL to load into the drawer
|
54
|
+
# @param drawer_title [String] title for the drawer
|
55
|
+
# @param args [Hash] additional options
|
56
|
+
# @yield button body
|
57
|
+
# @return [String] HTML-safe link tag
|
58
|
+
def mv_drawer_link(drawer_url, drawer_title, args = {}, &)
|
59
|
+
data = { action: 'click->drawer#open', drawer_title: drawer_title, drawer_url: drawer_url }
|
60
|
+
args[:data] = (args[:data] || {}).merge(data)
|
61
|
+
mv_button_link '#', assign_test_id(args), &
|
62
|
+
end
|
63
|
+
|
64
|
+
# Renders a styled `button_to` element.
|
65
|
+
#
|
66
|
+
# @param href [String] target URL
|
67
|
+
# @param opts [Hash] options for styling, data attributes, etc.
|
68
|
+
# @yield button content
|
69
|
+
# @return [String] HTML-safe button tag
|
70
|
+
def mv_button_to(href, opts = {}, &)
|
71
|
+
button_to href, **link_options(assign_test_id(opts)) do
|
72
|
+
capture(&)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Renders a drawer action button with optional tooltip.
|
77
|
+
#
|
78
|
+
# @param label [String] ARIA label for accessibility
|
79
|
+
# @param action [String] Stimulus action method
|
80
|
+
# @param tooltip [String, nil] optional tooltip text
|
81
|
+
# @param tooltip_placement [String, nil] placement of tooltip (default: "top")
|
82
|
+
# @param args_orig [Hash] additional HTML options
|
83
|
+
# @yield button content
|
84
|
+
# @return [String] HTML-safe button tag
|
85
|
+
def mv_drawer_action_button(label, action, tooltip = nil, tooltip_placement = nil, args_org = {}, &)
|
86
|
+
args = assign_test_id(args_org)
|
87
|
+
data = { action: "drawer##{action}" }
|
88
|
+
data = data.merge(args[:data] || {})
|
89
|
+
if tooltip
|
90
|
+
data[:controller] = 'tooltip'
|
91
|
+
data[:'tooltip-text-value'] = tooltip
|
92
|
+
data[:'tooltip-placement'] = tooltip_placement || 'top'
|
93
|
+
end
|
94
|
+
|
95
|
+
args[:data] = data
|
96
|
+
|
97
|
+
button_tag(type: 'button', class: 'mv-drawer-action', 'aria-label': label, **args, &)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Renders a styled external or internal link, with optional tooltip.
|
101
|
+
#
|
102
|
+
# @param text [String, nil] link text (nil if using block form)
|
103
|
+
# @param url [String, nil] target URL
|
104
|
+
# @param args_orig [Hash] HTML options (supports `:tooltip`, `:is_blank`)
|
105
|
+
# @yield link body when block form is used
|
106
|
+
# @return [String] HTML-safe link tag
|
107
|
+
def mv_link_to(text = nil, url = nil, args_orig = nil, &block)
|
108
|
+
args = assign_test_id(args_orig || {})
|
109
|
+
if block_given?
|
110
|
+
args = assign_test_id(url || {})
|
111
|
+
url = text
|
112
|
+
end
|
113
|
+
|
114
|
+
tooltip = args.fetch(:tooltip, nil)
|
115
|
+
is_blank = args.fetch(:is_blank, true)
|
116
|
+
underline = args.fetch(:underline, true)
|
117
|
+
args_to_apply = args.except(:tooltip, :is_blank)
|
118
|
+
if is_blank
|
119
|
+
args_to_apply[:target] = '_blank'
|
120
|
+
args_to_apply[:rel] = 'noopener noreferrer'
|
121
|
+
end
|
122
|
+
args_to_apply[:class] = 'underline' if underline
|
123
|
+
if tooltip
|
124
|
+
args_to_apply[:'data-controller'] = 'tooltip'
|
125
|
+
args_to_apply[:'data-tooltip-text-value'] = tooltip
|
126
|
+
end
|
127
|
+
|
128
|
+
if block_given?
|
129
|
+
link_to url, args_to_apply do
|
130
|
+
capture(&block)
|
131
|
+
end
|
132
|
+
else
|
133
|
+
link_to text, url, **args_to_apply
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Renders a tab link for the tabs component.
|
138
|
+
# @param tab_name [String] unique name of the tab
|
139
|
+
# @param args_org [Hash] additional HTML options
|
140
|
+
#
|
141
|
+
# @yield tab link content
|
142
|
+
#
|
143
|
+
# @return [String] HTML-safe link tag
|
144
|
+
def mv_tab_link(tab_name, args_org = {}, &)
|
145
|
+
args = assign_test_id(args_org)
|
146
|
+
classes = ['mv-tab']
|
147
|
+
selected = args.delete(:selected)
|
148
|
+
classes << (selected ? 'mv-tab--on' : '')
|
149
|
+
args[:class] = [args[:class], classes.compact.join(' ')].compact.join(' ').strip
|
150
|
+
args[:data] = args.fetch(:data, {}).merge({ action: 'click->tabs#show', 'tabs-target': 'link', name: tab_name })
|
151
|
+
link_to '#', **args, &
|
152
|
+
end
|
153
|
+
|
154
|
+
# Shortcut for translating keys under the `mat_views` namespace.
|
155
|
+
#
|
156
|
+
# @param key [String, Symbol] translation key under `mat_views.*`
|
157
|
+
# @param args [Hash] interpolation values
|
158
|
+
# @return [String] translated string
|
159
|
+
def mv_t(key, **args)
|
160
|
+
I18n.t("mat_views.#{key}", **args)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Renders a badge element styled by status.
|
164
|
+
#
|
165
|
+
# @param status [String, Symbol] status name (`success`, `running`, `failed`, etc.)
|
166
|
+
# @param text [String] text to display inside the badge
|
167
|
+
# @return [String] HTML-safe span tag with badge classes
|
168
|
+
def mv_badge(status, text)
|
169
|
+
klass = case status.to_s.downcase
|
170
|
+
when 'success' then 'mv-badge mv-badge--success'
|
171
|
+
when 'running' then 'mv-badge mv-badge--running'
|
172
|
+
when 'failed' then 'mv-badge mv-badge--failed'
|
173
|
+
else 'mv-badge'
|
174
|
+
end
|
175
|
+
content_tag(:span, text, class: klass)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Renders an inline SVG icon.
|
179
|
+
#
|
180
|
+
# @param name [String, Symbol] icon name (method suffix after `svg_icon_`)
|
181
|
+
# @param size [Integer] icon width/height in pixels
|
182
|
+
# @param class_name [String, nil] optional extra CSS class
|
183
|
+
# @return [String] HTML-safe SVG element
|
184
|
+
def mv_icon(name, size: 16, class_name: nil)
|
185
|
+
icon_method_name = :"svg_icon_#{name}"
|
186
|
+
content_tag :svg,
|
187
|
+
class: ['mv-icon', class_name].compact.join(' '),
|
188
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
189
|
+
width: size, height: size, viewBox: '0 0 24 24',
|
190
|
+
fill: 'none', stroke: 'currentColor',
|
191
|
+
'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
192
|
+
'aria-hidden': 'true' do
|
193
|
+
respond_to?(icon_method_name, true) ? raw(send(icon_method_name)) : ''.html_safe
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Renders a styled submit button.
|
198
|
+
#
|
199
|
+
# @param opts [Hash] options for styling, data attributes, etc.
|
200
|
+
#
|
201
|
+
# @yield button content
|
202
|
+
#
|
203
|
+
# @return [String] HTML-safe button tag
|
204
|
+
def mv_submit_button(opts = {}, &)
|
205
|
+
opts = link_options(assign_test_id(opts))
|
206
|
+
opts[:type] = 'submit'
|
207
|
+
opts.delete(:method)
|
208
|
+
button_tag(capture(&), **opts)
|
209
|
+
end
|
210
|
+
|
211
|
+
# Renders a styled cancel button.
|
212
|
+
# @param opts [Hash] options for styling, data attributes, etc.
|
213
|
+
#
|
214
|
+
# @yield button content
|
215
|
+
#
|
216
|
+
# @return [String] HTML-safe button tag
|
217
|
+
def mv_cancel_button(opts = {}, &)
|
218
|
+
opts = link_options(assign_test_id(opts))
|
219
|
+
opts[:type] = 'button'
|
220
|
+
opts.delete(:method)
|
221
|
+
button_tag(capture(&), **opts)
|
222
|
+
end
|
223
|
+
|
224
|
+
private
|
225
|
+
|
226
|
+
# Builds standardized options hash for button/link helpers.
|
227
|
+
#
|
228
|
+
# @param opts [Hash] options including :variant, :size, :method, :tooltip, etc.
|
229
|
+
# @return [Hash] merged HTML attributes (class, method, data)
|
230
|
+
def link_options(opts)
|
231
|
+
variant = opts.fetch(:variant, :primary)
|
232
|
+
size = opts.fetch(:size, :md)
|
233
|
+
method = opts.fetch(:method, :get)
|
234
|
+
underline = opts[:underline] ? ' underline' : ''
|
235
|
+
|
236
|
+
tip = opts[:tooltip]
|
237
|
+
tooltip = if tip
|
238
|
+
{
|
239
|
+
controller: 'tooltip',
|
240
|
+
'tooltip-text-value': tip,
|
241
|
+
'tooltip-placement': opts.fetch(:tooltip_placement, 'top')
|
242
|
+
}
|
243
|
+
else
|
244
|
+
{}
|
245
|
+
end
|
246
|
+
|
247
|
+
html_data = (opts[:data] || {}).dup.merge(tooltip)
|
248
|
+
html_data[:'turbo-confirm'] = opts.fetch(:confirm, nil)
|
249
|
+
{ class: "#{mv_button_classes(variant, size)}#{underline}", method: method, data: html_data }
|
250
|
+
end
|
251
|
+
|
252
|
+
# @return [String] SVG markup for a right-pointing arrow icon
|
253
|
+
def svg_icon_arrow_right
|
254
|
+
<<~SVG.strip.freeze
|
255
|
+
<polyline points="9 18 15 12 9 6"/>
|
256
|
+
SVG
|
257
|
+
end
|
258
|
+
|
259
|
+
# @return [String] SVG markup for a refresh/reload icon
|
260
|
+
def svg_icon_refresh
|
261
|
+
<<~SVG.strip.freeze
|
262
|
+
<path d="M21 12a9 9 0 1 1-2.64-6.36"/>
|
263
|
+
<polyline points="23 4 23 10 17 10"/>
|
264
|
+
SVG
|
265
|
+
end
|
266
|
+
|
267
|
+
# @return [String] SVG markup for a trash/delete (bin) icon
|
268
|
+
def svg_icon_trash
|
269
|
+
<<~SVG.strip.freeze
|
270
|
+
<polyline points="3 6 5 6 21 6"/>
|
271
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
272
|
+
<path d="M10 11v6"/>
|
273
|
+
<path d="M14 11v6"/>
|
274
|
+
<path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/>
|
275
|
+
SVG
|
276
|
+
end
|
277
|
+
|
278
|
+
# @return [String] SVG markup for a hammer/tool icon
|
279
|
+
def svg_icon_hammer
|
280
|
+
<<~SVG.strip.freeze
|
281
|
+
<path d="M14 4l7 7"/>
|
282
|
+
<path d="M5 15l7-7 3 3-7 7H5v-3z"/>
|
283
|
+
SVG
|
284
|
+
end
|
285
|
+
|
286
|
+
# @return [String] SVG markup for an “X in a circle” (close/error) icon
|
287
|
+
def svg_icon_x_circle
|
288
|
+
<<~SVG.strip.freeze
|
289
|
+
<circle cx="12" cy="12" r="10"/>
|
290
|
+
<path d="M15 9l-6 6"/>
|
291
|
+
<path d="M9 9l6 6"/>
|
292
|
+
SVG
|
293
|
+
end
|
294
|
+
|
295
|
+
# @return [String] SVG markup for a “plus in a circle” (add) icon
|
296
|
+
def svg_icon_plus_circle
|
297
|
+
<<~SVG.strip.freeze
|
298
|
+
<circle cx="12" cy="12" r="10"/>
|
299
|
+
<line x1="12" y1="8" x2="12" y2="16"/>
|
300
|
+
<line x1="8" y1="12" x2="16" y2="12"/>
|
301
|
+
SVG
|
302
|
+
end
|
303
|
+
|
304
|
+
# @return [String] SVG markup for a checkmark-in-circle (success) icon
|
305
|
+
def svg_icon_check_circle
|
306
|
+
<<~SVG.strip.freeze
|
307
|
+
<circle cx="12" cy="12" r="10"/>
|
308
|
+
<polyline points="9 12 12 15 16 9"/>
|
309
|
+
SVG
|
310
|
+
end
|
311
|
+
|
312
|
+
# @return [String] SVG markup for an alert/warning triangle icon
|
313
|
+
def svg_icon_alert
|
314
|
+
<<~SVG.strip.freeze
|
315
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
316
|
+
<line x1="12" y1="9" x2="12" y2="13"/>
|
317
|
+
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
318
|
+
SVG
|
319
|
+
end
|
320
|
+
|
321
|
+
# @return [String] SVG markup for a history/clock icon
|
322
|
+
def svg_icon_history
|
323
|
+
<<~SVG.strip.freeze
|
324
|
+
<path d="M3 3v5h5"/>
|
325
|
+
<path d="M3.05 13a9 9 0 1 0 .5-5.5"/>
|
326
|
+
<path d="M12 7v5l3 3"/>
|
327
|
+
SVG
|
328
|
+
end
|
329
|
+
|
330
|
+
# @return [String] SVG markup for an edit/pencil icon
|
331
|
+
def svg_icon_edit
|
332
|
+
<<~SVG.strip.freeze
|
333
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
334
|
+
<path d="M18.5 2.5a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
335
|
+
SVG
|
336
|
+
end
|
337
|
+
|
338
|
+
# @return [String] SVG markup for a stacked-layers icon
|
339
|
+
def svg_icon_layers
|
340
|
+
<<~SVG.strip.freeze
|
341
|
+
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
342
|
+
<polyline points="2 17 12 22 22 17"/>
|
343
|
+
<polyline points="2 12 12 17 22 12"/>
|
344
|
+
SVG
|
345
|
+
end
|
346
|
+
|
347
|
+
# @return [String] SVG markup for a database/cylinder icon
|
348
|
+
def svg_icon_database
|
349
|
+
<<~SVG.strip.freeze
|
350
|
+
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
351
|
+
<path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/>
|
352
|
+
<path d="M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6"/>
|
353
|
+
SVG
|
354
|
+
end
|
355
|
+
|
356
|
+
# @return [String] SVG markup for a gear/settings icon
|
357
|
+
def svg_icon_gear
|
358
|
+
<<~SVG.strip.freeze
|
359
|
+
<circle cx="12" cy="12" r="3"></circle>
|
360
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
361
|
+
SVG
|
362
|
+
end
|
363
|
+
|
364
|
+
# Maps a symbolic test ID to its actual string value for data attributes.
|
365
|
+
# if `:testid` key is not present, returns original args unchanged.
|
366
|
+
#
|
367
|
+
# @param args [Hash] original options hash
|
368
|
+
# @return [Hash] modified options hash with `data-testid` set
|
369
|
+
#
|
370
|
+
# @example
|
371
|
+
# assign_test_id(class: 'btn', testid: :HEADER_LINK)
|
372
|
+
# # => { class: 'btn', data: { testid: 'header_link' } }
|
373
|
+
#
|
374
|
+
def assign_test_id(args = {})
|
375
|
+
return args unless args[:testid].present?
|
376
|
+
|
377
|
+
testid_constant = args.delete(:testid)
|
378
|
+
testid_identifier = args.delete(:testid_identifier) || ''
|
379
|
+
args_data = args[:data] || {}
|
380
|
+
args_data[:testid] = "#{MatViews::Helpers::UiTestIds.const_get(testid_constant)}-#{testid_identifier}".chomp('-')
|
381
|
+
args.merge data: args_data
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Application } from "@hotwired/stimulus";
|
8
|
+
const application = Application.start();
|
9
|
+
window.Stimulus = application;
|
10
|
+
export { application };
|
@@ -0,0 +1,122 @@
|
|
1
|
+
/**
|
2
|
+
* Copyright Codevedas Inc. 2025-present
|
3
|
+
*
|
4
|
+
* This source code is licensed under the MIT license found in the
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
6
|
+
*/
|
7
|
+
import { Controller } from "@hotwired/stimulus";
|
8
|
+
|
9
|
+
export default class extends Controller {
|
10
|
+
static targets = ["content"];
|
11
|
+
static values = { duration: Number };
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
this.animating = false;
|
15
|
+
// Normalize initial state (support SSR or toggled-by-server)
|
16
|
+
if (this.element.open) {
|
17
|
+
this._setHeightAuto();
|
18
|
+
} else {
|
19
|
+
this._setHeight(0);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
toggle(event) {
|
24
|
+
event.preventDefault();
|
25
|
+
if (this.animating) this._cancelAnimation();
|
26
|
+
|
27
|
+
if (this.element.open) {
|
28
|
+
this._collapse();
|
29
|
+
} else {
|
30
|
+
this._expand();
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
// ────────────────────────────────────────────────────────────────
|
35
|
+
// internal
|
36
|
+
// ────────────────────────────────────────────────────────────────
|
37
|
+
|
38
|
+
_expand() {
|
39
|
+
const el = this.contentTarget;
|
40
|
+
this.animating = true;
|
41
|
+
|
42
|
+
// Set starting point
|
43
|
+
this._setTransitionNone();
|
44
|
+
this._setHeight(0);
|
45
|
+
// Make <details> open *before* measuring end height, so children are laid out.
|
46
|
+
this.element.open = true;
|
47
|
+
|
48
|
+
// Next frame: measure, then animate to end height
|
49
|
+
requestAnimationFrame(() => {
|
50
|
+
const end = el.scrollHeight;
|
51
|
+
this._setTransition();
|
52
|
+
this._setHeight(end);
|
53
|
+
|
54
|
+
this._onTransitionEnd(() => {
|
55
|
+
this._setHeightAuto(); // allow content to grow/shrink after expand
|
56
|
+
this.animating = false;
|
57
|
+
});
|
58
|
+
});
|
59
|
+
}
|
60
|
+
|
61
|
+
_collapse() {
|
62
|
+
const el = this.contentTarget;
|
63
|
+
this.animating = true;
|
64
|
+
|
65
|
+
// Freeze current height (auto → px) to start a smooth collapse
|
66
|
+
this._setTransitionNone();
|
67
|
+
const start = el.scrollHeight;
|
68
|
+
this._setHeight(start);
|
69
|
+
|
70
|
+
// Next frame: animate to 0, then close details
|
71
|
+
requestAnimationFrame(() => {
|
72
|
+
this._setTransition();
|
73
|
+
this._setHeight(0);
|
74
|
+
|
75
|
+
this._onTransitionEnd(() => {
|
76
|
+
this.element.open = false;
|
77
|
+
this._setTransitionNone();
|
78
|
+
this._setHeight(0); // keep at 0 when closed
|
79
|
+
this.animating = false;
|
80
|
+
});
|
81
|
+
});
|
82
|
+
}
|
83
|
+
|
84
|
+
_cancelAnimation() {
|
85
|
+
// Interrupt ongoing animation cleanly
|
86
|
+
const el = this.contentTarget;
|
87
|
+
const computed = parseFloat(getComputedStyle(el).height);
|
88
|
+
this._setTransitionNone();
|
89
|
+
this._setHeight(computed); // lock current visual height
|
90
|
+
this.animating = false;
|
91
|
+
}
|
92
|
+
|
93
|
+
_onTransitionEnd(cb) {
|
94
|
+
const el = this.contentTarget;
|
95
|
+
const handler = (e) => {
|
96
|
+
if (e.target !== el || e.propertyName !== "height") return;
|
97
|
+
el.removeEventListener("transitionend", handler);
|
98
|
+
el.removeEventListener("transitioncancel", handler);
|
99
|
+
cb();
|
100
|
+
};
|
101
|
+
el.addEventListener("transitionend", handler, { once: false });
|
102
|
+
el.addEventListener("transitioncancel", handler, { once: false });
|
103
|
+
}
|
104
|
+
|
105
|
+
_setTransition() {
|
106
|
+
const ms = this.hasDurationValue ? this.durationValue : 200;
|
107
|
+
this.contentTarget.style.transition = `height ${ms}ms ease`;
|
108
|
+
}
|
109
|
+
|
110
|
+
_setTransitionNone() {
|
111
|
+
this.contentTarget.style.transition = "none";
|
112
|
+
}
|
113
|
+
|
114
|
+
_setHeight(px) {
|
115
|
+
this.contentTarget.style.height = `${px}px`;
|
116
|
+
}
|
117
|
+
|
118
|
+
_setHeightAuto() {
|
119
|
+
this._setTransitionNone();
|
120
|
+
this.contentTarget.style.height = "auto";
|
121
|
+
}
|
122
|
+
}
|