yummy-guide-generic-administrate 0.8.13 → 0.8.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f39b663ae3463f915b180637532395e54c341dafac50ad331dbb600ab7e275ab
4
- data.tar.gz: c690e6ccabd5efdb8a31b74b8c6b3ff0cebe8c54e694bba11725c663108ed84d
3
+ metadata.gz: a9da6643ffb00010859cc51766113d7c757744a92cb1415ce70a30dafb33c194
4
+ data.tar.gz: 984902613fa65606e1ca1404bed57cc90b40cf821f678343583ec80e42ee8d95
5
5
  SHA512:
6
- metadata.gz: ff05d854eaa89f3110eec3936ea31ee769c14a6787ca3dad63070957b38ebc9e185a9a36536f7fcfe86bba2525e15d3ea9929caf16416534b609ab371be07238
7
- data.tar.gz: 436be499cc212d05255da8a299ca251a50390199ba73bcd9de6c560e614e3d173df728dd9910e27a25c430fed4e729f190ed74187fda3047e1b45ae3446aa93f
6
+ metadata.gz: 7cb593f606f48ca2bb18af508fd9df0e567cabfb457ae105901152d7501bc32c069ec5c7e662365e26084cd5b1095e949a9ea920d206da2525eec17db08bca2e
7
+ data.tar.gz: 1ea3603669a04a6a83ef151dfe48b2c961befdd8e1d6de81f13407fb871980b5556f4e14d4db3ab47746b1907cc83b832354d2cd57db14190b4f649296c01f53
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
2
+ <rect x="2.25" y="2.25" width="11.5" height="11.5" rx="2" stroke="#000" stroke-width="1.5"/>
3
+ <path d="m4.85 8.15 2.05 2.05 4.25-4.4" stroke="#000" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2
+ <path fill="#000" d="M9.22 1.66a1.75 1.75 0 0 1 2.48 0l2.64 2.64a1.75 1.75 0 0 1 0 2.48l-6.18 6.18a1.75 1.75 0 0 1-2.48 0L3.04 10.32a1.75 1.75 0 0 1 0-2.48l6.18-6.18Zm.95 1.24L4.28 8.79a.4.4 0 0 0 0 .58l2.35 2.35a.4.4 0 0 0 .58 0l5.89-5.89a.4.4 0 0 0 0-.58L10.75 2.9a.4.4 0 0 0-.58 0Z"/>
3
+ <path fill="#000" d="M7.85 4.72a.67.67 0 0 1 .95 0l2.48 2.48a.67.67 0 1 1-.95.95L7.85 5.67a.67.67 0 0 1 0-.95ZM2.25 13.58c0-.37.3-.66.67-.66h10.16a.67.67 0 0 1 0 1.33H2.92a.67.67 0 0 1-.67-.67Z"/>
4
+ </svg>
@@ -1,30 +1,92 @@
1
1
  (function() {
2
2
  function initialize(root) {
3
3
  var scope = root || document;
4
- scope.querySelectorAll('[data-admin-filter-form="true"]').forEach(function(formEl) {
5
- if (formEl.dataset.adminFilterControlsInitialized === 'true') return;
6
-
7
- formEl.dataset.adminFilterControlsInitialized = 'true';
8
-
9
- formEl.addEventListener('submit', function(event) {
10
- if (formEl.dataset.submitMode !== 'event') return;
11
-
12
- event.preventDefault();
13
- document.dispatchEvent(new CustomEvent('yummy-guide:administrate-filter:submit', {
14
- detail: {
15
- form: formEl,
16
- formData: new FormData(formEl)
17
- }
18
- }));
19
- });
4
+
5
+ scope.querySelectorAll('[data-admin-filter-form="true"]').forEach(initializeForm);
6
+ scope.querySelectorAll('[data-admin-filter-controls="true"]').forEach(initializeControls);
7
+ }
8
+
9
+ function initializeForm(formEl) {
10
+ if (formEl.dataset.adminFilterControlsInitialized === "true") return;
11
+
12
+ formEl.dataset.adminFilterControlsInitialized = "true";
13
+
14
+ formEl.addEventListener("submit", function(event) {
15
+ if (formEl.dataset.submitMode !== "event") return;
16
+
17
+ event.preventDefault();
18
+ document.dispatchEvent(new CustomEvent("yummy-guide:administrate-filter:submit", {
19
+ detail: {
20
+ form: formEl,
21
+ formData: new FormData(formEl)
22
+ }
23
+ }));
24
+ });
25
+ }
26
+
27
+ function initializeControls(rootEl) {
28
+ if (!rootEl || rootEl.dataset.adminFilterModalInitialized === "true") return;
29
+
30
+ var modalRootEl = modalRootFor(rootEl);
31
+ var formScopeEl = modalRootEl || rootEl;
32
+ var triggerEl = rootEl.querySelector("a, button");
33
+ var formEl = formScopeEl.querySelector("form");
34
+ var overlayEl = overlayFor(rootEl, modalRootEl);
35
+
36
+ if (!triggerEl || !formEl || !overlayEl) return;
37
+
38
+ rootEl.dataset.adminFilterModalInitialized = "true";
39
+
40
+ triggerEl.addEventListener("click", showForm);
41
+ overlayEl.addEventListener("click", hideForm);
42
+ formEl.querySelectorAll('[data-behavior="filter-form-close"]').forEach(function(buttonEl) {
43
+ buttonEl.addEventListener("click", hideForm);
44
+ });
45
+ document.addEventListener("keydown", function(event) {
46
+ if (event.key === "Escape") hideForm();
20
47
  });
48
+
49
+ function showForm(event) {
50
+ if (event) event.preventDefault();
51
+
52
+ document.documentElement.classList.add("admin-filter-open");
53
+ document.body.classList.add("admin-filter-open");
54
+ overlayEl.style.display = "block";
55
+ formEl.style.display = "flex";
56
+ }
57
+
58
+ function hideForm() {
59
+ document.documentElement.classList.remove("admin-filter-open");
60
+ document.body.classList.remove("admin-filter-open");
61
+ overlayEl.style.removeProperty("display");
62
+ formEl.style.removeProperty("display");
63
+ }
64
+ }
65
+
66
+ function modalRootFor(rootEl) {
67
+ var modalId = rootEl.dataset.adminFilterModalId;
68
+ if (!modalId) return null;
69
+
70
+ return document.getElementById(modalId);
71
+ }
72
+
73
+ function overlayFor(rootEl, modalRootEl) {
74
+ if (modalRootEl) {
75
+ return modalRootEl.querySelector("[data-admin-filter-overlay]");
76
+ }
77
+
78
+ if (rootEl.previousElementSibling && rootEl.previousElementSibling.matches("[data-admin-filter-overlay]")) {
79
+ return rootEl.previousElementSibling;
80
+ }
81
+
82
+ return rootEl.querySelector("[data-admin-filter-overlay]");
21
83
  }
22
84
 
23
- if (document.readyState === 'loading') {
24
- document.addEventListener('DOMContentLoaded', function() { initialize(document); }, { once: true });
85
+ if (document.readyState === "loading") {
86
+ document.addEventListener("DOMContentLoaded", function() { initialize(document); }, { once: true });
25
87
  } else {
26
88
  initialize(document);
27
89
  }
28
90
 
29
- document.addEventListener('turbo:load', function() { initialize(document); });
91
+ document.addEventListener("turbo:load", function() { initialize(document); });
30
92
  })();
@@ -110,7 +110,6 @@
110
110
  var inputType = (inputEl.getAttribute("type") || "").toLowerCase();
111
111
 
112
112
  if (inputType === "checkbox" || inputType === "radio") return;
113
- if (inputType === "hidden" && inputEl.dataset.datetimePart !== "combined") return;
114
113
 
115
114
  inputEl.value = "";
116
115
  inputEl.setAttribute("value", "");
@@ -140,6 +140,11 @@
140
140
  box-shadow: 0 calc(0px - var(--admin-sticky-page-header-top, 0px)) 0 var(--admin-sticky-page-header-top, 0px) var(--admin-page-background, #f6f7f7);
141
141
  }
142
142
 
143
+ .app-container > .main-content:has(> .main-content__header > .main-content__header-pagination),
144
+ body.admin-body > main.main-content:has(> .main-content__header > .main-content__header-pagination) {
145
+ --admin-sticky-page-header-height: 8rem;
146
+ }
147
+
143
148
  .main-content__header--sticky-table-layout + .main-content__body,
144
149
  [data-reservations-sticky-header] + .main-content__body,
145
150
  .app-container > .main-content:has(> .main-content__body--flush [data-css-sticky-table]) > .main-content__body--flush,
@@ -188,6 +193,57 @@
188
193
  --admin-sticky-page-header-height: 6.75rem;
189
194
  --admin-sticky-page-header-z-index: 10;
190
195
  }
196
+
197
+ .app-container > .main-content:has(> .main-content__header > .main-content__header-pagination),
198
+ body.admin-body > main.main-content:has(> .main-content__header > .main-content__header-pagination) {
199
+ --admin-sticky-page-header-height: 9rem;
200
+ }
201
+ }
202
+
203
+ .main-content__header:has(> .main-content__header-pagination) {
204
+ padding-bottom: calc(var(--admin-sticky-page-header-pagination-height, 2.5rem) + 0.75rem);
205
+ }
206
+
207
+ .main-content__header-pagination {
208
+ align-items: center;
209
+ box-sizing: border-box;
210
+ display: flex;
211
+ inset-block-end: 0.5rem;
212
+ inset-inline-start: 50%;
213
+ justify-content: center;
214
+ max-inline-size: calc(100% - 2rem);
215
+ pointer-events: none;
216
+ position: absolute;
217
+ transform: translateX(-50%);
218
+ width: max-content;
219
+ z-index: 1;
220
+ }
221
+
222
+ .main-content__header-pagination .pagination {
223
+ align-items: center;
224
+ display: flex;
225
+ flex-wrap: wrap;
226
+ gap: 0.25rem 0.5rem;
227
+ justify-content: center;
228
+ margin: 0;
229
+ max-inline-size: 100%;
230
+ padding-left: 0;
231
+ padding-right: 0;
232
+ pointer-events: auto;
233
+ text-align: center;
234
+ }
235
+
236
+ .main-content__header-pagination .pagination .first,
237
+ .main-content__header-pagination .pagination .prev,
238
+ .main-content__header-pagination .pagination .page,
239
+ .main-content__header-pagination .pagination .next,
240
+ .main-content__header-pagination .pagination .last {
241
+ flex: 0 0 auto;
242
+ inline-size: auto;
243
+ margin: 0;
244
+ max-inline-size: none;
245
+ white-space: nowrap;
246
+ width: auto;
191
247
  }
192
248
 
193
249
  .scroll-table {
@@ -1044,6 +1100,8 @@ td.actions-column .sticky__item,
1044
1100
  }
1045
1101
 
1046
1102
  .yummy-guide-administrate-filter-form .filter_table {
1103
+ border-collapse: separate;
1104
+ border-spacing: 4px 0;
1047
1105
  width: 100%;
1048
1106
  }
1049
1107
 
@@ -1055,7 +1113,10 @@ td.actions-column .sticky__item,
1055
1113
  .yummy-guide-administrate-filter-form .filter_table td:first-child {
1056
1114
  width: 160px;
1057
1115
  font-weight: 600;
1116
+ overflow-wrap: anywhere;
1058
1117
  vertical-align: middle;
1118
+ white-space: normal;
1119
+ word-break: normal;
1059
1120
  }
1060
1121
 
1061
1122
  .yummy-guide-administrate-filter-form input[type="text"],
@@ -1084,6 +1145,206 @@ td.actions-column .sticky__item,
1084
1145
  box-shadow: 0 -12px 24px rgba(15, 23, 42, 0.08);
1085
1146
  }
