rest_framework 0.9.5 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +130 -0
  4. data/VERSION +1 -1
  5. data/app/views/layouts/rest_framework.html.erb +9 -183
  6. data/app/views/rest_framework/_breadcrumbs.html.erb +27 -0
  7. data/app/views/rest_framework/_head.html.erb +347 -190
  8. data/app/views/rest_framework/_header.html.erb +15 -0
  9. data/app/views/rest_framework/_heading.html.erb +10 -0
  10. data/app/views/rest_framework/_payloads.html.erb +36 -0
  11. data/app/views/rest_framework/_request_metadata.html.erb +16 -0
  12. data/app/views/rest_framework/_routes_and_forms.html.erb +52 -0
  13. data/app/views/rest_framework/head/_external.html.erb +7 -2
  14. data/app/views/rest_framework/header/_mode.html.erb +14 -0
  15. data/app/views/rest_framework/heading/_actions.html.erb +9 -0
  16. data/app/views/rest_framework/{_routes.html.erb → routes_and_forms/_routes.html.erb} +2 -2
  17. data/lib/rest_framework/controller_mixins/base.rb +11 -12
  18. data/lib/rest_framework/engine.rb +5 -3
  19. data/lib/rest_framework/filters/ransack.rb +6 -6
  20. data/lib/rest_framework/version.rb +3 -8
  21. data/lib/rest_framework.rb +11 -2
  22. data/vendor/assets/javascripts/rest_framework/external.min.js +1256 -0
  23. data/vendor/assets/stylesheets/rest_framework/{bootstrap-icons.min.css → external.min.css} +415 -0
  24. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-dark.min.css +1 -1
  25. data/vendor/assets/stylesheets/rest_framework/highlight-a11y-light.min.css +1 -1
  26. metadata +16 -33
  27. data/README.md +0 -1
  28. data/app/views/rest_framework/head/_shared.html +0 -164
  29. data/docs/CNAME +0 -1
  30. data/docs/Gemfile +0 -5
  31. data/docs/Gemfile.lock +0 -264
  32. data/docs/_config.yml +0 -19
  33. data/docs/_guide/1_routers.md +0 -110
  34. data/docs/_guide/2_controllers.md +0 -342
  35. data/docs/_guide/3_serializers.md +0 -60
  36. data/docs/_guide/4_filtering_and_ordering.md +0 -41
  37. data/docs/_guide/5_pagination.md +0 -21
  38. data/docs/_includes/anchor_headings.html +0 -144
  39. data/docs/_includes/external.html +0 -9
  40. data/docs/_includes/head.html +0 -155
  41. data/docs/_includes/header.html +0 -58
  42. data/docs/_includes/shared.html +0 -164
  43. data/docs/_layouts/default.html +0 -11
  44. data/docs/assets/images/favicon.ico +0 -0
  45. data/docs/index.md +0 -133
  46. data/vendor/assets/javascripts/rest_framework/bootstrap.min.js +0 -6
  47. data/vendor/assets/javascripts/rest_framework/highlight-json.min.js +0 -7
  48. data/vendor/assets/javascripts/rest_framework/highlight-xml.min.js +0 -29
  49. data/vendor/assets/javascripts/rest_framework/highlight.min.js +0 -1202
  50. data/vendor/assets/javascripts/rest_framework/neatjson.min.js +0 -7
  51. data/vendor/assets/javascripts/rest_framework/trix.min.js +0 -6
  52. data/vendor/assets/stylesheets/rest_framework/bootstrap.min.css +0 -5
  53. data/vendor/assets/stylesheets/rest_framework/trix.min.css +0 -410
  54. /data/app/views/rest_framework/{_html_form.html.erb → routes_and_forms/_html_form.html.erb} +0 -0
  55. /data/app/views/rest_framework/{_raw_form.html.erb → routes_and_forms/_raw_form.html.erb} +0 -0
  56. /data/app/views/rest_framework/{_route.html.erb → routes_and_forms/routes/_route.html.erb} +0 -0
@@ -2,219 +2,376 @@
2
2
  <meta name="viewport" content="width=device-width, initial-scale=1">
3
3
 
4
4
  <%= render partial: "rest_framework/head/external" %>
5
- <%= render partial: "rest_framework/head/shared" %>
6
5
 
7
6
  <style>
