smriti 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +168 -0
  4. data/Rakefile +15 -0
  5. data/app/assets/images/smriti/android-chrome-192x192.png +0 -0
  6. data/app/assets/images/smriti/android-chrome-512x512.png +0 -0
  7. data/app/assets/images/smriti/apple-touch-icon.png +0 -0
  8. data/app/assets/images/smriti/favicon-16x16.png +0 -0
  9. data/app/assets/images/smriti/favicon-32x32.png +0 -0
  10. data/app/assets/images/smriti/favicon-48x48.png +0 -0
  11. data/app/assets/images/smriti/favicon.ico +0 -0
  12. data/app/assets/images/smriti/favicon.svg +18 -0
  13. data/app/assets/images/smriti/logo.svg +18 -0
  14. data/app/assets/images/smriti/mask-icon.svg +5 -0
  15. data/app/assets/stylesheets/smriti/application.css +1040 -0
  16. data/app/controllers/smriti/admin/application_controller.rb +135 -0
  17. data/app/controllers/smriti/admin/dashboard_controller.rb +32 -0
  18. data/app/controllers/smriti/admin/mat_view_definitions_controller.rb +372 -0
  19. data/app/controllers/smriti/admin/mat_view_runs_controller.rb +185 -0
  20. data/app/controllers/smriti/admin/preferences_controller.rb +91 -0
  21. data/app/helpers/smriti/admin/datatable_helper.rb +249 -0
  22. data/app/helpers/smriti/admin/localized_digit_helper.rb +70 -0
  23. data/app/helpers/smriti/admin/ui_helper.rb +539 -0
  24. data/app/javascript/smriti/application.js +8 -0
  25. data/app/javascript/smriti/controllers/application.js +10 -0
  26. data/app/javascript/smriti/controllers/body_setup_controller.js +120 -0
  27. data/app/javascript/smriti/controllers/datatable_controller.js +351 -0
  28. data/app/javascript/smriti/controllers/details_controller.js +200 -0
  29. data/app/javascript/smriti/controllers/drawer_controller.js +470 -0
  30. data/app/javascript/smriti/controllers/flash_controller.js +112 -0
  31. data/app/javascript/smriti/controllers/index.js +10 -0
  32. data/app/javascript/smriti/controllers/mv_confirm_controller.js +435 -0
  33. data/app/javascript/smriti/controllers/tabs_controller.js +184 -0
  34. data/app/javascript/smriti/controllers/tooltip_controller.js +525 -0
  35. data/app/javascript/smriti/controllers/turbo_frame_lifecycle_controller.js +342 -0
  36. data/app/jobs/smriti/application_job.rb +144 -0
  37. data/app/jobs/smriti/create_view_job.rb +87 -0
  38. data/app/jobs/smriti/delete_view_job.rb +89 -0
  39. data/app/jobs/smriti/refresh_view_job.rb +94 -0
  40. data/app/models/concerns/smriti_i18n.rb +139 -0
  41. data/app/models/concerns/smriti_paginate.rb +70 -0
  42. data/app/models/concerns/smriti_query_helper.rb +36 -0
  43. data/app/models/smriti/application_record.rb +39 -0
  44. data/app/models/smriti/mat_view_definition.rb +254 -0
  45. data/app/models/smriti/mat_view_run.rb +275 -0
  46. data/app/views/layouts/smriti/_footer.html.erb +47 -0
  47. data/app/views/layouts/smriti/_header.html.erb +25 -0
  48. data/app/views/layouts/smriti/admin.html.erb +47 -0
  49. data/app/views/layouts/smriti/turbo_frame.html.erb +3 -0
  50. data/app/views/smriti/admin/dashboard/index.html.erb +38 -0
  51. data/app/views/smriti/admin/mat_view_definitions/_definition_actions.html.erb +94 -0
  52. data/app/views/smriti/admin/mat_view_definitions/_dt-index-empty-row.html.erb +11 -0
  53. data/app/views/smriti/admin/mat_view_definitions/_dt-index-row.html.erb +27 -0
  54. data/app/views/smriti/admin/mat_view_definitions/empty.html.erb +1 -0
  55. data/app/views/smriti/admin/mat_view_definitions/form.html.erb +79 -0
  56. data/app/views/smriti/admin/mat_view_definitions/index.html.erb +10 -0
  57. data/app/views/smriti/admin/mat_view_definitions/show.html.erb +40 -0
  58. data/app/views/smriti/admin/mat_view_runs/_dt-index-empty-row.html.erb +11 -0
  59. data/app/views/smriti/admin/mat_view_runs/_dt-index-row.html.erb +41 -0
  60. data/app/views/smriti/admin/mat_view_runs/index.html.erb +1 -0
  61. data/app/views/smriti/admin/mat_view_runs/show.html.erb +64 -0
  62. data/app/views/smriti/admin/preferences/show.html.erb +49 -0
  63. data/app/views/smriti/admin/ui/_card.html.erb +15 -0
  64. data/app/views/smriti/admin/ui/_datatable.html.erb +34 -0
  65. data/app/views/smriti/admin/ui/_datatable_filters.html.erb +45 -0
  66. data/app/views/smriti/admin/ui/_datatable_tbody.html.erb +11 -0
  67. data/app/views/smriti/admin/ui/_datatable_tfoot.html.erb +70 -0
  68. data/app/views/smriti/admin/ui/_datatable_thead.html.erb +105 -0
  69. data/app/views/smriti/admin/ui/_details.html.erb +10 -0
  70. data/app/views/smriti/admin/ui/_flash.html.erb +6 -0
  71. data/app/views/smriti/admin/ui/_table.html.erb +8 -0
  72. data/config/importmap.rb +9 -0
  73. data/config/locales/ar.yml +223 -0
  74. data/config/locales/de.yml +230 -0
  75. data/config/locales/en-AU-ocker.yml +223 -0
  76. data/config/locales/en-AU.yml +202 -0
  77. data/config/locales/en-BORK.yml +225 -0
  78. data/config/locales/en-CA.yml +223 -0
  79. data/config/locales/en-GB.yml +223 -0
  80. data/config/locales/en-LOL.yml +219 -0
  81. data/config/locales/en-SCOT.yml +223 -0
  82. data/config/locales/en-SHAKESPEARE.yml +225 -0
  83. data/config/locales/en-US-pirate.yml +222 -0
  84. data/config/locales/en-US.yml +225 -0
  85. data/config/locales/en-YODA.yml +221 -0
  86. data/config/locales/en.yml +223 -0
  87. data/config/locales/es.yml +226 -0
  88. data/config/locales/fa.yml +223 -0
  89. data/config/locales/fr-CA.yml +227 -0
  90. data/config/locales/fr.yml +227 -0
  91. data/config/locales/he.yml +218 -0
  92. data/config/locales/hi.yml +223 -0
  93. data/config/locales/it.yml +225 -0
  94. data/config/locales/ja-JP.yml +215 -0
  95. data/config/locales/pt.yml +225 -0
  96. data/config/locales/ru.yml +228 -0
  97. data/config/locales/ur.yml +225 -0
  98. data/config/locales/zh-CN.yml +214 -0
  99. data/config/locales/zh-TW.yml +214 -0
  100. data/config/routes.rb +36 -0
  101. data/lib/ext/exception.rb +20 -0
  102. data/lib/generators/smriti/install/install_generator.rb +86 -0
  103. data/lib/generators/smriti/install/templates/create_mat_view_definitions.rb +29 -0
  104. data/lib/generators/smriti/install/templates/create_mat_view_runs.rb +32 -0
  105. data/lib/generators/smriti/install/templates/smriti_initializer.rb +23 -0
  106. data/lib/smriti/admin/auth_bridge.rb +93 -0
  107. data/lib/smriti/admin/default_auth.rb +62 -0
  108. data/lib/smriti/configuration.rb +58 -0
  109. data/lib/smriti/engine.rb +82 -0
  110. data/lib/smriti/helpers/ui_test_ids.rb +49 -0
  111. data/lib/smriti/jobs/adapter.rb +81 -0
  112. data/lib/smriti/service_response.rb +75 -0
  113. data/lib/smriti/services/base_service.rb +471 -0
  114. data/lib/smriti/services/check_matview_exists.rb +76 -0
  115. data/lib/smriti/services/concurrent_refresh.rb +94 -0
  116. data/lib/smriti/services/create_view.rb +173 -0
  117. data/lib/smriti/services/delete_view.rb +111 -0
  118. data/lib/smriti/services/regular_refresh.rb +90 -0
  119. data/lib/smriti/services/swap_refresh.rb +181 -0
  120. data/lib/smriti/version.rb +21 -0
  121. data/lib/smriti.rb +64 -0
  122. data/lib/tasks/helpers.rb +185 -0
  123. data/lib/tasks/smriti_tasks.rake +151 -0
  124. metadata +206 -0