1086
1147
 
1148
+ #reserv-filter-options {
1149
+ position: relative;
1150
+ }
1151
+
1152
+ [data-admin-filter-overlay] {
1153
+ display: none;
1154
+ position: fixed;
1155
+ inset: 0;
1156
+ z-index: 1000;
1157
+ background: rgba(15, 23, 42, 0.42);
1158
+ }
1159
+
1160
+ html.admin-filter-open,
1161
+ body.admin-filter-open {
1162
+ overflow: hidden;
1163
+ }
1164
+
1165
+ #reserv-filter-options > .filter-form,
1166
+ .admin-filter-modal-root > .filter-form {
1167
+ display: none;
1168
+ flex-direction: column;
1169
+ position: fixed;
1170
+ top: 50%;
1171
+ left: 50%;
1172
+ right: auto;
1173
+ z-index: 1001;
1174
+ width: calc(100vw - 48px);
1175
+ max-width: 720px;
1176
+ max-height: calc(100vh - 48px);
1177
+ padding: 0;
1178
+ background: #fff;
1179
+ border: 1px solid #d7dce5;
1180
+ border-radius: 8px;
1181
+ box-shadow: 0 18px 44px rgba(15, 23, 42, 0.18);
1182
+ transform: translate(-50%, -50%);
1183
+ overflow: hidden;
1184
+ overscroll-behavior: contain;
1185
+ }
1186
+
1187
+ .filter-form__header {
1188
+ flex: 0 0 auto;
1189
+ display: flex;
1190
+ align-items: center;
1191
+ justify-content: space-between;
1192
+ gap: 12px;
1193
+ padding: 20px 20px 16px;
1194
+ }
1195
+
1196
+ .filter-form__title {
1197
+ min-width: 0;
1198
+ margin: 0;
1199
+ padding: 0;
1200
+ font-size: 1.2rem;
1201
+ }
1202
+
1203
+ .filter-form__close {
1204
+ display: inline-flex;
1205
+ align-items: center;
1206
+ justify-content: center;
1207
+ flex: 0 0 32px;
1208
+ width: 32px;
1209
+ height: 32px;
1210
+ padding: 0;
1211
+ border: 1px solid #d0d7de;
1212
+ border-radius: 50%;
1213
+ background: #fff;
1214
+ color: #57606a;
1215
+ cursor: pointer;
1216
+ font: inherit;
1217
+ line-height: 1;
1218
+ }
1219
+
1220
+ .filter-form__close:hover,
1221
+ .filter-form__close:focus,
1222
+ .filter-field-clear-button:hover,
1223
+ .filter-field-clear-button:focus,
1224
+ .filter-icon-button:hover,
1225
+ .filter-icon-button:focus {
1226
+ border-color: #0d6efd;
1227
+ background: #eef5ff;
1228
+ color: #0d6efd;
1229
+ }
1230
+
1231
+ .filter-form__body {
1232
+ flex: 1 1 auto;
1233
+ min-height: 0;
1234
+ overflow-y: auto;
1235
+ overflow-x: hidden;
1236
+ overscroll-behavior: contain;
1237
+ -webkit-overflow-scrolling: touch;
1238
+ padding: 0 20px;
1239
+ }
1240
+
1241
+ .filter-form .filter_table {
1242
+ border-collapse: separate;
1243
+ border-spacing: 4px 0;
1244
+ width: 100%;
1245
+ }
1246
+
1247
+ .filter-form .filter_table td {
1248
+ padding: 8px 0;
1249
+ vertical-align: top;
1250
+ }
1251
+
1252
+ .filter-form .filter_table td:first-child {
1253
+ width: 160px;
1254
+ font-weight: 600;
1255
+ overflow-wrap: anywhere;
1256
+ vertical-align: middle;
1257
+ white-space: normal;
1258
+ word-break: normal;
1259
+ }
1260
+
1261
+ .filter-form input[type="text"],
1262
+ .filter-form input[type="number"],
1263
+ .filter-form input[type="search"],
1264
+ .filter-form input[type="date"],
1265
+ .filter-form select,
1266
+ .filter-form textarea,
1267
+ .filter-form .filter-datetime-group__time {
1268
+ width: 100%;
1269
+ min-height: 40px;
1270
+ height: 40px;
1271
+ box-sizing: border-box;
1272
+ border: 1px solid #dfe0e1;
1273
+ border-radius: 5px;
1274
+ background-color: #fff;
1275
+ }
1276
+
1277
+ .filter-form__actions {
1278
+ flex: 0 0 auto;
1279
+ display: flex;
1280
+ gap: 8px;
1281
+ justify-content: flex-end;
1282
+ align-items: center;
1283
+ flex-wrap: wrap;
1284
+ padding: 12px 20px 20px;
1285
+ background: #fff;
1286
+ border-top: 1px solid #e5e7eb;
1287
+ box-shadow: 0 -12px 24px rgba(15, 23, 42, 0.08);
1288
+ }
1289
+
1290
+ .filter-table__clear {
1291
+ width: 1%;
1292
+ padding-left: 0;
1293
+ text-align: right;
1294
+ vertical-align: middle;
1295
+ white-space: nowrap;
1296
+ }
1297
+
1298
+ .filter-form .filter_table td.filter-table__clear,
1299
+ .yummy-guide-administrate-filter-form .filter_table td.filter-table__clear {
1300
+ vertical-align: middle;
1301
+ }
1302
+
1303
+ .filter-field-clear-button,
1304
+ .filter-icon-button {
1305
+ display: inline-flex;
1306
+ align-items: center;
1307
+ justify-content: center;
1308
+ min-width: 40px;
1309
+ height: 32px;
1310
+ box-sizing: border-box;
1311
+ padding: 0 8px;
1312
+ border: 1px solid #d0d7de;
1313
+ border-radius: 5px;
1314
+ background: #fff;
1315
+ color: #57606a;
1316
+ cursor: pointer;
1317
+ font: inherit;
1318
+ font-size: 0.8rem;
1319
+ line-height: 1;
1320
+ }
1321
+
1322
+ .filter-control-icon {
1323
+ display: inline-block;
1324
+ flex: 0 0 16px;
1325
+ width: 16px;
1326
+ height: 16px;
1327
+ background-color: currentColor;
1328
+ }
1329
+
1330
+ .filter-control-icon--eraser {
1331
+ -webkit-mask: image-url("yummy_guide_administrate/icon-eraser.svg") center / contain no-repeat;
1332
+ mask: image-url("yummy_guide_administrate/icon-eraser.svg") center / contain no-repeat;
1333
+ }
1334
+
1335
+ .filter-control-icon--check-square {
1336
+ -webkit-mask: image-url("yummy_guide_administrate/icon-check-square.svg") center / contain no-repeat;
1337
+ mask: image-url("yummy_guide_administrate/icon-check-square.svg") center / contain no-repeat;
1338
+ }
1339
+
1340
+ .filter-checkbox-group__actions.filter-checkbox-group__actions--clear-cell {
1341
+ flex-direction: column;
1342
+ flex-wrap: nowrap;
1343
+ justify-content: flex-end;
1344
+ align-items: center;
1345
+ gap: 4px;
1346
+ }
1347
+
1087
1348
  .filter-datetime-group {
1088
1349
  display: grid;
1089
1350
  grid-template-columns: minmax(180px, 1fr) 88px auto 88px;
@@ -1195,11 +1456,26 @@ td.actions-column .sticky__item,
1195
1456
  }