8
- /* Header adjustments. */
9
- h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
10
-
11
- /* Reduce label font size. */
12
- label.form-label {
13
- font-size: .8em;
14
- }
15
-
16
- /* Make Trix buttons visible even in dark mode. */
17
- trix-toolbar .trix-button-group {
18
- background-color: #eee;
19
- }
20
-
21
- /* Make Trix dialog URL input visible in dark mode. */
22
- input.trix-input--dialog {
23
- color: black;
24
- }
25
-
26
- /* Make route group expansion obvious to the user. */
27
- .rrf-routes .rrf-route-group-header:hover {
28
- background-color: #ddd;
29
- }
30
- html[data-bs-theme="dark"] .rrf-routes .rrf-route-group-header:hover {
31
- background-color: #333;
32
- }
33
- .rrf-routes .rrf-route-group-header td {
34
- cursor: pointer;
35
- }
36
-
37
- /* Disable bootstrap's collapsing animation because in tables it causes delayed jerkiness. */
38
- .rrf-routes .collapsing {
39
- -webkit-transition: none;
40
- transition: none;
41
- display: none;
42
- }
43
-
44
- /* Copy-to-clipboard styles. */
45
- .rrf-copy {
46
- position: relative;
47
- }
48
- .rrf-copy .rrf-copy-link {
49
- position: absolute;
50
- top: .25em;
51
- right: .4em;
52
- transition: 0.3s ease;
53
- font-size: 1.5em;
54
- }
55
- .rrf-copy .rrf-copy-link.rrf-clicked {
56
- color: green;
57
- }
58
- .rrf-copy .rrf-copy-link.rrf-clicked:hover {
59
- color: green;
60
- }
7
+ :root {
8
+ --rrf-red: #900;
9
+ --rrf-red-hover: #5f0c0c;
10
+ --rrf-light-red: #db2525;
11
+ --rrf-light-red-hover: #b80404;
12
+ }
13
+ #rrfAccentBar {
14
+ background-color: var(--rrf-red);
15
+ height: .3em;
16
+ width: 100%;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+ header nav { background-color: black; }
21
+
22
+ /* Header adjustments. */
23
+ h1 { font-size: 2rem; }
24
+ h2 { font-size: 1.7rem; }
25
+ h3 { font-size: 1.5rem; }
26
+ h4 { font-size: 1.3rem; }
27
+ h5 { font-size: 1.1rem; }
28
+ h6 { font-size: 1rem; }
29
+ h1, h2, h3, h4, h5, h6 {
30
+ color: var(--rrf-red);
31
+ font-weight: normal;
32
+ margin-bottom: 0;
33
+ }
34
+ html[data-bs-theme="dark"] h1,
35
+ html[data-bs-theme="dark"] h2,
36
+ html[data-bs-theme="dark"] h3,
37
+ html[data-bs-theme="dark"] h4,
38
+ html[data-bs-theme="dark"] h5,
39
+ html[data-bs-theme="dark"] h6 {
40
+ color: var(--rrf-light-red);
41
+ }
42
+
43
+ /* Improve code and code blocks. */
44
+ pre code, .trix-content pre {
45
+ display: block;
46
+ overflow-x: auto;
47
+ padding: .5em !important;
48
+ }
49
+ code, .trix-content pre {
50
+ --bs-code-color: black;
51
+ background-color: #eee !important;
52
+ border: 1px solid #aaa;
53
+ padding: .1em .3em;
54
+ }
55
+ html[data-bs-theme="dark"] code, html[data-bs-theme="dark"] .trix-content pre {
56
+ --bs-code-color: white;
57
+ background-color: #2b2b2b !important;
58
+ }
59
+
60
+ /* Anchors */
61
+ a:not(.nav-link) {
62
+ text-decoration: none;
63
+ color: var(--rrf-red);
64
+ }
65
+ a:hover:not(.nav-link) {
66
+ text-decoration: underline;
67
+ color: var(--rrf-red-hover);
68
+ }
69
+ html[data-bs-theme="dark"] a:not(.nav-link) { color: var(--rrf-light-red); }
70
+ html[data-bs-theme="dark"] a:hover:not(.nav-link) { color: var(--rrf-light-red-hover); }
71
+
72
+ /* Reduce label font size. */
73
+ label.form-label {
74
+ font-size: .8em;
75
+ }
76
+
77
+ /* Make Trix buttons visible even in dark mode. */
78
+ trix-toolbar .trix-button-group {
79
+ background-color: #eee;
80
+ }
81
+
82
+ /* Make Trix dialog URL input visible in dark mode. */
83
+ input.trix-input--dialog {
84
+ color: black;
85
+ }
86
+
87
+ /* Make route group expansion obvious to the user. */
88
+ .rrf-routes .rrf-route-group-header:hover {
89
+ background-color: #ddd;
90
+ }
91
+ html[data-bs-theme="dark"] .rrf-routes .rrf-route-group-header:hover {
92
+ background-color: #333;
93
+ }
94
+ .rrf-routes .rrf-route-group-header td {
95
+ cursor: pointer;
96
+ }
97
+
98
+ /* Disable bootstrap's collapsing animation because in tables it causes delayed jerkiness. */
99
+ .rrf-routes .collapsing {
100
+ -webkit-transition: none;
101
+ transition: none;
102
+ display: none;
103
+ }
104
+
105
+ /* Copy-to-clipboard styles. */
106
+ .rrf-copy {
107
+ position: relative;
108
+ }
109
+ .rrf-copy .rrf-copy-link {
110
+ position: absolute;
111
+ top: .25em;
112
+ right: .4em;
113
+ transition: 0.3s ease;
114
+ font-size: 1.5em;
115
+ }
116
+ .rrf-copy .rrf-copy-link.rrf-clicked {
117
+ color: green;
118
+ }
119
+ .rrf-copy .rrf-copy-link.rrf-clicked:hover {
120
+ color: green;
121
+ }
61
122
  </style>
