robin_cms 0.2.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee6ad0de352f4750144a35a2b258d5d2da00e8fc345f269be1aeec80a45bf5ee
4
- data.tar.gz: 5bed86dbdf4f9c4041e324d60284583beb1c65551b3ff15964d3950fed8c9cd3
3
+ metadata.gz: 26f13be1b3e97bce69e705daf56e969916467591b7c30b2877c2ce29e07e62d3
4
+ data.tar.gz: 7f34fbee635184285025e757e3c52ef8e25467255ed4a90647203bbfcd656756
5
5
  SHA512:
6
- metadata.gz: dc0022fb5043ef5a2a640111ca650926c542cbd0a2a39f5895e6e2f6461edb4c6e4f44d1b8a9fbab01096dfb23ded529bc1458ac6923938eed8a82bcde2e2e71
7
- data.tar.gz: 7cc63df15ad7619328ac1c9cf0c45c0dddf106e724081ad49a6b60d5f61645dab33dd6d7329e4b8ff39ebbfad0188aef714952e64c100b9ad59827eb0cfc69e1
6
+ metadata.gz: a420e205f0f06d34817b5cb1424f513051cddb7a665dcddb9d707c091088969e7031bef154542347691fe109a612f58741c229f622380281e4289a2bcfa1a268
7
+ data.tar.gz: 7d1e1bbd2b23d02c02a7dcca004c35015663bb2ece865f7d4ebfabbcd96e2897bc95bd758bd64145cb70d5ef2f7819717d195417f7121254080773b1f70a9f43
data/lib/robin_cms/cms.rb CHANGED
@@ -90,6 +90,11 @@ module RobinCMS
90
90
  published: params[:published],
91
91
  q: params[:q]
92
92
  )
93
+
94
+ if @library.singular?
95
+ redirect to("/libraries/#{params[:kind]}/item/0")
96
+ end
97
+
93
98
  erb :library
94
99
  end
95
100
 
@@ -31,6 +31,17 @@ module RobinCMS
31
31
  fields: []
32
32
  }.freeze
33
33
 
34
+ SINGULAR_DEFAULTS = {
35
+ type: "singular",
36
+ location: "_data",
37
+ static_location: "uploads",
38
+ filetype: "yml",
39
+ description: "",
40
+ can_create: false,
41
+ can_delete: false,
42
+ fields: []
43
+ }.freeze
44
+
34
45
  FIELD_DEFAULTS = {
35
46
  type: "text",
36
47
  default: nil,
@@ -55,7 +66,7 @@ module RobinCMS
55
66
  type: "hidden"
56
67
  }].freeze
57
68
 