1196
1457
 
1197
1458
  .yummy-guide-administrate-filter-form__actions,
1459
+ .filter-form__actions,
1198
1460
  .filter-checkbox-group__actions {
1199
1461
  flex-direction: column;
1200
1462
  align-items: stretch;
1201
1463
  }
1202
1464
 
1465
+ #reserv-filter-options > .filter-form,
1466
+ .admin-filter-modal-root > .filter-form {
1467
+ width: calc(100vw - 32px);
1468
+ max-height: calc(100vh - 32px);
1469
+ }
1470
+
1471
+ .filter-form__header {
1472
+ padding: 16px 16px 12px;
1473
+ }
1474
+
1475
+ .filter-form__body {
1476
+ padding: 0 16px;
1477
+ }
1478
+
1203
1479
  .filter-datetime-group {
1204
1480
  grid-template-columns: 1fr;
1205
1481
  }
@@ -3,6 +3,11 @@
3
3
  module YummyGuide
4
4
  module Administrate
5
5
  module FilterControlsHelper
6
+ FILTER_CONTROL_ICON_CLASS_MAP = {
7
+ "fa-eraser" => :eraser,
8
+ "fa-check-square" => :check_square
9
+ }.freeze
10
+
6
11
  def admin_filter_controls(
7
12
  dashboard: nil,
8
13
  page: nil,
@@ -73,7 +78,7 @@ module YummyGuide
73
78
  end
74
79
 
75
80
  safe_join([
76
- content_tag(:div, "", class: "modal_overlay"),
81
+ content_tag(:div, "", class: "modal_overlay", data: { admin_filter_overlay: true }),
77
82
  admin_filter_button_container(button_label: button_label, form_markup: form_markup)
78
83
  ])
79
84
  end
@@ -135,7 +140,7 @@ module YummyGuide
135
140
  end,
136
141
  content_tag(:div, class: "filter-form__actions") do
137
142
  safe_join([
138
- link_to("Clear", clear_path, class: "button button--outline-primary"),
143
+ link_to("Clear", clear_path, class: "button button--outline-primary", data: { behavior: "filter-form-clear-link" }),
139
144
  Array(extra_actions),
140
145
  f.submit(submit_label, class: "submit_filter")
141
146
  ].flatten)
@@ -159,7 +164,7 @@ module YummyGuide
159
164
  def admin_filter_modal_root(modal_id:, form_markup:)
160
165
  content_tag(:div, id: modal_id, class: "admin-filter-modal-root", data: { admin_filter_modal_root: true }) do
161
166
  safe_join([
162
- content_tag(:div, "", class: "modal_overlay"),
167
+ content_tag(:div, "", class: "modal_overlay", data: { admin_filter_overlay: true }),
163
168
  form_markup
164
169
  ])
165
170
  end
@@ -233,6 +238,91 @@ module YummyGuide
233
238
 
234
239
  values.deep_stringify_keys
235
240
  end
241
+
242
+ def filter_field_clear_button
243
+ button_tag(
244
+ type: "button",
245
+ class: "filter-field-clear-button",
246
+ data: { behavior: "filter-field-clear" },
247
+ aria: { label: "Clear this filter" },
248
+ title: "Clear this filter"
249
+ ) do
250
+ filter_control_icon(:eraser)
251
+ end
252
+ end
253
+
254
+ def filter_field_clear_cell
255
+ content_tag(:td, filter_field_clear_button, class: "filter-table__clear")
256
+ end
257
+
258
+ def checkbox_group_select_all_button(target:)
259
+ checkbox_group_action_button(
260
+ icon: :check_square,
261
+ label: "Select all",
262
+ behavior: "checkbox-group-select-all",
263
+ target: target
264
+ )
265
+ end
266
+
267
+ def checkbox_group_clear_all_button(target:)
268
+ checkbox_group_action_button(
269
+ icon: :eraser,
270
+ label: "Clear all",
271
+ behavior: "checkbox-group-clear-all",
272
+ target: target
273
+ )
274
+ end
275
+
276
+ def checkbox_group_action_cell(target:)
277
+ content_tag(:td, class: "filter-table__clear filter-table__checkbox-actions") do
278
+ content_tag(
279
+ :div,
280
+ safe_join([
281
+ checkbox_group_select_all_button(target: target),
282
+ checkbox_group_clear_all_button(target: target)
283
+ ]),
284
+ class: "filter-checkbox-group__actions filter-checkbox-group__actions--clear-cell"
285
+ )
286
+ end
287
+ end
288
+
289
+ def filter_control_icon(icon)
290
+ icon_name = icon.to_s.tr("_", "-")
291
+
292
+ content_tag(
293
+ :span,
294
+ "",
295
+ class: "filter-control-icon filter-control-icon--#{icon_name}",
296
+ data: { filter_icon: icon_name },
297
+ aria: { hidden: true }
298
+ )
299
+ end
300
+
301
+ def checkbox_group_action_button(label:, behavior:, target:, icon: nil, icon_class: nil, title: nil)
302
+ accessible_label = title || label
303
+ icon ||= filter_control_icon_name_from_class(icon_class)
304
+
305
+ button_tag(
306
+ type: "button",
307
+ class: "filter-icon-button",
308
+ data: { behavior: behavior, target: target },
309
+ aria: { label: accessible_label },
310
+ title: accessible_label
311
+ ) do
312
+ if icon.present?
313
+ filter_control_icon(icon)
314
+ else
315
+ label
316
+ end
317
+ end
318
+ end
319
+
320
+ def filter_control_icon_name_from_class(icon_class)
321
+ classes = icon_class.to_s.split
322
+ icon_class_name = FILTER_CONTROL_ICON_CLASS_MAP.keys.find { |class_name| classes.include?(class_name) }
323
+
324
+ FILTER_CONTROL_ICON_CLASS_MAP[icon_class_name]
325
+ end
236
326
  end
237
327
  end
238
328
  end
@@ -0,0 +1,7 @@
1
+ <% pagination = local_assigns[:pagination] %>
2
+
3
+ <% if pagination.present? %>
4
+ <div class="main-content__header-pagination">
5
+ <%= pagination %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <% content_for(:title) do %>
2
+ <%= display_resource_name(page.resource_name) %>
3
+ <% end %>
4
+
5
+ <header class="main-content__header main-content__header--sticky-table-layout">
6
+ <h1 class="main-content__page-title" id="page-title">
7
+ <%= content_for(:title) %>
8
+ </h1>
9
+
10
+ <% if show_search_bar %>
11
+ <%= render(
12
+ "search",
13
+ search_term: search_term,
14
+ resource_name: display_resource_name(page.resource_name)
15
+ ) %>
16
+ <% end %>
17
+
18
+ <div>
19
+ <%= link_to(
20
+ t(
21
+ "administrate.actions.new_resource",
22
+ name: display_resource_name(page.resource_name, singular: true).downcase
23
+ ),
24
+ [:new, namespace, page.resource_path.to_sym],
25
+ class: "button",
26
+ ) if accessible_action?(new_resource, :new) %>
27
+ </div>
28
+
29
+ <%= render("administrate/application/header_pagination", pagination: local_assigns[:pagination]) %>
30
+ </header>
@@ -0,0 +1,22 @@
1
+ <% pagination = render("pagination", resources: resources) %>
2
+
3
+ <%=
4
+ render("index_header",
5
+ resources: resources,
6
+ search_term: search_term,
7
+ page: page,
8
+ show_search_bar: show_search_bar,
9
+ pagination: pagination,
10
+ )
11
+ %>
12
+
13
+ <section class="main-content__body main-content__body--flush">
14
+ <%= render(
15
+ "collection",
16
+ collection_presenter: page,
17
+ collection_field_name: resource_name,
18
+ page: page,
19
+ resources: resources,
20
+ table_title: "page-title"
21
+ ) %>
22
+ </section>
@@ -17,14 +17,19 @@
17
17
  <% end %>
18
18
  </div>
19
19
  <div class="filter-checkbox-group__actions">
20
- <%= button_tag "Select all",
21
- type: "button",
22
- class: "button button--outline-primary filter-checkbox-group__action",
23
- data: { behavior: "checkbox-group-select-all", target: group_name } %>
24
- <%= button_tag "Clear all",
25
- type: "button",
26
- class: "button button--outline-primary filter-checkbox-group__action",
27
- data: { behavior: "checkbox-group-clear-all", target: group_name } %>
20
+ <%= button_tag type: "button",
21
+ class: "button button--outline-primary filter-checkbox-group__action filter-icon-button",
22
+ data: { behavior: "checkbox-group-select-all", target: group_name },
23
+ aria: { label: "Select all" },
24
+ title: "Select all" do %>
25
+ <%= filter_control_icon(:check_square) %>
26
+ <% end %>
27
+ <%= button_tag type: "button",
28
+ class: "button button--outline-primary filter-checkbox-group__action filter-icon-button",
29
+ data: { behavior: "checkbox-group-clear-all", target: group_name },
30
+ aria: { label: "Clear all" },
31
+ title: "Clear all" do %>
32
+ <%= filter_control_icon(:eraser) %>
33
+ <% end %>
28
34
  </div>
29
35
  </div>
30
-
@@ -1,4 +1,5 @@
1
1
  <% parts = yummy_guide_administrate_datetime_filter_parts(current_value, end_of_day: local_assigns[:end_of_day]) %>
2
+ <% minute_options = local_assigns[:minute_options].presence || yummy_guide_administrate_filter_minute_options %>
2
3
  <div
3
4
  class="filter-datetime-group"
4
5
  data-datetime-filter="true"
@@ -22,9 +23,8 @@
22
23
  disabled: parts[:date].blank?) %>
