robin_cms 0.1.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobinCMS
4
+ # The data access layer for Items.
5
+ #
6
+ # This class implements a Data Mapper pattern for persisting items to disk.
7
+ # It works well for this domain because there are two ways that items are
8
+ # written to disk, depending on whether they are a Data or Collection item.
9
+ # Since the Data Mapper is independent of the in-memory representation of
10
+ # Items, Items don't need to know about which 'type' they are.
4
11
  class Library
5
12
  include Editable
6
13
  include Sluggable
@@ -22,11 +29,13 @@ module RobinCMS
22
29
  extend CollectionLibrary
23
30
  when "data"
24
31
  extend DataLibrary
32
+ when "singular"
33
+ extend SingularLibrary
25
34
  end
26
35
  end
27
36
 
28
37
  def blank
29
- Item.new(nil, **@schema)
38
+ Item.new(nil, {}, **@schema)
30
39
  end
31
40
 
32
41
  def create_static(filename, tempfile)
@@ -79,5 +88,21 @@ module RobinCMS
79
88
  def kind
80
89
  @schema[:id]
81
90
  end
91
+
92
+ def type
93
+ @schema[:type]
94
+ end
95
+
96
+ def collection?
97
+ type == "collection"
98
+ end
99
+
100
+ def data?
101
+ type == "data"
102
+ end
103
+
104
+ def singular?
105
+ type == "singular"
106
+ end
82
107
  end
83
108
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RobinCMS
4
+ module SingularLibrary
5
+ # Since there is only one of these, give it the special id 0.
6
+ ID = 0
7
+
8
+ def self.extended(target)
9
+ # Since singular files can't be created or delete by users, create an
10
+ # empty one automatically if it doesn't already exist.
11
+
12
+ filename = target.schema[:id]
13
+ filetype = target.schema[:filetype]
14
+ filepath = File.join(target.schema[:location], "#{filename}.#{filetype}")
15
+
16
+ return if File.exist?(filepath)
17
+
18
+ FileUtils.mkdir_p(File.dirname(filepath))
19
+ item = Item.new(nil, {title: target.schema[:label]}, **target.schema)
20
+ item.update_timestamps!
21
+ serialized = YAML.dump(item.frontmatter, stringify_names: true)
22
+ File.write(filepath, serialized)
23
+ end
24
+
25
+ def create(_attributes = nil)
26
+ raise InvalidOperationError, "Invalid operation for Singular item"
27
+ end
28
+
29
+ def find_one(_id = nil)
30
+ # Ignore the id parameter. For singular libraries, there is only a single
31
+ # item. The id parameter is left for API compatibility with the other
32
+ # library types.
33
+
34
+ item = YAML.load_file(filepath, symbolize_names: true, permitted_classes: [Date])
35
+
36
+ return nil unless item
37
+
38
+ Item.new(ID, item, **@schema)
39
+ end
40
+
41
+ def all
42
+ # Returns an array of the only item. For API compatibility with the other
43
+ # library types.
44
+ [find_one].compact
45
+ end
46
+
47
+ def write(item)
48
+ FileUtils.mkdir_p(File.dirname(filepath))
49
+ FileUtils.touch(filepath)
50
+
51
+ item.update_timestamps!
52
+ serialized = YAML.dump(item.frontmatter, stringify_names: true)
53
+ File.write(filepath, serialized)
54
+
55
+ filepath
56
+ end
57
+
58
+ def delete(_id = nil)
59
+ raise InvalidOperationError, "Invalid operation for Singular item"
60
+ end
61
+
62
+ private
63
+
64
+ def filepath
65
+ File.join(@schema[:location], "#{@schema[:id]}.#{@schema[:filetype]}")
66
+ end
67
+ end
68
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RobinCMS
4
- VERSION = "0.1.7"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -16,7 +16,7 @@
16
16
  </div>
17
17
  <%= erb :flash %>
18
18
  <footer class="controls --align-right">
19
- <a href="<%= root_url %>">Back</a>
19
+ <a href="<%= home_url %>">Back</a>
20
20
  <button type="submit">Update</button>
21
21
  </footer>
22
22
  </form>
@@ -9,7 +9,6 @@
9
9
  initial values. This one probably needs to be fixed.
10
10
  %>
11
11
  <img
12
- id="image-preview"
13
12
  class="image-preview"
14
13
  src="<%= item_image_url(@item) %>"
15
14
  alt="<%= @item.image_alt %>"
