iron-cms 0.17.3 → 0.18.1

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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -4
  3. data/app/assets/builds/iron.css +236 -106
  4. data/app/assets/tailwind/iron/lexxy.css +111 -87
  5. data/app/controllers/concerns/iron/schema_editing.rb +19 -0
  6. data/app/controllers/concerns/iron/web_page.rb +3 -1
  7. data/app/controllers/iron/api/schema/base_controller.rb +25 -0
  8. data/app/controllers/iron/api/schema/block_definitions_controller.rb +49 -0
  9. data/app/controllers/iron/api/schema/content_types_controller.rb +64 -0
  10. data/app/controllers/iron/api/schema/field_definitions_controller.rb +88 -0
  11. data/app/controllers/iron/api/schema/locales_controller.rb +52 -0
  12. data/app/controllers/iron/application_controller.rb +1 -1
  13. data/app/controllers/iron/block_definitions_controller.rb +1 -0
  14. data/app/controllers/iron/content_types_controller.rb +1 -0
  15. data/app/controllers/iron/field_definitions_controller.rb +2 -1
  16. data/app/controllers/iron/first_runs_controller.rb +1 -1
  17. data/app/controllers/iron/locales_controller.rb +1 -0
  18. data/app/controllers/iron/sessions_controller.rb +1 -1
  19. data/app/helpers/iron/avatar_helper.rb +24 -2
  20. data/app/models/iron/account.rb +1 -1
  21. data/app/models/iron/api/openapi_spec.rb +37 -15
  22. data/app/models/iron/block_definition/exportable.rb +1 -1
  23. data/app/models/iron/block_definition.rb +3 -1
  24. data/app/models/iron/content.rb +176 -0
  25. data/app/models/iron/content_type/exportable.rb +3 -1
  26. data/app/models/iron/content_type/importable.rb +1 -1
  27. data/app/models/iron/content_type.rb +6 -1
  28. data/app/models/iron/entry/content_assignable.rb +10 -2
  29. data/app/models/iron/entry/presentable.rb +1 -1
  30. data/app/models/iron/entry/web_publishable.rb +17 -3
  31. data/app/models/iron/exporter.rb +1 -26
  32. data/app/models/iron/field/length_constrained.rb +17 -0
  33. data/app/models/iron/field/validatable.rb +16 -0
  34. data/app/models/iron/field.rb +1 -1
  35. data/app/models/iron/field_definition/exportable.rb +3 -4
  36. data/app/models/iron/field_definition/importable.rb +16 -9
  37. data/app/models/iron/field_definition/ranked.rb +1 -1
  38. data/app/models/iron/field_definition/searchable.rb +2 -0
  39. data/app/models/iron/field_definition/validatable.rb +40 -0
  40. data/app/models/iron/field_definition/validations.rb +175 -0
  41. data/app/models/iron/field_definition.rb +1 -1
  42. data/app/models/iron/field_definitions/date.rb +2 -0
  43. data/app/models/iron/field_definitions/file.rb +3 -11
  44. data/app/models/iron/field_definitions/number.rb +2 -0
  45. data/app/models/iron/field_definitions/reference.rb +2 -0
  46. data/app/models/iron/field_definitions/rich_text_area.rb +1 -0
  47. data/app/models/iron/field_definitions/text_area.rb +2 -0
  48. data/app/models/iron/field_definitions/text_field.rb +1 -9
  49. data/app/models/iron/fields/block.rb +5 -1
  50. data/app/models/iron/fields/block_list.rb +5 -1
  51. data/app/models/iron/fields/date.rb +19 -2
  52. data/app/models/iron/fields/file.rb +5 -3
  53. data/app/models/iron/fields/number.rb +28 -1
  54. data/app/models/iron/fields/reference.rb +2 -0
  55. data/app/models/iron/fields/rich_text_area.rb +2 -0
  56. data/app/models/iron/fields/text_area.rb +4 -0
  57. data/app/models/iron/fields/text_field.rb +9 -5
  58. data/app/models/iron/importer.rb +1 -54
  59. data/app/models/iron/locale.rb +2 -0
  60. data/app/models/iron/schema/auto_dumpable.rb +26 -0
  61. data/app/models/iron/schema/diff.rb +194 -0
  62. data/app/models/iron/schema/validation.rb +214 -0
  63. data/app/models/iron/schema.rb +282 -0
  64. data/app/models/iron/seed.rb +5 -3
  65. data/app/models/iron/system.rb +7 -0
  66. data/app/models/iron/user/deactivatable.rb +5 -0
  67. data/app/models/iron/user.rb +18 -1
  68. data/app/views/iron/api/fields/_rich_text_area.json.jbuilder +2 -2
  69. data/app/views/iron/api/schema/block_definitions/_block_definition.json.jbuilder +4 -0
  70. data/app/views/iron/api/schema/block_definitions/index.json.jbuilder +1 -0
  71. data/app/views/iron/api/schema/block_definitions/show.json.jbuilder +1 -0
  72. data/app/views/iron/api/schema/content_types/_content_type.json.jbuilder +5 -0
  73. data/app/views/iron/api/schema/content_types/index.json.jbuilder +1 -0
  74. data/app/views/iron/api/schema/content_types/show.json.jbuilder +1 -0
  75. data/app/views/iron/api/schema/field_definitions/_field_definition.json.jbuilder +7 -0
  76. data/app/views/iron/api/schema/field_definitions/show.json.jbuilder +1 -0
  77. data/app/views/iron/api/schema/locales/_locale.json.jbuilder +4 -0
  78. data/app/views/iron/api/schema/locales/index.json.jbuilder +1 -0
  79. data/app/views/iron/api/schema/locales/show.json.jbuilder +1 -0
  80. data/app/views/iron/block_definitions/_empty_state.html.erb +5 -3
  81. data/app/views/iron/block_definitions/_form.html.erb +3 -1
  82. data/app/views/iron/block_definitions/edit.html.erb +19 -17
  83. data/app/views/iron/block_definitions/index.html.erb +7 -3
  84. data/app/views/iron/block_definitions/show.html.erb +14 -8
  85. data/app/views/iron/content_types/_content_type.html.erb +1 -1
  86. data/app/views/iron/content_types/_empty_state.html.erb +5 -3
  87. data/app/views/iron/content_types/_form.html.erb +12 -8
  88. data/app/views/iron/content_types/edit.html.erb +19 -17
  89. data/app/views/iron/content_types/index.html.erb +7 -3
  90. data/app/views/iron/content_types/show.html.erb +14 -8
  91. data/app/views/iron/entries/_empty_state.html.erb +1 -1
  92. data/app/views/iron/entries/edit.html.erb +1 -1
  93. data/app/views/iron/entries/entry.html.erb +1 -1
  94. data/app/views/iron/entries/fields/_block.html.erb +4 -0
  95. data/app/views/iron/entries/fields/_block_list.html.erb +2 -0
  96. data/app/views/iron/entries/fields/_boolean.html.erb +1 -0
  97. data/app/views/iron/entries/fields/_date.html.erb +3 -2
  98. data/app/views/iron/entries/fields/_field_errors.html.erb +7 -0
  99. data/app/views/iron/entries/fields/_field_label.html.erb +8 -0
  100. data/app/views/iron/entries/fields/_file.html.erb +4 -19
  101. data/app/views/iron/entries/fields/_number.html.erb +6 -2
  102. data/app/views/iron/entries/fields/_reference.html.erb +2 -1
  103. data/app/views/iron/entries/fields/_reference_list.html.erb +2 -0
  104. data/app/views/iron/entries/fields/_rich_text_area.html.erb +2 -1
  105. data/app/views/iron/entries/fields/_text_area.html.erb +6 -2
  106. data/app/views/iron/entries/fields/_text_field.html.erb +5 -12
  107. data/app/views/iron/field_definitions/_field_definition.html.erb +51 -29
  108. data/app/views/iron/field_definitions/date/_form.html.erb +5 -1
  109. data/app/views/iron/field_definitions/edit.html.erb +10 -8
  110. data/app/views/iron/field_definitions/file/_form.html.erb +6 -0
  111. data/app/views/iron/field_definitions/new.html.erb +5 -3
  112. data/app/views/iron/field_definitions/number/_form.html.erb +23 -1
  113. data/app/views/iron/field_definitions/reference/_form.html.erb +5 -0
  114. data/app/views/iron/field_definitions/rich_text_area/_form.html.erb +5 -0
  115. data/app/views/iron/field_definitions/text_area/_form.html.erb +23 -0
  116. data/app/views/iron/field_definitions/text_field/_form.html.erb +20 -1
  117. data/app/views/iron/home/_content_types.html.erb +1 -1
  118. data/app/views/iron/locales/_form.html.erb +5 -3
  119. data/app/views/iron/locales/edit.html.erb +1 -1
  120. data/app/views/iron/locales/index.html.erb +12 -6
  121. data/app/views/iron/shared/_schema_lock_badge.html.erb +19 -0
  122. data/config/locales/en.yml +13 -1
  123. data/config/locales/it.yml +18 -1
  124. data/config/routes.rb +11 -0
  125. data/db/migrate/20260612131538_create_iron_systems.rb +9 -0
  126. data/exe/iron +5 -0
  127. data/lib/generators/iron/agents/agents_generator.rb +52 -0
  128. data/lib/generators/iron/agents/templates/AGENTS.md +24 -0
  129. data/lib/generators/iron/agents/templates/SKILL.md +423 -0
  130. data/lib/generators/iron/install/install_generator.rb +118 -0
  131. data/lib/generators/iron/install/templates/iron_release.rake +5 -0
  132. data/lib/generators/iron/install/templates/schema.json +12 -0
  133. data/lib/generators/iron/install/templates/seeds.rb +13 -0
  134. data/lib/generators/iron/pages/pages_generator.rb +5 -0
  135. data/lib/generators/iron/pages/templates/pages_controller.rb +2 -2
  136. data/lib/generators/iron/pages/templates/show.html.erb +1 -1
  137. data/lib/install/template.rb +9 -0
  138. data/lib/iron/cli.rb +43 -0
  139. data/lib/iron/published_page_constraint.rb +33 -0
  140. data/lib/iron/routing.rb +2 -26
  141. data/lib/iron/version.rb +1 -1
  142. data/lib/tasks/iron_content.rake +82 -0
  143. data/lib/tasks/iron_schema.rake +45 -0
  144. metadata +62 -3