23
24
  <span class="filter-datetime-group__separator">:</span>
24
25
  <%= select_tag("#{form_scope}[#{field_name}_minute]",
25
- options_for_select(yummy_guide_administrate_filter_minute_options, parts[:minute]),
26
+ options_for_select(minute_options, parts[:minute]),
26
27
  class: "#{css_class} #{css_class}--minute filter-datetime-group__time",
27
28
  data: { datetime_part: "minute" },
28
29
  disabled: parts[:date].blank?) %>
29
30
  </div>
30
-
@@ -189,7 +189,12 @@ module YummyGuide
189
189
  protected
190
190
 
191
191
  def input_cell(view_context, _form, scope, current_values, locals)
192
- selected_values = Array(current_values[name.to_s]).map(&:to_s).reject(&:blank?)
192
+ selected_value = if current_values.key?(name.to_s)
193
+ current_values[name.to_s]
194
+ else
195
+ evaluate_option(options[:default], view_context, locals)
196
+ end
197
+ selected_values = Array(selected_value).map(&:to_s).reject(&:blank?)
193
198
  group_name = (options[:group] || name).to_s.dasherize
194
199
  controls = normalize_options(view_context, locals).map do |label, value|
195
200
  value_string = value.to_s
@@ -240,6 +245,7 @@ module YummyGuide
240
245
  from_name = (options[:from] || :"start_#{name}").to_sym