@@ -19,7 +19,7 @@
19
19
  <% if session[:auth_user] %>
20
20
  <header id="site-header">
21
21
  <span class="controls">
22
- <h1><a href="<%= root_url %>"><%= @config[:title] %> admin</a></h1>
22
+ <h1><a href="<%= home_url %>"><%= @config[:title] %> admin</a></h1>
23
23
  </span>
24
24
  <span class="controls --gap-md">
25
25
  <a href="<%= @config[:url] %>" target="_blank">
@@ -35,7 +35,13 @@
35
35
  </span>
36
36
  </header>
37
37
  <% end %>
38
+
38
39
  <main id="site-content">
40
+ <% if session[:auth_user] %>
41
+ <header class="card --clear">
42
+ <%= erb :nav %>
43
+ </header>
44
+ <% end %>
39
45
  <%= yield %>
40
46
  <footer id="site-footer" class="card --clear">
41
47
  <%= erb :"logo.svg" %>
@@ -46,6 +52,7 @@
46
52
  </p>
47
53
  </footer>
48
54
  </main>
55
+
49
56
  <dialog id="successdialog" class="card">
50
57
  <h2>Success!</h2>
51
58
  <p><%= flash[:success_dialog] %></p>
@@ -55,6 +62,7 @@
55
62
  </form>
56
63
  </div>
57
64
  </dialog>
65
+
58
66
  <dialog id="errordialog" class="card">
59
67
  <h2>Oops, something went wrong.</h2>
60
68
  <p><%= flash[:error_dialog] %></p>
@@ -65,6 +73,7 @@
65
73
  </div>
66
74
  </dialog>
67
75
  </body>
76
+
68
77
  <script>
69
78
  const image = document.getElementById("__field_image")