@@ -37,25 +37,26 @@
37
37
  @apply focus-visible:outline-2 focus-visible:outline-sky-600 dark:focus-visible:outline-sky-500;
38
38
  }
39
39
 
40
- /* Toolbar */
40
+ /* Toolbar — a single, non-wrapping row. Lexxy's controller measures how far
41
+ the buttons overflow the row (scrollWidth vs clientWidth) and tucks the
42
+ ones that don't fit into a trailing "more" menu, so the row must never
43
+ wrap. `relative` lets the dropdown panels anchor to the toolbar. */
41
44
  lexxy-toolbar {
42
- @apply flex flex-wrap items-center gap-1 p-2;
45
+ @apply relative flex flex-nowrap items-center gap-1 p-2;
46
+ @apply max-w-full;
43
47
  @apply border-b border-stone-950/10 dark:border-white/10;
44
48
  @apply bg-stone-50 dark:bg-white/5;
45
49
  @apply rounded-t-xl;
46
50
  }
47
51
 
48
- /* Toolbar button groups */
49
- lexxy-toolbar [data-toolbar-group] {
50
- @apply flex items-center;
52
+ /* Items keep their natural size; shrinking them would let everything squeeze
53
+ in and rob the overflow measurement of the signal it needs. */
54
+ lexxy-toolbar > * {
55
+ @apply flex-none;
51
56
  }
52
57
 
53
- lexxy-toolbar [data-toolbar-group]:not(:last-child)::after {
54
- content: "";
55
- @apply mx-2 h-5 w-px bg-stone-300 dark:bg-white/20;
56
- }
57
-
58
- /* Toolbar buttons */
58
+ /* Toolbar buttons — square, icon-only. Buttons that live inside dropdown
59
+ panels are reshaped by the panel rules below. */
59
60
  lexxy-toolbar button,
60
61
  lexxy-toolbar summary {
61
62
  @apply inline-flex items-center justify-center;
@@ -64,12 +65,12 @@
64
65
  @apply hover:bg-stone-200 dark:hover:bg-white/10;
65
66
  @apply focus-visible:outline-2 focus-visible:outline-stone-600;
66
67
  @apply disabled:opacity-40 disabled:pointer-events-none;
68
+ @apply aria-disabled:opacity-40 aria-disabled:pointer-events-none;
67
69
  @apply transition-colors;
68
70
  }
69
71
 
70
72
  lexxy-toolbar button[aria-pressed="true"],
71
- lexxy-toolbar button.active,
72
- lexxy-toolbar details[open] > summary {
73
+ lexxy-toolbar button.active {
73
74
  @apply bg-stone-200 text-stone-900;
74
75
  @apply dark:bg-white/15 dark:text-white;
75
76
  }
@@ -80,6 +81,41 @@
80
81
  fill: currentColor;
81
82
  }
82
83
 
84
+ /* Dropdown triggers wear a chevron, so let them outgrow the square button. */
85
+ lexxy-toolbar .lexxy-editor__toolbar-button--chevron {
86
+ @apply w-auto gap-1 px-1.5;
87
+ }
88
+
89
+ lexxy-toolbar .lexxy-editor__toolbar-button--chevron::after {
90
+ content: "";
91
+ @apply size-1.5 rotate-45;
92
+ @apply border-b-2 border-e-2 border-current;
93
+ @apply opacity-50;
94
+ margin-block-start: -0.15rem;
95
+ }
96
+
97
+ /* Group separators — lexxy flags the last button of each cluster with
98
+ `…group-end`; we hang a hairline rule in the trailing gap. Scope to the
99
+ toolbar's own rows so the same class, reused inside the format menu and the
100
+ overflow menu, doesn't sprout stray dividers. */
101
+ lexxy-toolbar > button.lexxy-editor__toolbar-group-end,
102
+ lexxy-toolbar > lexxy-link-dropdown > button.lexxy-editor__toolbar-group-end {
103
+ @apply relative me-2.5;
104
+ }
105
+
106
+ lexxy-toolbar > button.lexxy-editor__toolbar-group-end::after,
107
+ lexxy-toolbar > lexxy-link-dropdown > button.lexxy-editor__toolbar-group-end::after {
108
+ content: "";
109
+ @apply absolute inset-y-1 w-px;
110
+ inset-inline-end: -0.3125rem;
111
+ @apply bg-stone-300 dark:bg-white/20 pointer-events-none;
112
+ }
113
+
114
+ /* History controls and the overflow menu sit at the trailing edge. */
115
+ lexxy-toolbar .lexxy-editor__toolbar-button--push-right {
116
+ margin-inline-start: auto;
117
+ }
118
+
83
119
  /* Editor content area — only the main editable surface, not decorator children */
84
120
  lexxy-editor [contenteditable="true"] {
85
121
  @apply block w-full min-h-48 p-4;
@@ -94,72 +130,38 @@
94
130
  @apply pointer-events-none;
95
131
  }
96
132
 
97
- /* Toolbar dropdowns (shared container styles) */
133
+ /* ---- Dropdowns -------------------------------------------------------- */
134
+
135
+ /* Each dropdown anchors its panel beneath its own trigger. Compact mode (the
136
+ [overflowing] rules at the end) relaxes this so panels span the toolbar. */
98
137
  .lexxy-editor__toolbar-dropdown {
99
138
  @apply relative select-none;
100
139
  }
101
140
 
102
- .lexxy-editor__toolbar-dropdown > summary {
103
- list-style: none;
104
- @apply cursor-pointer;
105
- }
106
-
107
- .lexxy-editor__toolbar-dropdown > summary::-webkit-details-marker {
108
- display: none;
109
- }
110
-
111
- .lexxy-editor__toolbar-dropdown > summary::marker {
112
- display: none;
113
- content: "";
114
- }
115
-
116
- .lexxy-editor__toolbar-dropdown-content {
117
- --dropdown-padding: --spacing(3);
118
- --dropdown-gap: --spacing(1.5);
119
-
120
- @apply absolute z-10;
121
- @apply flex gap-(--dropdown-gap) p-(--dropdown-padding);
141
+ /* Shared panel chrome. Panels reveal/hide through the `hidden` attribute, so
142
+ each panel below sets its own display + padding once it is shown. */
143
+ lexxy-toolbar [data-dropdown-panel] {
144
+ @apply absolute z-20 top-full start-0 mt-1;
122
145
  @apply bg-white dark:bg-stone-800;
123
146
  @apply border-2 border-sky-200 dark:border-sky-500/30;
124
- @apply rounded-md;
147
+ @apply rounded-md rounded-ss-none shadow-lg;
125
148
  @apply text-stone-900 dark:text-white;
126
- @apply top-8 start-0;
127
- @apply max-w-[40ch];
128
- @apply rounded-ss-none;
129
149
  }
130
150
 
131
- /* Active state for the dropdown's *trigger* (the <summary> itself, which carries
132
- the toolbar-button class). Scope to direct summary so it doesn't bleed into
133
- content buttons like highlight swatches that share the same class. */
134
- .lexxy-editor__toolbar-dropdown:is([open]) > summary.lexxy-editor__toolbar-button {
135
- @apply bg-sky-100 dark:bg-sky-500/20;
136
- @apply rounded-ee-none rounded-es-none;
151
+ lexxy-toolbar [data-dropdown-panel][hidden] {
152
+ @apply hidden;
137
153
  }
138
154
 
139
- .lexxy-editor__toolbar-dropdown:is([open]) > summary.lexxy-editor__toolbar-button:hover {
155
+ /* Open trigger — tint the button and square off the edge meeting the panel. */
156
+ .lexxy-editor__toolbar-dropdown > [data-dropdown-trigger][aria-expanded="true"],
157
+ .lexxy-editor__toolbar-dropdown > [data-dropdown-trigger][aria-expanded="true"]:hover {
140
158
  @apply bg-sky-100 dark:bg-sky-500/20;
159
+ @apply rounded-ee-none rounded-es-none;
141
160
  }
142
161
 
143
- /* Responsive dropdown in overflow menu */
144
- [overflowing] .lexxy-editor__toolbar-dropdown {
145
- @apply static;
146
- }
147
-
148
- [overflowing] .lexxy-editor__toolbar-dropdown-content {
149
- --dropdown-padding: --spacing(2);
150
- @apply end-(--dropdown-padding) start-(--dropdown-padding);
151
- }
152
-
153
- /* Vertical dropdown menu (used by the text-formatting dropdown) — items show
154
- an icon followed by a text label; needs to override toolbar's h-8 w-8 rule. */
155
- .lexxy-editor__toolbar-dropdown-list {
156
- @apply absolute z-10;
157
- @apply flex flex-col p-1;
158
- @apply bg-white dark:bg-stone-800;
159
- @apply border-2 border-sky-200 dark:border-sky-500/30;
160
- @apply rounded-md rounded-ss-none;
161
- @apply text-stone-900 dark:text-white;
162
- @apply top-8 start-0 min-w-[12rem];
162
+ /* Text-formatting menu a vertical list of icon + label rows. */
163
+ [data-dropdown-panel].lexxy-editor__toolbar-dropdown-list {
164
+ @apply flex flex-col gap-0.5 p-1 min-w-[12rem];
163
165
  }
164
166
 
165
167
  .lexxy-editor__toolbar-dropdown-list button {
@@ -180,25 +182,24 @@
180
182
  fill: currentColor;
181
183
  }
182
184
 
183
- /* Lexxy emits explicit <div class="lexxy-editor__toolbar-separator"> elements
184
- between groups inside the dropdown — relying on those means no extra
185
- border-b for `group-end` (which would double up the divider). */
186
185
  .lexxy-editor__toolbar-dropdown-list .lexxy-editor__toolbar-separator {
187
186
  @apply my-1 h-px w-full;
188
187
  @apply bg-stone-200 dark:bg-white/10;
189
188
  }
190
189
 
191
- /* Link dropdownURL input with Link / Unlink action buttons */
192
- lexxy-link-dropdown {
193
- @apply gap-2 min-w-[22rem];
190
+ /* Overflow ("more") menu a compact grid of the buttons that didn't fit,
191
+ anchored to the trailing edge under its trigger. */
192
+ .lexxy-editor__toolbar-overflow [data-dropdown-panel] {
193
+ @apply start-auto end-0 rounded-ss-md rounded-se-none;
194
194
  }
195
195
 
196
- [overflowing] lexxy-link-dropdown {
197
- @apply min-w-0 flex-col;
196
+ [data-dropdown-panel].lexxy-editor__toolbar-overflow-menu {
197
+ @apply grid grid-cols-5 gap-1 p-2 justify-items-center;
198
198
  }
199
199
 
200
- [overflowing] lexxy-link-dropdown .lexxy-editor__toolbar-dropdown-actions {
201
- @apply mt-2;
200
+ /* Link dropdown — URL input with Link / Unlink action buttons. */
201
+ lexxy-link-dropdown [data-dropdown-panel] {
202
+ @apply flex items-center gap-2 p-2 min-w-[22rem];
202
203
  }
203
204
 
204
205
  lexxy-link-dropdown input[type="url"] {
@@ -236,15 +237,15 @@
236
237
  }
237
238
 
238
239
  /* Highlight/Color dropdown
239
- Lexxy 0.9.9 emits 9 text-color buttons followed by 9 bg-color buttons.
240
- With `grid-auto-flow: column` and 2 rows, source order fills col-by-col so
241
- each column shows a matching color/bg pair (row 1 = color, row 2 = bg). */
242
- lexxy-highlight-dropdown {
243
- --button-size: --spacing(8);
244
- @apply flex flex-col gap-2;
240
+ Lexxy emits N text-color buttons followed by N background-color buttons.
241
+ A 2-row grid with `grid-auto-flow: column` fills column-by-column, so each
242
+ column pairs a color with its background (row 1 = color, row 2 = bg). */
243
+ lexxy-highlight-dropdown [data-dropdown-panel] {
244
+ @apply flex flex-col gap-2 p-2;
245
245
  }
246
246
 
247
247
  lexxy-highlight-dropdown .lexxy-highlight-colors {
248
+ --button-size: --spacing(7);
248
249
  display: grid;
249
250
  grid-template-columns: repeat(var(--max-colors, 9), var(--button-size));
250
251
  grid-template-rows: repeat(2, var(--button-size));
@@ -253,7 +254,9 @@
253
254
  }
254
255
 
255
256
  lexxy-highlight-dropdown .lexxy-highlight-button {
256
- @apply relative aspect-square size-8 rounded-md p-0;
257
+ @apply relative rounded-md p-0;
258
+ width: var(--button-size);
259
+ height: var(--button-size);
257
260
  @apply ring-1 ring-inset ring-stone-300 dark:ring-white/15;
258
261
  @apply hover:opacity-80;
259
262
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -262,7 +265,7 @@
262
265
  lexxy-highlight-dropdown .lexxy-highlight-button::after {
263
266
  content: "Aa";
264
267
  @apply absolute inset-0 flex items-center justify-center;
265
- @apply font-semibold text-sm leading-none;
268
+ @apply font-semibold text-xs leading-none;
266
269
  }
267
270
 
268
271
  /* Background-color button: letter in default ink color over the swatch */
@@ -281,7 +284,7 @@
281
284
  }
282
285
 
283
286
  lexxy-highlight-dropdown .lexxy-editor__toolbar-dropdown-reset {
284
- @apply mt-2 w-full whitespace-nowrap;
287
+ @apply mt-1 w-full whitespace-nowrap;
285
288
  @apply button-secondary button-sm;
286
289
  }
287
290
 
@@ -289,9 +292,30 @@
289
292
  @apply hidden;
290
293
  }
291
294
 
292
- /* Responsive highlight dropdown overflow menu is narrower */
293
- [overflowing] lexxy-highlight-dropdown {
294
- @apply w-fit;
295
+ /* ---- Compact (overflowing) toolbar ------------------------------------ */
296
+
297
+ /* When the row can't hold every control, lexxy sets `overflowing` on the
298
+ toolbar and moves the spill into the "more" menu. Panels then detach from
299
+ their trigger and drop below the whole toolbar, spanning its width so
300
+ nothing runs off a narrow screen. */
301
+ lexxy-toolbar[overflowing] .lexxy-editor__toolbar-dropdown {
302
+ position: static;
303
+ }
304
+
305
+ lexxy-toolbar[overflowing] [data-dropdown-panel] {
306
+ @apply start-2 end-2 w-auto rounded-md;
307
+ }
308
+
309
+ lexxy-toolbar[overflowing] [data-dropdown-trigger][aria-expanded="true"] {
310
+ @apply rounded-md;
311
+ }
312
+
313
+ lexxy-toolbar[overflowing] lexxy-link-dropdown [data-dropdown-panel] {
314
+ @apply min-w-0 flex-col items-stretch;
315
+ }
316
+
317
+ lexxy-toolbar[overflowing] lexxy-link-dropdown .lexxy-editor__toolbar-dropdown-actions {
318
+ @apply mt-2;
295
319
  }
296
320
 
297
321
  /* Attachment upload progress */
@@ -0,0 +1,19 @@
1
+ module Iron
2
+ module SchemaEditing
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ def ensure_schema_editable
8
+ return if Schema.editable?
9
+
10
+ if request.format.json?
11
+ render json: { schema: [ I18n.t("iron.schema.locked.api_error") ] }, status: :forbidden
12
+ elsif request.format.turbo_stream?
13
+ render turbo_stream: turbo_stream.refresh(request_id: nil)
14
+ else
15
+ redirect_back fallback_location: root_path, alert: I18n.t("iron.schema.locked.alert"), status: :see_other
16
+ end
17
+ end
18
+ end
19
+ end
@@ -12,7 +12,9 @@ module Iron
12
12
  end
13
13
 
14
14
  def set_entry
15
- @entry = @content_type.entries.find_by!(route: params[:route])
15
+ @entry = @content_type.entries.find_by(route: params[:route])
16
+
17
+ raise ActiveRecord::RecordNotFound unless @entry&.published_in?(Current.locale)
16
18
  end
17
19
  end
18
20
  end
@@ -0,0 +1,25 @@
1
+ module Iron
2
+ module Api
3
+ module Schema
4
+ class BaseController < Api::BaseController
5
+ include Iron::SchemaEditing
6
+
7
+ before_action :ensure_can_administer, only: %i[ create update destroy ]
8
+ before_action :ensure_schema_editable, only: %i[ create update destroy ]
9
+
10
+ rescue_from ActiveRecord::RecordNotUnique, with: :render_duplicate_handle
11
+ rescue_from ActiveModel::UnknownAttributeError, with: :render_unknown_attribute
12
+
13
+ private
14
+
15
+ def render_duplicate_handle
16
+ render json: { handle: [ "has already been taken" ] }, status: :unprocessable_entity
17
+ end
18
+
19
+ def render_unknown_attribute(exception)
20
+ render json: { exception.attribute => [ "is not supported by this field type" ] }, status: :unprocessable_entity
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ module Iron
2
+ module Api
3
+ module Schema
4
+ class BlockDefinitionsController < BaseController
5
+ before_action :set_block_definition, only: %i[ show update destroy ]
6
+
7
+ def index
8
+ @block_definitions = BlockDefinition.order(:name)
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def create
15
+ @block_definition = BlockDefinition.new(block_definition_params)
16
+
17
+ if @block_definition.save
18
+ render :show, status: :created, location: api_schema_block_definition_url(@block_definition)
19
+ else
20
+ render json: @block_definition.errors, status: :unprocessable_entity
21
+ end
22
+ end
23
+
24
+ def update
25
+ if @block_definition.update(block_definition_params)
26
+ render :show
27
+ else
28
+ render json: @block_definition.errors, status: :unprocessable_entity
29
+ end
30
+ end
31
+
32
+ def destroy
33
+ @block_definition.destroy!
34
+ head :no_content
35
+ end
36
+
37
+ private
38
+
39
+ def set_block_definition
40
+ @block_definition = BlockDefinition.find(params[:id])
41
+ end
42
+
43
+ def block_definition_params
44
+ params.permit(:name, :handle, :description)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,64 @@
1
+ module Iron
2
+ module Api
3
+ module Schema
4
+ class ContentTypesController < BaseController
5
+ before_action :set_content_type, only: %i[ show update destroy ]
6
+ before_action :ensure_supported_type, only: :create
7
+
8
+ def index
9
+ @content_types = ContentType.order(:name)
10
+ end
11
+
12
+ def show
13
+ end
14
+
15
+ def create
16
+ @content_type = ContentType.new(content_type_params.merge(type: classified_type))
17
+
18
+ if @content_type.save
19
+ render :show, status: :created, location: api_schema_content_type_url(@content_type)
20
+ else
21
+ render json: @content_type.errors, status: :unprocessable_entity
22
+ end
23
+ end
24
+
25
+ def update
26
+ if @content_type.update(content_type_params)
27
+ render :show
28
+ else
29
+ render json: @content_type.errors, status: :unprocessable_entity
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ @content_type.destroy!
35
+ head :no_content
36
+ end
37
+
38
+ private
39
+
40
+ def set_content_type
41
+ @content_type = ContentType.find(params[:id])
42
+ end
43
+
44
+ def content_type_params
45
+ params.permit(:name, :handle, :description, :icon, :base_path, :web_publishing_enabled, :title_field_definition_id, :web_page_title_field_definition_id)
46
+ end
47
+
48
+ def ensure_supported_type
49
+ if params[:type].present? && requested_type.nil?
50
+ render json: { type: [ "must be one of: #{ContentType::TYPES.map(&:downcase).join(", ")}" ] }, status: :unprocessable_entity
51
+ end
52
+ end
53
+
54
+ def requested_type
55
+ ContentType::TYPES.find { |type| type.casecmp?(params[:type].to_s) }
56
+ end
57
+
58
+ def classified_type
59
+ ContentType.classify_type(requested_type || "Collection")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,88 @@
1
+ module Iron
2
+ module Api
3
+ module Schema
4
+ class FieldDefinitionsController < BaseController
5
+ before_action :set_schemable, :ensure_supported_type, only: :create
6
+ before_action :set_field_definition, only: %i[ show update destroy ]
7
+ before_action :ensure_valid_position, only: %i[ create update ]
8
+
9
+ def show
10
+ end
11
+
12
+ def create
13
+ @field_definition = @schemable.field_definitions.build(type: classified_type)
14
+ @field_definition.assign_attributes(field_definition_params)
15
+ place_at_requested_position
16
+
17
+ if @field_definition.save
18
+ render :show, status: :created, location: api_schema_field_definition_url(@field_definition)
19
+ else
20
+ render json: @field_definition.errors, status: :unprocessable_entity
21
+ end
22
+ end
23
+
24
+ def update
25
+ @field_definition.assign_attributes(field_definition_params)
26
+ place_at_requested_position
27
+
28
+ if @field_definition.save
29
+ render :show
30
+ else
31
+ render json: @field_definition.errors, status: :unprocessable_entity
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ @field_definition.destroy!
37
+ head :no_content
38
+ end
39
+
40
+ private
41
+
42
+ def set_schemable
43
+ @schemable = if params[:content_type_id]
44
+ ContentType.find(params[:content_type_id])
45
+ else
46
+ BlockDefinition.find(params[:block_definition_id])
47
+ end
48
+ end
49
+
50
+ def set_field_definition
51
+ @field_definition = FieldDefinition.find(params[:id])
52
+ end
53
+
54
+ def field_definition_params
55
+ params.permit(:name, :handle, :required, :searchable, :file_scope, :min_length, :max_length, :min, :max, allowed_values: [], selected_presets: [], supported_block_definition_ids: [], supported_content_type_ids: [])
56
+ end
57
+
58
+ def ensure_valid_position
59
+ Integer(params[:position].to_s) if params.key?(:position)
60
+ rescue ArgumentError, TypeError
61
+ render json: { position: [ "must be an integer" ] }, status: :unprocessable_entity
62
+ end
63
+
64
+ def place_at_requested_position
65
+ @field_definition.move_to(requested_position) if params.key?(:position)
66
+ end
67
+
68
+ def requested_position
69
+ Integer(params[:position].to_s).clamp(0, highest_position)
70
+ end
71
+
72
+ def highest_position
73
+ @field_definition.schemable.field_definitions.where.not(id: @field_definition.id).count
74
+ end
75
+
76
+ def ensure_supported_type
77
+ unless FieldDefinition::TYPES.include?(params[:type].to_s)
78
+ render json: { type: [ "must be one of: #{FieldDefinition::TYPES.join(", ")}" ] }, status: :unprocessable_entity
79
+ end
80
+ end
81
+
82
+ def classified_type
83
+ FieldDefinition.classify_type(params[:type])
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,52 @@
1
+ module Iron
2
+ module Api
3
+ module Schema
4
+ class LocalesController < BaseController
5
+ before_action :set_locale, only: %i[ show update destroy ]
6
+
7
+ def index
8
+ @locales = Locale.order(:code)
9
+ end
10
+
11
+ def show
12
+ end
13
+
14
+ def create
15
+ @locale = Locale.new(locale_params)
16
+
17
+ if @locale.save
18
+ render :show, status: :created, location: api_schema_locale_url(@locale)
19
+ else
20
+ render json: @locale.errors, status: :unprocessable_entity
21
+ end
22
+ end
23
+
24
+ def update
25
+ if @locale.update(locale_params)
26
+ render :show
27
+ else
28
+ render json: @locale.errors, status: :unprocessable_entity
29
+ end
30
+ end
31
+
32
+ def destroy
33
+ if @locale.destroy
34
+ head :no_content
35
+ else
36
+ render json: @locale.errors, status: :unprocessable_entity
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def set_locale
43
+ @locale = Locale.find(params[:id])
44
+ end
45
+
46
+ def locale_params
47
+ params.permit(:code, :name)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -1,7 +1,7 @@
1
1
  module Iron
2
2
  class ApplicationController < ActionController::Base
3
3
  include ActiveStorage::SetCurrent
4
- include AdminLocale, Authentication, Authorization
4
+ include AdminLocale, Authentication, Authorization, SchemaEditing
5
5
  default_form_builder FormBuilder
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@ module Iron
2
2
  class BlockDefinitionsController < ApplicationController
3
3
  before_action :set_block_definition, only: %i[ show edit update destroy ]
4
4
  before_action :ensure_can_administer
5
+ before_action :ensure_schema_editable, only: %i[ create update destroy ]
5
6
 
6
7
  def index
7
8
  @block_definitions = BlockDefinition.all
@@ -2,6 +2,7 @@ module Iron
2
2
  class ContentTypesController < ApplicationController
3
3
  before_action :set_content_type, only: %i[ show edit update destroy ]
4
4
  before_action :ensure_can_administer
5
+ before_action :ensure_schema_editable, only: %i[ create update destroy ]
5
6
 
6
7
  def index
7
8
  @content_types = ContentType.order(:name)
@@ -5,6 +5,7 @@ module Iron
5
5
  before_action :set_schemable, only: %i[ new create ]
6
6
  before_action :set_field_definition, only: %i[ edit update destroy ]
7
7
  before_action :ensure_can_administer
8
+ before_action :ensure_schema_editable, only: %i[ create update destroy ]
8
9
 
9
10
  def new
10
11
  @field_definition = @schemable.field_definitions.build(type: selected_field_definition_type)
@@ -42,7 +43,7 @@ module Iron
42
43
  end
43
44
 
44
45
  def field_definition_params
45
- params.expect(field_definition: [ :type, :name, :handle, :rank, :allowed_values_text, :required, :searchable, :file_scope, selected_presets: [], supported_block_definition_ids: [], supported_content_type_ids: [] ])
46
+ params.expect(field_definition: [ :type, :name, :handle, :rank, :allowed_values_text, :required, :min_length, :max_length, :min, :max, :searchable, :file_scope, selected_presets: [], supported_block_definition_ids: [], supported_content_type_ids: [] ])
46
47
  end
47
48
 
48
49
  def selected_field_definition_type
@@ -19,7 +19,7 @@ module Iron
19
19
 
20
20
  private
21
21
  def prevent_running_after_setup
22
- redirect_to root_url if User.any?
22
+ redirect_to root_url if User.people.any?
23
23
  end
24
24
 
25
25
  def user_params
@@ -2,6 +2,7 @@ module Iron
2
2
  class LocalesController < ApplicationController
3
3
  before_action :set_locale, only: %i[ edit update destroy ]
4
4
  before_action :ensure_can_administer
5
+ before_action :ensure_schema_editable, only: %i[ create update destroy ]
5
6
 
6
7
  def index
7
8
  @locales = Locale.all