241
246
  to_name = (options[:to] || :"end_#{name}").to_sym
242
247
  css_class = evaluate_option(options[:css_class] || "#{scope}_#{name}", view_context, locals)
248
+ minute_options = evaluate_option(options[:minute_options], view_context, locals)
243
249
 
244
250
  view_context.safe_join([
245
251
  view_context.render(
@@ -248,7 +254,8 @@ module YummyGuide
248
254
  field_name: from_name,
249
255
  current_value: current_values[from_name.to_s],
250
256
  css_class: css_class,
251
- end_target: to_name
257
+ end_target: to_name,
258
+ minute_options: minute_options
252
259
  ),
253
260
  view_context.content_tag(:p, "〜", class: "filter-datetime-range-separator"),
254
261
  view_context.render(
@@ -257,7 +264,8 @@ module YummyGuide
257
264
  field_name: to_name,
258
265
  current_value: current_values[to_name.to_s],
259
266
  css_class: css_class,
260
- end_of_day: true
267
+ end_of_day: true,
268
+ minute_options: minute_options
261
269
  )
262
270
  ])
263
271
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module YummyGuide
4
4
  module Administrate
5
- VERSION = "0.8.13"
5
+ VERSION = "0.8.15"
6
6
  end
7
7
  end
@@ -358,6 +358,22 @@ RSpec.describe "column resizer assets" do
358
358
  expect(resizable_navigation_source).to include("z-index: var(--admin-navigation-z-index, 40)")