70
79
  if (image) {
@@ -1,6 +1,3 @@
1
- <header class="card --clear">
2
- <%= erb :nav %>
3
- </header>
4
1
  <div class="card">
5
2
  <% if @library[:description] %>
6
3
  <p><%= @library[:description] %></p>
@@ -13,7 +10,9 @@
13
10
  </form>
14
11
  <% end %>
15
12
  </div>
13
+
16
14
  <%= erb :filter_form %>
15
+
17
16
  <% if @items.empty? %>
18
17
  <div class="no-items">
19
18
  Nothing to show.
@@ -55,12 +54,12 @@
55
54
  <% end %>
56
55
  <td class="date">
57
56
  <a href="<%= library_item_url(@library, item) %>">
58
- <%= item.created_at.strftime("%b %d, %Y") %>
57
+ <%= item.created_at&.strftime("%b %d, %Y") %>
59
58
  </a>
60
59
  </td>
61
60
  <td class="date">
62
61
  <a href="<%= library_item_url(@library, item) %>">
63
- <%= item.updated_at.strftime("%b %d, %Y") %>
62
+ <%= item.updated_at&.strftime("%b %d, %Y") %>
64
63
  </a>
65
64
  </td>
66
65
  </tr>
@@ -1,5 +1,7 @@
1
1
  <span class="controls">
2
- <a href="<%= library_url(@library) %>">Back</a>
2
+ <% unless @library.singular? %>
3
+ <a href="<%= library_url(@library) %>">Back</a>
4
+ <% end %>
3
5
  <button form="<%= safe_id(@item.id, 'edit_item') %>" type="submit">Save</button>
4
6
  <%= erb :delete_dialog if has_delete %>
5
7
  </span>
@@ -4,6 +4,7 @@
4
4
  </h2>
5
5
  <%= erb :library_actions, locals: { has_delete: @item.can_delete? } %>
6
6
  </header>
7
+
7
8
  <form
8
9
  id="<%= safe_id(@item.id, 'edit_item') %>"
9
10
  class="card"
@@ -19,7 +20,7 @@
19
20
  <div class="field">
20
21
  <%=
21
22
  case field[:type]
22
- when "text", "date", "number", "color", "email", "url"
23
+ when "text", "date", "number", "color", "email", "url", "array"
23
24
  erb :input_field, locals: { field: field }
24
25
  when "hidden"
25
26
  erb :hidden_field, locals: { field: field }
@@ -28,7 +29,7 @@
28
29
  when "image"
29
30
  erb :image_field, locals: { field: field }
30
31
  when "richtext"
31
- erb :richtext_field, locals: { field: field }
32
+ erb :richtext_field, locals: { field: field, library: @library }
32
33
  else
33
34
  ""
34
35
  end
@@ -1,4 +1,3 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
1
  <svg fill="#000000" width="12px" height="12px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
3
2
  <path fill-rule="evenodd" d="M5,2 L7,2 C7.55228475,2 8,2.44771525 8,3 C8,3.51283584 7.61395981,3.93550716 7.11662113,3.99327227 L7,4 L5,4 C4.48716416,4 4.06449284,4.38604019 4.00672773,4.88337887 L4,5 L4,19 C4,19.5128358 4.38604019,19.9355072 4.88337887,19.9932723 L5,20 L19,20 C19.5128358,20 19.9355072,19.6139598 19.9932723,19.1166211 L20,19 L20,17 C20,16.4477153 20.4477153,16 21,16 C21.5128358,16 21.9355072,16.3860402 21.9932723,16.8833789 L22,17 L22,19 C22,20.5976809 20.75108,21.9036609 19.1762728,21.9949073 L19,22 L5,22 C3.40231912,22 2.09633912,20.75108 2.00509269,19.1762728 L2,19 L2,5 C2,3.40231912 3.24891996,2.09633912 4.82372721,2.00509269 L5,2 L7,2 L5,2 Z M21,2 L21.081,2.003 L21.2007258,2.02024007 L21.2007258,2.02024007 L21.3121425,2.04973809 L21.3121425,2.04973809 L21.4232215,2.09367336 L21.5207088,2.14599545 L21.5207088,2.14599545 L21.6167501,2.21278596 L21.7071068,2.29289322 L21.7071068,2.29289322 L21.8036654,2.40469339 L21.8036654,2.40469339 L21.8753288,2.5159379 L21.9063462,2.57690085 L21.9063462,2.57690085 L21.9401141,2.65834962 L21.9401141,2.65834962 L21.9641549,2.73400703 L21.9641549,2.73400703 L21.9930928,2.8819045 L21.9930928,2.8819045 L22,3 L22,3 L22,9 C22,9.55228475 21.5522847,10 21,10 C20.4477153,10 20,9.55228475 20,9 L20,5.414 L13.7071068,11.7071068 C13.3466228,12.0675907 12.7793918,12.0953203 12.3871006,11.7902954 L12.2928932,11.7071068 C11.9324093,11.3466228 11.9046797,10.7793918 12.2097046,10.3871006 L12.2928932,10.2928932 L18.584,4 L15,4 C14.4477153,4 14,3.55228475 14,3 C14,2.44771525 14.4477153,2 15,2 L21,2 Z"/>
4
3
  </svg>
@@ -4,7 +4,7 @@
4
4
  <section class="card">
5
5
  <p>You are currently logged in as <b><%= @username %></b>.</p>
6
6
  <div class="controls --align-right">
7
- <a href="<%= root_url %>">Back</a>
7
+ <a href="<%= home_url %>">Back</a>
8
8
  <form action="<%= change_password_url %>">
9
9
  <button type="submit">Change password</button>
10
10
  </form>
@@ -1,10 +1,96 @@
1
1
  <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
2
+
3
+ <style>
4
+ <%= erb :"trix.css" %>
5
+ </style>
6
+
2
7
  <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
8
+ <script>
9
+ addEventListener("trix-before-initialize", function () {
10
+ Trix.config.attachments.preview.caption = false;
11
+ Trix.config.attachments.file.caption = false;
12
+ });
13
+
14
+ addEventListener("trix-attachment-add", function (e) {
15
+ if (e.attachment.file) {
16
+ uploadFileAttachment(e.attachment);
17
+ }
18
+ });
19
+
20
+ addEventListener("trix-attachment-remove", function (e) {
21
+ if (e.attachment.file) {
22
+ removeFile(e.attachment);
23
+ }
24
+ });
25
+
26
+ function uploadFileAttachment(attachment) {
27
+ uploadFile(attachment.file, setProgress, setAttributes);
28
+
29
+ function setProgress(progress) {
30
+ attachment.setUploadProgress(progress);
31
+ }
32
+
33
+ function setAttributes(attributes) {
34
+ attachment.setAttributes(attributes);
35
+ }
36
+ }
37
+
38
+ function uploadFile(file, progressCallback, successCallback) {
39
+ var formData = new FormData();
40
+ formData.append("key", createStorageKey(file));
41
+ formData.append("Content-Type", file.type);
42
+ formData.append("file", file);
43
+
44
+ var xhr = new XMLHttpRequest();
45
+
46
+ xhr.open("POST", "<%= library_attachment_url(library) %>", true);
47
+
48
+ xhr.upload.addEventListener("progress", function (e) {
49
+ var progress = e.loaded / e.total * 100;
50
+ progressCallback(progress);
51
+ });
52
+
53
+ xhr.addEventListener("load", function (e) {
54
+ if (xhr.status == 200) {
55
+ var res = JSON.parse(xhr.response);
56
+ var attributes = {
57
+ url: res.url,
58
+ href: res.url + "?content-disposition=attachment"
59
+ };
60
+ successCallback(attributes);
61
+ }
62
+ });
63
+
64
+ xhr.send(formData);
65
+ }
66
+
67
+ function removeFile(attachment) {
68
+ var url = attachment.getAttribute("url");
69
+ var parts = url.split("/");
70
+ var key = parts[parts.length - 1];
71
+
72
+ var formData = new FormData();
73
+ formData.append("key", key);
74
+
75
+ var xhr = new XMLHttpRequest();
76
+
77
+ xhr.open("POST", "<%= delete_library_attachment_url(library) %>", true);
78
+ xhr.send(formData);
79
+ }
80
+
81
+ function createStorageKey(file) {
82
+ var date = new Date();
83
+ var day = date.toISOString().slice(0, 10);
84
+ return date.getTime() + "-" + file.name;
85
+ }
86
+ </script>
87
+
3
88
  <%= erb :field_label, locals: { field: field } %>
89
+ <%# TODO: label for attribute doesn't reference correct id %>
4
90
  <input
5
91
  id="richtext-content"
6
92
  type="hidden"
7
93
  name="<%= field[:id] %>"
8
- value="<%= @item.field_value_or_default(field[:id]) %>"
94
+ value="<%= escape_for_trix(@item.field_value_or_default(field[:id])) %>"
9
95
  >
10
- <trix-editor input="richtext-content"></trix-editor>
96
+ <trix-editor class="trix-content" input="richtext-content"></trix-editor>
@@ -7,7 +7,7 @@
7
7
  <% for option in field[:options] %>
8
8
  <option
9
9
  value="<%= option[:value] %>"
10
- <% if option[:value].to_s == @item.published?.to_s %>
10
+ <% if option[:value].to_s == @item.field_value_or_default(field[:id]) %>
11
11
  selected
12
12
  <% end %>
13
13
  >
@@ -194,7 +194,7 @@ select {
194
194
  input,
195
195
  select,
196
196
  trix-toolbar,
197
- img#image-preview {
197
+ img.image-preview {
198
198
  display: block;
199
199
  margin-top: var(--padding-xs);
200
200
  }
@@ -425,7 +425,6 @@ hr {
425
425
  /* trix editor overrides */
426
426
 
427
427
  trix-editor {
428
- height: 16rem;
429
428
  border: 1px solid var(--border-color) !important;
430
429
  border-radius: var(--border-radius) !important;
431
430
  box-shadow: var(--box-shadow-input);
@@ -446,3 +445,4 @@ trix-editor {
446
445
  .trix-button.trix-active {
447
446
  background: var(--accent-color-light) !important;
448
447
  }
448
+
@@ -0,0 +1,75 @@
1
+ .trix-content {
2
+ line-height: 1.5; }
3
+ .trix-content * {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0; }
7
+ .trix-content h1 {
8
+ font-size: 1.2em;
9
+ line-height: 1.2; }
10
+ .trix-content blockquote {
11
+ border: 0 solid #ccc;
12
+ border-left-width: 0.3em;
13
+ margin-left: 0.3em;
14
+ padding-left: 0.6em; }
15
+ .trix-content [dir=rtl] blockquote,
16
+ .trix-content blockquote[dir=rtl] {
17
+ border-width: 0;
18
+ border-right-width: 0.3em;
19
+ margin-right: 0.3em;
20
+ padding-right: 0.6em; }
21
+ .trix-content li {
22
+ margin-left: 1em; }
23
+ .trix-content [dir=rtl] li {
24
+ margin-right: 1em; }
25
+ .trix-content pre {
26
+ display: inline-block;
27
+ width: 100%;
28
+ vertical-align: top;
29
+ font-family: monospace;
30
+ font-size: 0.9em;
31
+ padding: 0.5em;
32
+ white-space: pre;
33
+ background-color: #eee;
34
+ overflow-x: auto; }
35
+ .trix-content img {
36
+ max-width: 100%;
37
+ height: auto; }
38
+ .trix-content .attachment {
39
+ display: inline-block;
40
+ position: relative;
41
+ max-width: 100%; }
42
+ .trix-content .attachment a {
43
+ color: inherit;
44
+ text-decoration: none; }
45
+ .trix-content .attachment a:hover, .trix-content .attachment a:visited:hover {
46
+ color: inherit; }
47
+ .trix-content .attachment__caption {
48
+ text-align: center; }
49
+ .trix-content .attachment__caption .attachment__name + .attachment__size::before {
50
+ content: ' \2022 '; }
51
+ .trix-content .attachment--preview {
52
+ width: 50%;
53
+ text-align: center; }
54
+ .trix-content .attachment--preview .attachment__caption {
55
+ color: #666;
56
+ font-size: 0.9em;
57
+ line-height: 1.2; }
58
+ .trix-content .attachment--file {
59
+ color: #333;
60
+ line-height: 1;
61
+ margin: 0 2px 2px 2px;
62
+ padding: 0.4em 1em;
63
+ border: 1px solid #bbb;
64
+ border-radius: 5px; }
65
+ .trix-content .attachment-gallery {
66
+ display: flex;
67
+ flex-wrap: wrap;
68
+ position: relative; }
69
+ .trix-content .attachment-gallery .attachment {
70
+ flex: 1 0 33%;
71
+ padding: 0 0.5em;
72
+ max-width: 33%; }
73
+ .trix-content .attachment-gallery.attachment-gallery--2 .attachment, .trix-content .attachment-gallery.attachment-gallery--4 .attachment {
74
+ flex-basis: 50%;
75
+ max-width: 50%; }
data/lib/robin_cms.rb CHANGED
@@ -23,10 +23,14 @@ module RobinCMS
23
23
 
24
24
  class ConversionError < StandardError
25
25
  end
26
+
27
+ class InvalidOperationError < StandardError
28
+ end
26
29
  end
27
30
 
28
31
  require_relative "robin_cms/version"
29
32
  require_relative "robin_cms/configuration"
33
+ require_relative "robin_cms/configuration_schema"
30
34
  require_relative "robin_cms/editable"
31
35
  require_relative "robin_cms/sluggable"
32
36
  require_relative "robin_cms/queryable"
@@ -34,6 +38,7 @@ require_relative "robin_cms/item"
34
38
  require_relative "robin_cms/library"
35
39
  require_relative "robin_cms/collection_library"
36
40
  require_relative "robin_cms/data_library"
41
+ require_relative "robin_cms/singular_library"
37
42
  require_relative "robin_cms/item"
38
43
  require_relative "robin_cms/auth"
39
44
  require_relative "robin_cms/flash"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robin_cms
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aron Lebani
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-25 00:00:00.000000000 Z
11
+ date: 2026-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt
@@ -80,8 +80,8 @@ files:
80
80
  - lib/robin_cms/auth.rb
81
81
  - lib/robin_cms/cms.rb
82
82
  - lib/robin_cms/collection_library.rb
83
- - lib/robin_cms/configuration-schema.json
84
83
  - lib/robin_cms/configuration.rb
84
+ - lib/robin_cms/configuration_schema.rb
85
85
  - lib/robin_cms/data_library.rb
86
86
  - lib/robin_cms/editable.rb
87
87
  - lib/robin_cms/flash.rb
@@ -89,6 +89,7 @@ files:
89
89
  - lib/robin_cms/item.rb
90
90
  - lib/robin_cms/library.rb
91
91
  - lib/robin_cms/queryable.rb
92
+ - lib/robin_cms/singular_library.rb
92
93
  - lib/robin_cms/sluggable.rb
93
94
  - lib/robin_cms/version.rb
94
95
  - lib/robin_cms/views/change_password.erb
@@ -112,6 +113,7 @@ files:
112
113
  - lib/robin_cms/views/richtext_field.erb
113
114
  - lib/robin_cms/views/select_field.erb
114
115
  - lib/robin_cms/views/stylesheet.css.erb
116
+ - lib/robin_cms/views/trix.css.erb
115
117
  homepage: https://robincms.org
116
118
  licenses:
117
119
  - MIT
@@ -133,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
133
135
  - !ruby/object:Gem::Version
134
136
  version: '0'
135
137
  requirements: []
136
- rubygems_version: 3.5.16
138
+ rubygems_version: 3.5.22
137
139
  signing_key:
138
140
  specification_version: 4
139
141
  summary: A simple, flat-file CMS for Static Site Generators.
@@ -1,120 +0,0 @@
1
- {
2
- "$schema": "http://json-schema.org/draft-06/schema#",
3
- "title": "RobinCMS configuration schema",
4
- "type": "object",
5
- "required": ["url", "libraries"],
6
- "properties": {
7
- "title": { "type": "string" },
8
- "url": { "type": "string" },
9
- "build_command": { "type": "string" },
10
- "accent_color": {
11
- "type": "string",
12
- "pattern": "^\#{1}[a-fA-F0-9]{6}"
13
- },
14
- "libraries": {
15
- "type": "array",
16
- "items": {
17
- "anyOf": [
18
- { "$ref": "#/$defs/library_schema" },
19
- { "$ref": "#/$defs/data_schema" }
20
- ]
21
- }
22
- }
23
- },
24
- "$defs": {
25
- "options_schema": {
26
- "type": "array",
27
- "items": {
28
- "type": "object",
29
- "required": ["label", "value"],
30
- "properties": {
31
- "label": { "type": "string" },
32
- "value": { "type": "string" }
33
- }
34
- }
35
- },
36
- "image_dimensions_schema": { "type": "string", "pattern": "^[0-9]+x[0-9]+$" },
37
- "image_filetype_schema": { "type": "string", "enum": ["png", "jpg"] },
38
- "library_schema": {
39
- "type": "object",
40
- "required": ["id", "label", "label_singular"],
41
- "properties": {
42
- "id": { "type": "string" },
43
- "type": { "type": "string", "const": "collection" },
44
- "filetype": { "type": "string", "enum": ["html"] },
45
- "label": { "type": "string" },
46
- "label_singular": { "type": "string" },
47
- "location": { "type": "string" },
48
- "static_location": { "type": "string" },
49
- "description": { "type": "string" },
50
- "can_create": { "type": "boolean" },
51
- "can_delete": { "type": "boolean" },
52
- "pattern": { "type": "string" },
53
- "display_name_pattern": { "type": "string" },
54
- "fields": {
55
- "type": "array",
56
- "items": {
57
- "type": "object",
58
- "required": ["id", "label"],
59
- "properties": {
60
- "id": { "type": "string" },
61
- "label": { "type": "string" },
62
- "type": {
63
- "type": "string",
64
- "enum": ["text", "richtext", "date", "hidden", "number", "color", "email", "url", "select", "image"]
65
- },
66
- "default": { "type": "string" },
67
- "required": { "type": "boolean" },
68
- "readonly": { "type": "boolean" },
69
- "options": { "$ref": "#/$defs/options_schema" },
70
- "dimensions": { "$ref": "#/$defs/image_dimensions_schema" },
71
- "filetype": { "$ref": "#/$defs/image_filetype_schema" },
72
- "description": { "type": "string" },
73
- "order": { "type": "number" }
74
- }
75
- }
76
- }
77
- }
78
- },
79
- "data_schema": {
80
- "type": "object",
81
- "required": ["id", "label", "label_singular"],
82
- "properties": {
83
- "id": { "type": "string" },
84
- "type": { "type": "string", "const": "data" },
85
- "filetype": { "type": "string", "enum": ["yml"] },
86
- "label": { "type": "string" },
87
- "label_singular": { "type": "string" },
88
- "location": { "type": "string" },
89
- "static_location": { "type": "string" },
90
- "description": { "type": "string" },
91
- "can_create": { "type": "boolean" },
92
- "can_delete": { "type": "boolean" },
93
- "pattern": { "type": "string" },
94
- "display_name_pattern": { "type": "string" },
95
- "fields": {
96
- "type": "array",
97
- "items": {
98
- "type": "object",
99
- "required": ["id", "label"],
100
- "properties": {
101
- "id": { "type": "string" },
102
- "label": { "type": "string" },
103
- "type": {
104
- "type": "string",
105
- "enum": ["text", "date", "hidden", "number", "color", "email", "url", "select", "image"]
106
- },
107
- "default": { "type": "string" },
108
- "required": { "type": "boolean" },
109
- "readonly": { "type": "boolean" },
110
- "options": { "$ref": "#/$defs/options_schema" },
111
- "dimensions": { "$ref": "#/$defs/image_dimensions_schema" },
112
- "filetype": { "$ref": "#/$defs/image_filetype_schema" },
113
- "description": { "type": "string" }
114
- }
115
- }
116
- }
117
- }
118
- }
119
- }
120
- }