58
- AUTOMATIC_DRAFT_FIELDS = [{
69
+ AUTOMATIC_COLLECTION_FIELDS = [{
59
70
  label: "Published",
60
71
  id: "published",
61
72
  type: "select",
@@ -72,6 +83,14 @@ module RobinCMS
72
83
  order: 2
73
84
  }].freeze
74
85
 
86
+ AUTOMATIC_SINGULAR_FIELDS = [{
87
+ label: "Title",
88
+ id: "title",
89
+ type: "hidden",
90
+ required: true,
91
+ order: 1
92
+ }]
93
+
75
94
  AUTOMATIC_IMAGE_FIELDS = [{
76
95
  id: "image_src",
77
96
  type: "hidden"
@@ -94,10 +113,7 @@ module RobinCMS
94
113
  YAML.load_file(config_file, symbolize_names: true)
95
114
  end
96
115
 
97
- # TODO: use a ruby schema instead of a json file. Makes it easier to
98
- # reuse elements and what not.
99
- schema_file = File.join(__dir__, "configuration-schema.json")
100
- schema = JSON.parse(File.read(schema_file))
116
+ schema = ConfigurationSchema.schema
101
117
  errors = JSON::Validator.fully_validate(schema, data)
102
118
 
103
119
  unless errors.empty?
@@ -122,11 +138,24 @@ module RobinCMS
122
138
  library[:type].nil? || library[:type] == "collection"
123
139
  end
124
140
 
141
+ def data?(library)
142
+ library[:type] == "data"
143
+ end
144
+
145
+ def singular?(library)
146
+ library[:type] == "singular"
147
+ end
148
+
125
149
  def merge_config(config)
126
150
  merge(config, {
127
151
  libraries: config[:libraries].map do |library|
128
- library_defaults =
129
- collection?(library) ? COLLECTION_DEFAULTS : DATA_DEFAULTS
152
+ library_defaults = if collection?(library)
153
+ COLLECTION_DEFAULTS
154
+ elsif data?(library)
155
+ DATA_DEFAULTS
156
+ elsif singular?(library)
157
+ SINGULAR_DEFAULTS
158
+ end
130
159
 
131
160
  library_defaults.merge(library, {
132
161
  fields: library[:fields].map do |field|
@@ -144,7 +173,15 @@ module RobinCMS
144
173
  library[:fields].append(*AUTOMATIC_FIELDS)
145
174
 
146
175
  if collection?(library)
147
- library[:fields].append(*AUTOMATIC_DRAFT_FIELDS)
176
+ library[:fields].append(*AUTOMATIC_COLLECTION_FIELDS)
177
+ end
178
+
179
+ # Replace the automatic title field with a hidden one. The title is not
180
+ # used for singular fields, but every item is required to have one, so
181
+ # hide it in the UI.
182
+ if singular?(library)
183
+ # Using prepend ensures this one stays when we call uniq! below.
184
+ library[:fields].prepend(*AUTOMATIC_SINGULAR_FIELDS)
148
185
  end
149
186
 
150
187
  # Ensure the automatic image fields are inserted directly after the
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal => true
2
+
3
+ module RobinCMS
4
+ module ConfigurationSchema
5
+ def self.field_schema(kind)
6
+ field_types = [
7
+ "text",
8
+ "date",
9
+ "hidden",
10
+ "number",
11
+ "color",
12
+ "email",
13
+ "url",
14
+ "select",
15
+ "image"
16
+ ]
17
+
18
+ if kind == "collection"
19
+ field_types << "richtext"
20
+ field_types << "array"
21
+ end
22
+
23
+ {
24
+ "type" => "object",
25
+ "required" => ["id", "label"],
26
+ "properties" => {
27
+ "id" => { "type" => "string" },
28
+ "label" => { "type" => "string" },
29
+ "type" => { "type" => "string", "enum" => field_types },
30
+ "default" => { "type" => "string" },
31
+ "required" => { "type" => "boolean" },
32
+ "readonly" => { "type" => "boolean" },
33
+ "option" => {
34
+ "type" => "array",
35
+ "items" => {
36
+ "type" => "object",
37
+ "required" => ["label", "value"],
38
+ "properties" => {
39
+ "label" => { "type" => "string" },
40
+ "value" => { "type" => "string" }
41
+ }
42
+ }
43
+ },
44
+ "dimensions" => { "type" => "string", "pattern" => "^[0-9]+x[0-9]+$" },
45
+ "filetype" => { "type" => "string", "enum" => ["png", "jpg"] },
46
+ "description" => { "type" => "string" },
47
+ "order" => { "type" => "number" }
48
+ }
49
+ }
50
+ end
51
+
52
+ def self.library_schema(kind)
53
+ filetypes = case kind
54
+ when "collection"
55
+ ["html"]
56
+ when "data", "singular"
57
+ ["yml"]
58
+ end
59
+
60
+ {
61
+ "type" => "object",
62
+ "required" => ["id", "label", "label_singular"],
63
+ "properties" => {
64
+ "id" => { "type" => "string" },
65
+ "type" => { "type" => "string", "const" => kind },
66
+ "filetype" => { "type" => "string", "enum" => filetypes },
67
+ "label" => { "type" => "string" },
68
+ "label_singular" => { "type" => "string" },
69
+ "location" => { "type" => "string" },
70
+ "static_location" => { "type" => "string" },
71
+ "description" => { "type" => "string" },
72
+ "can_create" => { "type" => "boolean" },
73
+ "can_delete" => { "type" => "boolean" },
74
+ "pattern" => { "type" => "string" },
75
+ "display_name_pattern" => { "type" => "string" },
76
+ "fields" => { "type" => "array", "items" => field_schema(kind) }
77
+ }
78
+ }
79
+ end
80
+
81
+ def self.schema
82
+ {
83
+ "$schema" => "http://json-schema.org/draft-06/schema#",
84
+ "title" => "RobinCMS configuration schema",
85
+ "type" => "object",
86
+ "required" => ["url", "libraries"],
87
+ "properties" => {
88
+ "title" => { "type" => "string" },
89
+ "url" => { "type" => "string" },
90
+ "build_command" => { "type" => "string" },
91
+ "accent_color" => {
92
+ "type" => "string",
93
+ "pattern" => '^\\#{1}[a-fA-F0-9]{6}'
94
+ },
95
+ "libraries" => {
96
+ "type" => "array",
97
+ "items" => {
98
+ "anyOf" => [
99
+ library_schema("collection"),
100
+ library_schema("data"),
101
+ library_schema("singular")
102
+ ]
103
+ }
104
+ }
105
+ }
106
+ }
107
+ end
108
+ end
109
+ end
@@ -29,6 +29,8 @@ module RobinCMS
29
29
  extend CollectionLibrary
30
30
  when "data"
31
31
  extend DataLibrary
32
+ when "singular"
33
+ extend SingularLibrary
32
34
  end
33
35
  end
34
36
 
@@ -86,5 +88,21 @@ module RobinCMS
86
88
  def kind
87
89
  @schema[:id]
88
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
89
107
  end
90
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.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -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.
@@ -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"
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.2.0
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: 2026-09-11 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
@@ -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", "array"]
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
- }