359
359
  end
360
360
 
361
+ # ページネーションを固定ページヘッダー下部中央に配置するCSSがあることを静的に確認する
362
+ it "places pagination at the bottom center of the sticky page header" do
363
+ expect(components_source).to include(".main-content__header-pagination")
364
+ expect(components_source).to include("inset-block-end: 0.5rem")
365
+ expect(components_source).to include("inset-inline-start: 50%")
366
+ expect(components_source).to include("transform: translateX(-50%)")
367
+ expect(components_source).to include("justify-content: center")
368
+ expect(components_source).to include("pointer-events: none")
369
+ expect(components_source).to include("pointer-events: auto")
370
+ expect(components_source).to include("--admin-sticky-page-header-height: 8rem")
371
+ expect(components_source).to include("--admin-sticky-page-header-height: 9rem")
372
+ expect(components_source).to include(".main-content__header:has(> .main-content__header-pagination)")
373
+ expect(components_source).to include("white-space: nowrap")
374
+ expect(components_source).to include("inline-size: auto")
375
+ end
376
+
361
377
  # 固定列リサイズ後にCSS変数のleftと幅を再計算する処理があることを静的に確認する
362
378
  it "recalculates CSS sticky-left offsets after applying column widths" do
363
379
  expect(javascript_source).to include("function refreshCssStickyLeftColumns(table)")