@@ -0,0 +1,185 @@
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 Smriti
9
+ module Admin
10
+ # Smriti::Admin::MatViewRunsController
11
+ # -------------------------------
12
+ # Controller for viewing materialised view run history in the admin UI.
13
+ #
14
+ # Responsibilities:
15
+ # - Provides a list of recent runs (create/refresh/delete) for all definitions.
16
+ # - Shows details for an individual run in a Turbo frame/drawer context.
17
+ #
18
+ # Filters:
19
+ # - `before_action :ensure_frame` → enforces frame-only access.
20
+ # - `before_action :set_mat_view_run` → loads and authorizes a single run.
21
+ #
22
+ # Views:
23
+ # - Index renders `smriti/admin/runs/embed_frame` partial.
24
+ # - Show renders `smriti/admin/runs/embed_show_drawer` partial.
25
+ #
26
+ class MatViewRunsController < ApplicationController
27
+ include Smriti::Admin::DatatableHelper
28
+
29
+ before_action :ensure_frame, :parse_headers_to_params
30
+ helper_method :definition
31
+ before_action :ensure_frame, only: %i[index show]
32
+ before_action :set_mat_view_run, only: %i[show]
33
+
34
+ # GET /:lang/admin/runs
35
+ #
36
+ # Two part rendering:
37
+ # - Full page load when no `stream` param: renders index with datatable frame. This is
38
+ # essentially shell of the datatable for initial load.
39
+ # - When shell is loaded, it requests the `stream` version which renders just the datatable rows
40
+ # and pagination controls. This allows for dynamic updates via Turbo Streams.
41
+ #
42
+ # @return [void]
43
+ def index
44
+ authorize_smriti!(:read, :smriti_runs)
45
+
46
+ assign_index_state
47
+
48
+ if params[:stream].present?
49
+ render_dt_turbo_streams
50
+ else
51
+ render 'index', formats: :html, layout: 'smriti/turbo_frame', locals: { row_meta: @row_meta }
52
+ end
53
+ end
54
+
55
+ # GET /:lang/admin/runs/:id
56
+ #
57
+ # Displays details for a single run.
58
+ #
59
+ # @return [void]
60
+ def show
61
+ authorize_smriti!(:read, :smriti_run, @run)
62
+
63
+ render 'show', formats: :html, layout: 'smriti/turbo_frame'
64
+ end
65
+
66
+ private
67
+
68
+ # Loads the requested run and checks authorization.
69
+ #
70
+ # @api private
71
+ #
72
+ # @return [void]
73
+ def set_mat_view_run
74
+ @run = Smriti::MatViewRun.find(params[:id])
75
+ end
76
+
77
+ # Loads data for the index datatable with filtering, searching, sorting, and pagination.
78
+ # sets @data.
79
+ #
80
+ # @api private
81
+ #
82
+ # @return [void]
83
+ def index_dt_load_data
84
+ rel = Smriti::MatViewRun
85
+ rel = dt_apply_filter(rel, index_dt_columns)
86
+ rel = dt_apply_search(rel, index_dt_columns)
87
+ rel = dt_apply_sort(rel, index_dt_columns)
88
+ @data = dt_apply_pagination(rel, @dt_config[:pagination][:per_page_default])
89
+ end
90
+
91
+ # Configuration for the index datatable.
92
+ #
93
+ # @api private
94
+ #
95
+ # @return [Hash] datatable configuration
96
+ def index_dt_config
97
+ columns = index_dt_columns
98
+ {
99
+ id: 'mv-runs-table',
100
+ index_url: admin_mat_view_runs_path(frame_id: @frame_id),
101
+ frame_id: 'mv-runs-datatable',
102
+ columns: columns,
103
+ dt_humanize_ref: 'Smriti::MatViewRun',
104
+ empty_row_partial_name: 'dt-index-empty-row',
105
+ row_partial_name: 'dt-index-row',
106
+ search_enabled: columns.any? { |_, col| col[:search].present? },
107
+ filter_enabled: columns.any? { |_, col| col[:filter].present? },
108
+ pagination: { per_page_default: 10, per_page_options: [10, 25, 50, 100] }
109
+ }
110
+ end
111
+
112
+ # Column definitions for the index datatable.
113
+ #
114
+ # @api private
115
+ #
116
+ # @return [Hash] column definitions
117
+ def index_dt_columns
118
+ {
119
+ operation: {
120
+ label_ref: 'operation',
121
+ label_type: 'humanize_attr',
122
+ sort: 'operation',
123
+ filter: 'operation',
124
+ search: 'operation'
125
+ },
126
+ definition: {
127
+ label_ref: 'definition',
128
+ label_type: 'i18n',
129
+ sort: 'definition',
130
+ filter: 'definition',
131
+ search: 'definition'
132
+ },
133
+ started_at: {
134
+ label_ref: 'started_at',
135
+ label_type: 'humanize_attr',
136
+ sort: 'started_at',
137
+ filter: nil,
138
+ search: nil
139
+ },
140
+ status: {
141
+ label_ref: 'status',
142
+ label_type: 'humanize_attr',
143
+ sort: 'status',
144
+ filter: 'status',
145
+ search: 'status'
146
+ },
147
+ duration: {
148
+ label_ref: 'duration_ms',
149
+ label_type: 'humanize_attr',
150
+ filter: nil,
151
+ sort: 'duration_ms',
152
+ search: 'duration_ms'
153
+ },
154
+ rows_before_after: {
155
+ label_ref: 'rows_before_after',
156
+ label_type: 'humanize_attr',
157
+ filter: nil,
158
+ sort: nil,
159
+ search: nil
160
+ },
161
+ details: {
162
+ label_ref: 'details',
163
+ label_type: 'humanize_attr',
164
+ filter: nil,
165
+ sort: nil,
166
+ search: nil
167
+ }
168
+ }
169
+ end
170
+
171
+ # Assigns instance variables for the index action.
172
+ #
173
+ # @api private
174
+ #
175
+ # @return [void]
176
+ def assign_index_state
177
+ @dt_config = index_dt_config
178
+ @data = []
179
+
180
+ index_dt_load_data
181
+ @row_meta = {}
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,91 @@
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 Smriti
9
+ module Admin
10
+ # Smriti::Admin::PreferencesController
11
+ # --------------------------------------
12
+ # Controller for managing user preferences in the Smriti admin UI.
13
+ #
14
+ # Responsibilities:
15
+ # - Allows users to view and update UI preferences such as theme and locale.
16
+ # - Stores theme in cookies and locale in session.
17
+ # - Provides a force-reload response option to update Turbo frames dynamically.
18
+ #
19
+ # Filters:
20
+ # - `before_action :authorize!` → ensures user can access preferences.
21
+ # - `before_action :ensure_frame` → requires Turbo frame context for `show`.
22
+ #
23
+ class PreferencesController < ApplicationController
24
+ before_action :authorize!
25
+ before_action :ensure_frame
26
+
27
+ # GET /:lang/admin/preferences
28
+ #
29
+ # Displays the current preferences (theme + locale) and available locales.
30
+ # If `force_reload=1` is passed, sets a non-standard status code (299) and
31
+ # a custom header to signal the client to reload.
32
+ #
33
+ # @return [void]
34
+ def show
35
+ @theme = read_theme
36
+ @locale = I18n.locale.to_s
37
+ @locales = Smriti::Engine.locale_code_mapping.sort_by { |_key, name| name }.map { |code, _name| code.to_s }.uniq
38
+
39
+ # force reload frame if requested
40
+ if params[:force_reload].to_s == '1'
41
+ response.status = 299
42
+ response.set_header('X-Status-Name', 'Success force reload')
43
+ end
44
+
45
+ render 'show', formats: :html, layout: 'smriti/turbo_frame'
46
+ end
47
+
48
+ # PATCH/PUT /:lang/admin/preferences
49
+ #
50
+ # Updates preferences:
51
+ # - Theme (`light`, `dark`, or deleted if invalid) stored in cookies.
52
+ # - Locale stored in session if valid.
53
+ # Redirects back to preferences with `force_reload=1` to trigger a refresh.
54
+ #
55
+ # @return [void]
56
+ def update
57
+ theme_param = params[:theme].to_s
58
+ case theme_param
59
+ when 'light', 'dark' then cookies[:theme] = { value: theme_param, expires: 1.year.from_now, httponly: false }
60
+ else cookies.delete(:theme)
61
+ end
62
+
63
+ locale = params[:locale].to_s.presence || Smriti::Engine.default_locale.to_s
64
+ session[:smriti_locale] = locale if Smriti::Engine.available_locales.map(&:to_s).include?(locale)
65
+
66
+ redirect_to "#{admin_preferences_path}?force_reload=1&frame_id=#{@frame_id}", status: :see_other
67
+ end
68
+
69
+ private
70
+
71
+ # Authorizes access to preferences.
72
+ #
73
+ # @api private
74
+ #
75
+ # @return [void]
76
+ def authorize!
77
+ authorize_smriti!(:view, :smriti_dashboard)
78
+ end
79
+
80
+ # Reads the theme from cookies.
81
+ #
82
+ # @api private
83
+ #
84
+ # @return ["light", "dark", "auto"] theme preference or "auto" if unset/invalid
85
+ def read_theme
86
+ t = cookies[:theme].to_s
87
+ %w[light dark].include?(t) ? t : 'auto'
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,249 @@
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 Smriti
9
+ module Admin
10
+ # Smriti::Admin::DatatableHelper
11
+ # ---------------------------
12
+ # Helper module providing methods to manage datatable functionalities
13
+ # such as sorting, searching, filtering, and pagination.
14
+ #
15
+ # Responsibilities:
16
+ # - Apply sorting based on request parameters.
17
+ # - Apply searching across multiple columns.
18
+ # - Apply filtering based on specified criteria.
19
+ # - Handle pagination with customizable page size.
20
+ # - Generate pagination window for UI display.
21
+ # - Parse custom headers to parameters for datatable requests.
22
+ # - Render Turbo Stream responses for dynamic datatable updates.
23
+ #
24
+ # Methods:
25
+ # - dt_apply_sort: applies sorting to a relation based on parameters.
26
+ # - dt_apply_search: applies search filtering to a relation.
27
+ # - dt_apply_filter: applies column-based filtering to a relation.
28
+ # - dt_apply_pagination: paginates a relation.
29
+ # - pagination_window: generates page numbers for pagination UI.
30
+ # - parse_headers_to_params: parses custom headers into params.
31
+ # - render_dt_turbo_streams: renders Turbo Stream responses for datatables.
32
+ # - param_dtfilter: accessor for dtfilter param.
33
+ # - param_dtsearch: accessor for dtsearch param.
34
+ # - param_dtsort: accessor for dtsort param.
35
+ module DatatableHelper
36
+ private
37
+
38
+ # Applies sorting to the given ActiveRecord relation based on the provided columns and request parameters.
39
+ #
40
+ # @api private
41
+ #
42
+ # @param rel [ActiveRecord::Relation] the relation to sort
43
+ # @param columns [Hash] a hash defining the columns and their sort attributes
44
+ #
45
+ # @return [ActiveRecord::Relation] the sorted relation
46
+ def dt_apply_sort(rel, columns)
47
+ return rel unless param_dtsort.present?
48
+
49
+ param_dtsort.split(',').each do |clause|
50
+ col, dir = clause.split(':')
51
+ dir = dir&.downcase == 'desc' ? :desc : :asc
52
+ col_def = columns[col.to_sym]
53
+ col_def_sort = col_def[:sort] if col_def
54
+ rel = rel.send("ordered_by_#{col_def_sort}", dir) if col_def_sort
55
+ end
56
+ rel
57
+ end
58
+
59
+ # Applies search filtering to the given ActiveRecord relation based on the provided columns and request parameters.
60
+ #
61
+ # @api private
62
+ #
63
+ # @param rel [ActiveRecord::Relation] the relation to search
64
+ # @param columns [Hash] a hash defining the columns and their search attributes
65
+ #
66
+ # @return [ActiveRecord::Relation] the filtered relation
67
+ def dt_apply_search(rel, columns)
68
+ return rel unless param_dtsearch.present?
69
+
70
+ scopes = columns.values.filter_map do |col_def|
71
+ col_def_search = col_def[:search]
72
+ rel.send("search_by_#{col_def_search}", param_dtsearch) if col_def_search
73
+ end
74
+
75
+ rel = scopes.reduce { |acc, scope| acc.or(scope) } if scopes.any?
76
+
77
+ rel
78
+ end
79
+
80
+ # Applies column-based filtering to the given ActiveRecord relation based on the provided columns and request parameters.
81
+ #
82
+ # @api private
83
+ #
84
+ # @param rel [ActiveRecord::Relation] the relation to filter
85
+ # @param columns [Hash] a hash defining the columns and their filter attributes
86
+ #
87
+ # @return [ActiveRecord::Relation] the filtered relation
88
+ def dt_apply_filter(rel, columns)
89
+ return rel unless param_dtfilter.present?
90
+
91
+ param_dtfilter.split(',').each do |clause|
92
+ col, val = clause.split(':')
93
+ col_def = columns[col.to_sym]
94
+ col_def_f = col_def[:filter] if col_def
95
+ rel = rel.send("filtered_by_#{col_def_f}", val) if col_def_f && val.present? && rel.respond_to?("filtered_by_#{col_def_f}")
96
+ end
97
+ rel
98
+ end
99
+
100
+ # Applies pagination to the given ActiveRecord relation based on request parameters.
101
+ #
102
+ # @api private
103
+ #
104
+ # @param rel [ActiveRecord::Relation] the relation to paginate
105
+ # @param default_per_page [Integer] the default number of items per page
106
+ #
107
+ # @return [ActiveRecord::Relation] the paginated relation
108
+ def dt_apply_pagination(rel, default_per_page)
109
+ @dt_page = (params[:dtpage] || 1).to_i
110
+ @dt_per_page = (params[:dtperpage] || default_per_page).to_i
111
+ total = rel.count
112
+ @dt_total_pages = rel.total_pages(total: total, per_page: @dt_per_page)
113
+ rel.paginate(total: total, page: @dt_page, per_page: @dt_per_page)
114
+ end
115
+
116
+ # Returns an array of page numbers and :gap symbols for pagination display
117
+ #
118
+ # @api private
119
+ #
120
+ # Example:
121
+ # pagination_window(current_page: 6, total_pages: 20)
122
+ # => [1, :gap, 4, 5, 6, 7, 8, :gap, 20]
123
+ #
124
+ # Use :gap to render "..." in your view.
125
+ #
126
+ # @param current_page [Integer] the current page number
127
+ # @param total_pages [Integer] the total number of pages
128
+ # @param window [Integer] the number of pages to show on each side of the current page
129
+ #
130
+ # @return [Array<Integer, Symbol>] array of page numbers and :gap symbols
131
+ def pagination_window(current_page:, total_pages:, window: 2)
132
+ return [] if total_pages < 1
133
+
134
+ pages = []
135
+ left = [1, current_page - window].max
136
+ right = [total_pages, current_page + window].min
137
+
138
+ pages << 1 unless left == 1
139
+ pages << :gap if left > 2
140
+
141
+ (left..right).each { |page| pages << page }
142
+
143
+ pages << :gap if right < total_pages - 1
144
+ pages << total_pages unless right == total_pages
145
+
146
+ pages
147
+ end
148
+
149
+ # Parses custom headers into params for datatable requests.
150
+ #
151
+ # @api private
152
+ #
153
+ # This allows clients to send datatable parameters via headers instead of query parameters.
154
+ # In this application, this is used to append headers when making Turbo Frame requests.
155
+ #
156
+ # Headers parsed:
157
+ # - X-Dtsearch -> params[:dtsearch]
158
+ # - X-Dtsort -> params[:dtsort]
159
+ # - X-Dtfilter -> params[:dtfilter]
160
+ # - X-DtPage -> params[:dtpage]
161
+ # - X-DtPerPage -> params[:dtperpage]
162
+ #
163
+ # @return [void]
164
+ def parse_headers_to_params
165
+ parse_header_to_params(:dtsearch, 'X-Dtsearch')
166
+ parse_header_to_params(:dtsort, 'X-Dtsort')
167
+ parse_header_to_params(:dtfilter, 'X-Dtfilter')
168
+ parse_header_to_params(:dtpage, 'X-DtPage')
169
+ parse_header_to_params(:dtperpage, 'X-DtPerPage')
170
+ end
171
+
172
+ # Helper method to parse a single header into a parameter if the parameter is not already set.
173
+ #
174
+ # @api private
175
+ #
176
+ # @param param_key [Symbol] the parameter key to set
177
+ # @param header_key [String] the header key to read from
178
+ #
179
+ # @return [void]
180
+ def parse_header_to_params(param_key, header_key)
181
+ return unless request.headers[header_key].present?
182
+ return if params[param_key].present?
183
+
184
+ values = request.headers[header_key].split(',').map(&:strip).reject(&:empty?)
185
+ params[param_key] = values.join(',')
186
+ end
187
+
188
+ # Renders Turbo Stream responses for datatable updates.
189
+ #
190
+ # @api private
191
+ #
192
+ # This method is used to dynamically update the datatable rows, pagination controls,
193
+ # and filters via Turbo Streams when the datatable requests new data.
194
+ #
195
+ # @dt_config must be set before calling this method.
196
+ #
197
+ # @return [void]
198
+ def render_dt_turbo_streams
199
+ dt_config_id = @dt_config[:id]
200
+ render turbo_stream: [
201
+ turbo_stream.replace(
202
+ "datatable-body-#{dt_config_id}",
203
+ partial: 'smriti/admin/ui/datatable_tbody',
204
+ locals: { row_meta: @row_meta }
205
+ ),
206
+ turbo_stream.replace(
207
+ "datatable-tfoot-#{dt_config_id}",
208
+ partial: 'smriti/admin/ui/datatable_tfoot',
209
+ locals: { dt_config: @dt_config, data: @data }
210
+ ),
211
+ if index_dt_config[:filter_enabled]
212
+ turbo_stream.replace(
213
+ "datatable-filters-#{dt_config_id}",
214
+ partial: 'smriti/admin/ui/datatable_filters',
215
+ locals: { dt_config: @dt_config, dtfilter: params[:dtfilter] || '' }
216
+ )
217
+ end
218
+ ].compact
219
+ end
220
+
221
+ # Accessor methods for datatable filter parameter
222
+ #
223
+ # @api private
224
+ #
225
+ # @return [String, nil] the dtfilter parameter from params
226
+ def param_dtfilter
227
+ params[:dtfilter]
228
+ end
229
+
230
+ # Accessor methods for datatable search parameter
231
+ #
232
+ # @api private
233
+ #
234
+ # @return [String, nil] the dtsearch parameter from params
235
+ def param_dtsearch
236
+ params[:dtsearch]
237
+ end
238
+
239
+ # Accessor methods for datatable sort parameter
240
+ #
241
+ # @api private
242
+ #
243
+ # @return [String, nil] the dtsort parameter from params
244
+ def param_dtsort
245
+ params[:dtsort]
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,70 @@
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 Smriti
9
+ module Admin
10
+ # Smriti::Admin::LocalizedDigitHelper
11
+ # -----------------------------------
12
+ # Helper methods for localizing digits in strings and dates.
13
+ # This is used in the admin UI to display numbers according to the current locale.
14
+ #
15
+ # Responsibilities:
16
+ # - Map ASCII digits (0-9) to localized representations based on I18n.t('numbers').
17
+ # - Replace digits in strings or numeric inputs with their localized equivalents.
18
+ # - Localize dates/times with localized digits.
19
+ #
20
+ # Methods:
21
+ # - localized_numbers: returns a hash mapping '0'-'9' to localized strings.
22
+ # - localized_digits: replaces digits in a string or number with localized versions.
23
+ # - l_with_digits: localizes an object (e.g. date/time) and replaces digits.
24
+ module LocalizedDigitHelper
25
+ private
26
+
27
+ # Replaces ASCII digits in the input with their localized equivalents.
28
+ #
29
+ # @api private
30
+ #
31
+ # @param str_or_num [String, Numeric] the input string or number
32
+ # @return [String] the input with digits replaced by localized versions
33
+ def localized_digits(str_or_num)
34
+ str_or_num.to_s.gsub(/[0-9]/, localized_numbers)
35
+ end
36
+
37
+ # Returns a hash mapping ASCII digits ('0'-'9') to their localized equivalents.
38
+ #
39
+ # @api private
40
+ #
41
+ # @return [Hash{String => String}] mapping of '0'-'9' to localized strings
42
+ def localized_numbers
43
+ map = I18n.t('numbers', default: nil)
44
+ {
45
+ '0' => map[:zero],
46
+ '1' => map[:one],
47
+ '2' => map[:two],
48
+ '3' => map[:three],
49
+ '4' => map[:four],
50
+ '5' => map[:five],
51
+ '6' => map[:six],
52
+ '7' => map[:seven],
53
+ '8' => map[:eight],
54
+ '9' => map[:nine]
55
+ }.compact
56
+ end
57
+
58
+ # Localizes an object (e.g. date/time) using I18n.l and replaces digits with localized versions.
59
+ #
60
+ # @api private
61
+ #
62
+ # @param obj [Object] the object to localize
63
+ # @param kwargs [Hash] additional keyword arguments passed to I18n.l
64
+ # @return [String] the localized string with digits replaced
65
+ def l_with_digits(obj, **)
66
+ localized_digits(I18n.l(obj, **))
67
+ end
68
+ end
69
+ end
70
+ end