62
123
 
63
124
  <script>
64
- document.addEventListener("DOMContentLoaded", (event) => {
65
- // Pretty-print JSON.
66
- document.querySelectorAll(".language-json").forEach((el) => {
67
- el.innerHTML = neatJSON(JSON.parse(el.innerText), {
68
- wrap: 80,
69
- afterComma: 1,
70
- afterColon: 1,
71
- }).replaceAll("&", "&amp;")
72
- .replaceAll("<", "&lt;")
73
- .replaceAll(">", "&gt;")
74
- .replaceAll('"', "&quot;")
75
- .replaceAll("'", "&#039;")
76
- })
125
+ // Javascript for dark/light mode.
126
+ ;(() => {
127
+ // Get the real mode from a selected mode. Anything other than "light" or "dark" is treated as
128
+ // "system" mode.
129
+ const rrfGetRealMode = (selectedMode) => {
130
+ if (selectedMode === "light" || selectedMode === "dark") {
131
+ return selectedMode
132
+ }
77
133
 
78
- // Then highlight it.
79
- hljs.configure({cssSelector: "pre code.auto-hljs"})
80
- hljs.highlightAll()
134
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
135
+ return "dark"
136
+ }
81
137
 
82
- // Replace text node links with anchor tag links.
83
- document.querySelectorAll(".rrf-copy code").forEach((el) => {
84
- el.innerHTML = rrfLinkify(el.innerHTML)
85
- })
138
+ return "light"
139
+ }
86
140
 
87
- // Insert copy links.
88
- document.querySelectorAll(".rrf-copy").forEach((el) => {
89
- el.insertAdjacentHTML(
90
- "afterbegin",
91
- '<a class="rrf-copy-link" title="Copy to Clipboard" href="#"><i class="bi bi-clipboard-fill"></i></a>',
92
- )
93
- })
141
+ // Set the mode, given a "selected" mode.
142
+ const rrfSetSelectedMode = (selectedMode) => {
143
+ // Anything except "light" or "dark" is casted to "system".
144
+ if (selectedMode !== "light" && selectedMode !== "dark") {
145
+ selectedMode = "system"
146
+ }
94
147
 
95
- // Copy link implementation.
96
- document.querySelectorAll(".rrf-copy-link").forEach((el) => {
97
- el.addEventListener("click", (event) => {
98
- const range = document.createRange()
99
- range.selectNode(el.nextSibling)
100
- window.getSelection().removeAllRanges()
101
- window.getSelection().addRange(range)
102
- if (document.execCommand("copy")) {
103
- // Trigger clicked animation.
104
- el.classList.add("rrf-clicked")
105
- el.innerHTML = '<i class="bi bi-clipboard-check-fill">'
106
- setTimeout(() => {
107
- el.classList.remove("rrf-clicked")
108
- el.innerHTML = '<i class="bi bi-clipboard-fill">'
109
- }, 1000)
148
+ // Store selected mode in `localStorage`.
149
+ localStorage.setItem("rrfMode", selectedMode)
150
+
151
+ // Set the mode selector to the selected mode.
152
+ const modeComponent = document.getElementById("rrfModeComponent")
153
+ if (modeComponent) {
154
+ let labelHTML
155
+ modeComponent.querySelectorAll("button[data-rrf-mode-value]").forEach((el) => {
156
+ if (el.getAttribute("data-rrf-mode-value") === selectedMode) {
157
+ el.classList.add("active")
158
+ labelHTML = el.querySelector("i").outerHTML.replace("ms-2", "me-1")
159
+ } else {
160
+ el.classList.remove("active")
161
+ }
162
+ })
163
+ modeComponent.querySelector("button[data-bs-toggle]").innerHTML = labelHTML
110
164
  }
111
- event.preventDefault()
112
- })
113
- })
114
165
 
115
- // Check if `rawFilesFormWrapper` should be displayed when media type is changed.
116
- const rawFormRouteSelect = document.getElementById("rawFormRoute")
117
- const rawFormMediaTypeSelect = document.getElementById("rawFormMediaType")
118
- const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
119
- if (rawFilesFormWrapper) {
120
- const rawFormFilesHandler = () => {
121
- const selectedRouteOption = rawFormRouteSelect.options[rawFormRouteSelect.selectedIndex]
122
- if (rawFormMediaTypeSelect.value === "multipart/form-data" && selectedRouteOption.dataset.supportsFiles) {
123
- rawFilesFormWrapper.style.display = "block"
166
+ // Get the real mode to use.
167
+ realMode = rrfGetRealMode(selectedMode)
168
+
169
+ // Set the `realMode` effects.
170
+ if (realMode === "light") {
171
+ document.querySelectorAll(".rrf-light-mode").forEach((el) => {
172
+ el.disabled = false
173
+ })
174
+ document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
175
+ el.disabled = true
176
+ })
177
+ document.querySelectorAll(".rrf-mode").forEach((el) => {
178
+ el.setAttribute("data-bs-theme", "light")
179
+ })
180
+ } else if (realMode === "dark") {
181
+ document.querySelectorAll(".rrf-light-mode").forEach((el) => {
182
+ el.disabled = true
183
+ })
184
+ document.querySelectorAll(".rrf-dark-mode").forEach((el) => {
185
+ el.disabled = false
186
+ })
187
+ document.querySelectorAll(".rrf-mode").forEach((el) => {
188
+ el.setAttribute("data-bs-theme", "dark")
189
+ })
124
190
  } else {
125
- rawFilesFormWrapper.style.display = "none"
191
+ console.log(`RRF: Unknown mode: ${mode}`)
126
192
  }
127
193
  }
128
194
 
129
- rawFormRouteSelect.addEventListener("change", rawFormFilesHandler)
130
- rawFormMediaTypeSelect.addEventListener("change", rawFormFilesHandler)
131
- }
132
- })
133
-
134
- // Convert plain-text links to anchor tag links.
135
- function rrfLinkify(text) {
136
- return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\" target=\"_blank\">$1</a>")
137
- }
138
-
139
- // Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
140
- function rrfReplaceDocument(content) {
141
- // Replace the document with provided content.
142
- document.open()
143
- document.write(content)
144
- document.close()
145
-
146
- // Trigger `DOMContentLoaded` manually so our custom JavaScript works.
147
- document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
148
- }
149
-
150
- // Refresh the window as a `GET` request.
151
- function rrfGet(button) {
152
- button.disabled = true
153
- window.location.replace(window.location.href)
154
- }
155
-
156
- // Call `DELETE` on the current path.
157
- function rrfDelete(button) {
158
- button.disabled = true
159
- rrfAPICall(window.location.pathname, "DELETE")
160
- }
161
-
162
- // Call `OPTIONS` on the current path.
163
- function rrfOptions(button) {
164
- button.disabled = true
165
- rrfAPICall(window.location.pathname, "OPTIONS")
166
- }
167
-
168
- // Submit the raw form.
169
- function rrfSubmitRawForm(button) {
170
- button.disabled = true
171
-
172
- // Grab the selected route/method, media type, and the body.
173
- const [method, path] = document.getElementById("rawFormRoute").value.split(":")
174
- const mediaType = document.getElementById("rawFormMediaType").value
175
- let body = document.getElementById("rawFormContent").value
176
-
177
- // If the media type is `multipart/form-data`, then we need to build a FormData object.
178
- if (mediaType == "multipart/form-data") {
179
- let formData = new FormData()
180
-
181
- // Add body to `formData`.
182
- const bodySearchParams = new URLSearchParams(body)
183
- bodySearchParams.forEach((value, key) => {
184
- formData.append(key, value)
195
+ // Initialize dark/light mode before page fully loads to prevent flash.
196
+ rrfSetSelectedMode(localStorage.getItem("rrfMode"))
197
+
198
+ // Initialize dark/light mode after page load (mostly so mode component is updated).
199
+ document.addEventListener("DOMContentLoaded", (event) => {
200
+ rrfSetSelectedMode(localStorage.getItem("rrfMode"))
201
+
202
+ // Also set up mode selector.
203
+ document.querySelectorAll("#rrfModeComponent button[data-rrf-mode-value]").forEach((el) => {
204
+ el.addEventListener("click", (event) => {
205
+ rrfSetSelectedMode(event.target.getAttribute("data-rrf-mode-value"))
206
+ })
207
+ })
208
+ })
209
+
210
+ // Handle case where user changes system theme.
211
+ if (window.matchMedia) {
212
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
213
+ const selectedMode = localStorage.getItem("rrfMode")
214
+ if (selectedMode !== "light" && selectedMode !== "dark") {
215
+ rrfSetSelectedMode("system")
216
+ }
217
+ })
218
+ }
219
+ })();
220
+
221
+ document.addEventListener("DOMContentLoaded", (event) => {
222
+ // Pretty-print JSON.
223
+ document.querySelectorAll(".language-json").forEach((el) => {
224
+ el.innerHTML = neatJSON(JSON.parse(el.innerText), {
225
+ wrap: 80,
226
+ afterComma: 1,
227
+ afterColon: 1,
228
+ }).replaceAll("&", "&amp;")
229
+ .replaceAll("<", "&lt;")
230
+ .replaceAll(">", "&gt;")
231
+ .replaceAll('"', "&quot;")
232
+ .replaceAll("'", "&#039;")
233
+ })
234
+
235
+ // Then highlight it.
236
+ hljs.configure({cssSelector: "pre code"})
237
+ hljs.highlightAll()
238
+
239
+ // Replace text node links with anchor tag links.
240
+ document.querySelectorAll(".rrf-copy code").forEach((el) => {
241
+ el.innerHTML = rrfLinkify(el.innerHTML)
185
242
  })
186
243
 
187
- // Add file(s) to `formData`.
188
- const rawFilesForm = document.getElementById("rawFilesForm")
189
- if (rawFilesForm) {
190
- rawFilesForm.querySelectorAll("input[type=file]").forEach((el) => {
191
- const files = el.files
192
- for (let i = 0; i < files.length; i++) {
193
- formData.append(el.name, files[i])
244
+ // Insert copy links.
245
+ document.querySelectorAll(".rrf-copy").forEach((el) => {
246
+ el.insertAdjacentHTML(
247
+ "afterbegin",
248
+ '<a class="rrf-copy-link" title="Copy to Clipboard" href="#"><i class="bi bi-clipboard-fill"></i></a>',
249
+ )
250
+ })
251
+
252
+ // Copy link implementation.
253
+ document.querySelectorAll(".rrf-copy-link").forEach((el) => {
254
+ el.addEventListener("click", (event) => {
255
+ const range = document.createRange()
256
+ range.selectNode(el.nextSibling)
257
+ window.getSelection().removeAllRanges()
258
+ window.getSelection().addRange(range)
259
+ if (document.execCommand("copy")) {
260
+ // Trigger clicked animation.
261
+ el.classList.add("rrf-clicked")
262
+ el.innerHTML = '<i class="bi bi-clipboard-check-fill">'
263
+ setTimeout(() => {
264
+ el.classList.remove("rrf-clicked")
265
+ el.innerHTML = '<i class="bi bi-clipboard-fill">'
266
+ }, 1000)
194
267
  }
268
+ event.preventDefault()
195
269
  })
270
+ })
271
+
272
+ // Check if `rawFilesFormWrapper` should be displayed when media type is changed.
273
+ const rawFormRouteSelect = document.getElementById("rawFormRoute")
274
+ const rawFormMediaTypeSelect = document.getElementById("rawFormMediaType")
275
+ const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
276
+ if (rawFilesFormWrapper) {
277
+ const rawFormFilesHandler = () => {
278
+ const selectedRouteOption = rawFormRouteSelect.options[rawFormRouteSelect.selectedIndex]
279
+ if (rawFormMediaTypeSelect.value === "multipart/form-data" && selectedRouteOption.dataset.supportsFiles) {
280
+ rawFilesFormWrapper.style.display = "block"
281
+ } else {
282
+ rawFilesFormWrapper.style.display = "none"
283
+ }
284
+ }
285
+
286
+ rawFormRouteSelect.addEventListener("change", rawFormFilesHandler)
287
+ rawFormMediaTypeSelect.addEventListener("change", rawFormFilesHandler)
196
288
  }
289
+ })
197
290
 
198
- // Set body to be the form data.
199
- body = formData
291
+ // Convert plain-text links to anchor tag links.
292
+ function rrfLinkify(text) {
293
+ return text.replace(/(https?:\/\/[^\s<>"]+)/g, "<a href=\"$1\" target=\"_blank\">$1</a>")
200
294
  }
201
295
 
202
- // Perform the API call.
203
- rrfAPICall(path, method, {
204
- body,
205
- // If the media type is `multipart/form-data`, then we don't want to set the content type
206
- // because it must be set by `fetch` to include boundary.
207
- headers: mediaType == "multipart/form-data" ? {} : {"Content-Type": mediaType},
208
- })
209
- }
296
+ // Replace the document when doing form submission (mainly to support PUT/PATCH/DELETE).
297
+ function rrfReplaceDocument(content) {
298
+ // Replace the document with provided content.
299
+ document.open()
300
+ document.write(content)
301
+ document.close()
210
302
 
211
- // Make an HTML API call and replace the document with the response.
212
- function rrfAPICall(path, method, kwargs={}) {
213
- const headers = kwargs.headers || {}
214
- delete kwargs.headers
303
+ // Trigger `DOMContentLoaded` manually so our custom JavaScript works.
304
+ document.dispatchEvent(new Event("DOMContentLoaded", {bubbles: true, cancelable: true}))
305
+ }
215
306
 
216
- fetch(path, {method, headers: {"Accept": "text/html", ...headers}, ...kwargs})
217
- .then((response) => response.text())
218
- .then((body) => { rrfReplaceDocument(body) })
219
- }
307
+ // Refresh the window as a `GET` request.
308
+ function rrfGet(button) {
309
+ button.disabled = true
310
+ window.location.replace(window.location.href)
311
+ }
312
+
313
+ // Call `DELETE` on the current path.
314
+ function rrfDelete(button) {
315
+ button.disabled = true
316
+ rrfAPICall(window.location.pathname, "DELETE")
317
+ }
318
+
319
+ // Call `OPTIONS` on the current path.
320
+ function rrfOptions(button) {
321
+ button.disabled = true
322
+ rrfAPICall(window.location.pathname, "OPTIONS")
323
+ }
324
+
325
+ // Submit the raw form.
326
+ function rrfSubmitRawForm(button) {
327
+ button.disabled = true
328
+
329
+ // Grab the selected route/method, media type, and the body.
330
+ const [method, path] = document.getElementById("rawFormRoute").value.split(":")
331
+ const mediaType = document.getElementById("rawFormMediaType").value
332
+ let body = document.getElementById("rawFormContent").value
333
+
334
+ // If the media type is `multipart/form-data`, then we need to build a FormData object.
335
+ if (mediaType == "multipart/form-data") {
336
+ let formData = new FormData()
337
+
338
+ // Add body to `formData`.
339
+ const bodySearchParams = new URLSearchParams(body)
340
+ bodySearchParams.forEach((value, key) => {
341
+ formData.append(key, value)
342
+ })
343
+
344
+ // Add file(s) to `formData`.
345
+ const rawFilesForm = document.getElementById("rawFilesForm")
346
+ if (rawFilesForm) {
347
+ rawFilesForm.querySelectorAll("input[type=file]").forEach((el) => {
348
+ const files = el.files
349
+ for (let i = 0; i < files.length; i++) {
350
+ formData.append(el.name, files[i])
351
+ }
352
+ })
353
+ }
354
+
355
+ // Set body to be the form data.
356
+ body = formData
357
+ }
358
+
359
+ // Perform the API call.
360
+ rrfAPICall(path, method, {
361
+ body,
362
+ // If the media type is `multipart/form-data`, then we don't want to set the content type
363
+ // because it must be set by `fetch` to include boundary.
364
+ headers: mediaType == "multipart/form-data" ? {} : {"Content-Type": mediaType},
365
+ })
366
+ }
367
+
368
+ // Make an HTML API call and replace the document with the response.
369
+ function rrfAPICall(path, method, kwargs={}) {
370
+ const headers = kwargs.headers || {}
371
+ delete kwargs.headers
372
+
373
+ fetch(path, {method, headers: {"Accept": "text/html", ...headers}, ...kwargs})
374
+ .then((response) => response.text())
375
+ .then((body) => { rrfReplaceDocument(body) })
376
+ }
220
377
  </script>
@@ -0,0 +1,15 @@
1
+ <div class="w-100 m-0 p-0" id="rrfAccentBar"></div>
2
+ <nav class="navbar navbar-expand-md py-0" data-bs-theme="dark">
3
+ <div class="container">
4
+ <span class="navbar-brand p-0">
5
+ <h1 title="RRF v<%= RESTFramework::VERSION %>" class="text-light font-weight-light m-0 p-0" style="font-size: 1em">
6
+ <%= @template_logo_text || @header_title || "Rails REST Framework" %>
7
+ </h1>
8
+ </span>
9
+ <% if content_for? :header_menu %>
10
+ <%= yield :header_menu %>
11
+ <% else %>
12
+ <%= render partial: "rest_framework/header/mode" %>
13
+ <% end %>
14
+ </div>
15
+ </nav>
@@ -0,0 +1,10 @@
1
+ <div class="row">
2
+ <div>
3
+ <%= render partial: "rest_framework/heading/actions" if @route_groups.present? %>
4
+ <h1 class="m-0"><%= @heading_title || @title %></h1>
5
+ <% if @description.present? %>
6
+ <br><br><p style="display: inline-block; margin-bottom: 0"><%= @description %></p>
7
+ <% end %>
8
+ </div>
9
+ </div>
10
+ <hr/>
@@ -0,0 +1,36 @@
1
+ <div class="row">
2
+ <div>
3
+ <ul class="nav nav-tabs">
4
+ <% if @json_payload.present? %>
5
+ <li class="nav-item">
6
+ <a class="nav-link active" href="#tab-json" data-bs-toggle="tab" role="tab">
7
+ .json
8
+ </a>
9
+ </li>
10
+ <% end %>
11
+ <% if @xml_payload.present? %>
12
+ <li class="nav-item">
13
+ <a class="nav-link" href="#tab-xml" data-bs-toggle="tab" role="tab">
14
+ .xml
15
+ </a>
16
+ </li>
17
+ <% end %>
18
+ </ul>
19
+ </div>
20
+ <div class="tab-content pt-2">
21
+ <div class="tab-pane fade show active" id="tab-json" role="tabpanel">
22
+ <% if @json_payload.present? %>
23
+ <div><pre class="rrf-copy"><code class="language-json"><%=
24
+ JSON.pretty_generate(JSON.parse(@json_payload)) unless @json_payload == ""
25
+ %></code></pre></div>
26
+ <% end %>
27
+ </div>
28
+ <div class="tab-pane fade" id="tab-xml" role="tabpanel">
29
+ <% if @xml_payload.present? %>
30
+ <div><pre class="rrf-copy"><code class="language-xml"><%=
31
+ CGI.unescapeHTML(@xml_payload)
32
+ %></code></pre></div>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="row">
2
+ <div>
3
+ <pre><code><%
4
+ concat request.request_method
5
+ if request.method != request.request_method
6
+ concat " (via #{request.method})"
7
+ end
8
+ concat " #{request.path}"
9
+ %></code></pre>
10
+ <pre><code><%
11
+ concat "HTTP #{response.status} #{response.message}"
12
+ concat "\n"
13
+ concat "Content-Type: #{response.content_type}"
14
+ %></code></pre>
15
+ </div>
16
+ </div>