@@ -24,4 +24,23 @@ RSpec.describe "copy cell assets" do
24
24
  it "keeps attribute copy controls visible" do
25
25
  expect(components_source).to match(/\.attribute-data \.admin-copy-cell__link,\s*\.attribute-data \.admin-copy-cell__button:not\(\[disabled\]\) \{\s*opacity: 1;/m)
26
26
  end
27
+
28
+ # フィルター操作アイコンをGem内のSVG assetからCSS maskで描画することを静的に確認する
29
+ it "uses bundled SVG assets for filter control icons" do
30
+ expect(components_source).to include('mask: image-url("yummy_guide_administrate/icon-eraser.svg") center / contain no-repeat;')
31
+ expect(components_source).to include('mask: image-url("yummy_guide_administrate/icon-check-square.svg") center / contain no-repeat;')
32
+ end
33
+
34
+ # フィルター操作ボタンのセルが項目内で上下中央に配置されることを静的に確認する
35
+ it "keeps filter control cells vertically centered" do
36
+ expect(components_source).to include(".filter-form .filter_table td.filter-table__clear,\n.yummy-guide-administrate-filter-form .filter_table td.filter-table__clear {\n vertical-align: middle;")
37
+ expect(components_source).to include(".filter-checkbox-group__actions.filter-checkbox-group__actions--clear-cell {\n flex-direction: column;\n flex-wrap: nowrap;")
38
+ end
39
+
40
+ # フィルターの項目名・入力フォーム・操作ボタンの列間を4pxに保ち、項目名を折り返せることを静的に確認する
41
+ it "keeps filter table columns spaced and labels wrappable" do
42
+ expect(components_source.scan(/border-spacing: 4px 0;/).size).to be >= 2
43
+ expect(components_source.scan(/overflow-wrap: anywhere;/).size).to be >= 2
44
+ expect(components_source).to include(".filter-table__clear {\n width: 1%;\n padding-left: 0;")
45
+ end
27
46
  end
@@ -54,6 +54,10 @@ RSpec.describe YummyGuide::Administrate::FilterControlsHelper do
54
54
  expect(close_button["aria-label"]).to eq("Close")
55
55
  expect(close_button["type"]).to eq("button")
56
56
  expect(document.at_css("form.filter-form")["action"]).to eq("/admin/resources")
57
+ expect(document.at_css('[data-admin-filter-overlay="true"]')).to be_present
58
+ clear_button = document.at_css('button.filter-field-clear-button[data-behavior="filter-field-clear"]')
59
+ expect(clear_button).to be_present
60
+ expect(clear_button.at_css("span.filter-control-icon.filter-control-icon--eraser[data-filter-icon='eraser'][aria-hidden='true']")).to be_present
57
61
  expect(document.at_css('input[name="search_options[keyword]"]')["value"]).to eq("tokyo")
58
62
  expect(document.at_css('select[name="search_options[status]"] option[selected]')["value"]).to eq("closed")
59
63
  end
@@ -81,11 +85,42 @@ RSpec.describe YummyGuide::Administrate::FilterControlsHelper do
81
85
  expect(controls.at_css("#reserv-filter-options > a.button").text).to eq("Filter")
82
86
  expect(controls.at_css("#reserv-filter-options > form")).to be_nil
83
87
  expect(modal_id).to eq("admin-filter-modal-1")
84
- expect(modals.at_css("##{modal_id}.admin-filter-modal-root > .modal_overlay")).to be_present
88
+ expect(modals.at_css("##{modal_id}.admin-filter-modal-root > .modal_overlay[data-admin-filter-overlay='true']")).to be_present
85
89
  expect(modals.at_css("##{modal_id} > form.filter-form")["action"]).to eq("/admin/resources")
86
90
  expect(modals.at_css('input[name="search_options[keyword]"]')["value"]).to eq("tokyo")
87
91
  end
88
92
 
93
+ # checkbox group の一括操作ボタンがアプリ側 helper なしで描画されることを確認する
94
+ it "renders built-in checkbox group action controls" do
95
+ dashboard = Class.new
96
+ dashboard.const_set(
97
+ :FILTER_ATTRIBUTES,
98
+ {
99
+ statuses: YummyGuide::Administrate::Filters::CheckboxGroup.with_options(
100
+ label: "Status",
101
+ collection: [["Open", "open"], ["Closed", "closed"]],
102
+ group: "resource-statuses"
103
+ )
104
+ }.freeze
105
+ )
106
+
107
+ html = helper_host.admin_filter_controls(
108
+ dashboard: dashboard,
109
+ path: "/admin/resources",
110
+ search_options: { statuses: ["open"] }
111
+ )
112
+ document = fragment(html)
113
+
114
+ select_all_button = document.at_css('button.filter-icon-button[data-behavior="checkbox-group-select-all"][data-target="resource-statuses"]')
115
+ clear_all_button = document.at_css('button.filter-icon-button[data-behavior="checkbox-group-clear-all"][data-target="resource-statuses"]')
116
+
117
+ expect(document.at_css('input[type="checkbox"][name="search_options[statuses][]"][value="open"]')["checked"]).to eq("checked")
118
+ expect(select_all_button["aria-label"]).to eq("Select all")
119
+ expect(select_all_button.at_css("span.filter-control-icon.filter-control-icon--check-square[data-filter-icon='check-square'][aria-hidden='true']")).to be_present
120
+ expect(clear_all_button["aria-label"]).to eq("Clear all")
121
+ expect(clear_all_button.at_css("span.filter-control-icon.filter-control-icon--eraser[data-filter-icon='eraser'][aria-hidden='true']")).to be_present
122
+ end
123
+
89
124
  # boolean radio group が未指定・true・falseを横並びで描画し、ラベルと選択状態を反映することを確認する
90
125
  it "renders boolean radio group controls with custom labels" do
91
126
  dashboard = Class.new
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
+ require "nokogiri"
4
5
 
5
6
  RSpec.describe YummyGuide::Administrate::Filters do
6
7
  describe YummyGuide::Administrate::Filters::Resolver do
@@ -32,4 +33,22 @@ RSpec.describe YummyGuide::Administrate::Filters do
32
33
  expect(field.visible?(view_context, {})).to be(false)
33
34
  end
34
35
  end
36
+
37
+ describe YummyGuide::Administrate::Filters::CheckboxGroup do
38
+ # current_values に値がない場合は default 配列を選択状態として利用することを確認する
39
+ it "uses default values when current values do not include the field" do
40
+ view_context = ActionController::Base.helpers
41
+ form = double("form")
42
+ field = described_class.with_options(
43
+ collection: [["Open", "open"], ["Closed", "closed"]],
44
+ default: ["closed"]
45
+ ).with_name(:statuses)
46
+
47
+ html = field.send(:input_cell, view_context, form, :search_options, {}, {})
48
+ document = Nokogiri::HTML.fragment(html)
49
+
50
+ expect(document.at_css('input[value="closed"]')["checked"]).to eq("checked")
51
+ expect(document.at_css('input[value="open"]')["checked"]).to be_nil
52
+ end
53
+ end
35
54
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yummy-guide-generic-administrate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.13
4
+ version: 0.8.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - akatsuki-kk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-22 00:00:00.000000000 Z
11
+ date: 2026-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: administrate
@@ -89,7 +89,9 @@ files:
89
89
  - Gemfile
90
90
  - README.md
91
91
  - Rakefile
92
+ - app/assets/images/yummy_guide_administrate/icon-check-square.svg
92
93
  - app/assets/images/yummy_guide_administrate/icon-copy.svg
94
+ - app/assets/images/yummy_guide_administrate/icon-eraser.svg
93
95
  - app/assets/javascripts/yummy_guide_administrate/clipboards.js
94
96
  - app/assets/javascripts/yummy_guide_administrate/column_resizer.js
95
97
  - app/assets/javascripts/yummy_guide_administrate/datetime_input.js
@@ -119,6 +121,9 @@ files:
119
121
  - app/helpers/yummy_guide/administrate/filter_form_helper.rb
120
122
  - app/helpers/yummy_guide/administrate/number_input_helper.rb
121
123
  - app/helpers/yummy_guide/administrate/tooltip_helper.rb
124
+ - app/views/administrate/application/_header_pagination.html.erb
125
+ - app/views/administrate/application/_index_header.html.erb
126
+ - app/views/administrate/application/index.html.erb
122
127
  - app/views/fields/number/_form.html.erb
123
128
  - app/views/fields/yummy_guide_administrate/area/picture/_form.html.erb
124
129
  - app/views/fields/yummy_guide_administrate/area/picture/_index.html.erb