primer_view_components 0.0.43 → 0.0.44

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -4
  3. data/app/assets/javascripts/primer_view_components.js +1 -1
  4. data/app/assets/javascripts/primer_view_components.js.map +1 -1
  5. data/app/components/primer/alpha/button_marketing.rb +70 -0
  6. data/app/components/primer/auto_complete.rb +97 -41
  7. data/app/components/primer/auto_complete/auto_complete.html.erb +1 -0
  8. data/app/components/primer/beta/text.rb +27 -0
  9. data/app/components/primer/blankslate_component.rb +2 -1
  10. data/app/components/primer/button_component.rb +3 -2
  11. data/app/components/primer/details_component.rb +12 -1
  12. data/app/components/primer/dropdown.d.ts +1 -0
  13. data/app/components/primer/{dropdown_component.html.erb → dropdown.html.erb} +2 -1
  14. data/app/components/primer/dropdown.js +1 -0
  15. data/app/components/primer/dropdown.rb +149 -0
  16. data/app/components/primer/dropdown.ts +1 -0
  17. data/app/components/primer/dropdown/menu.d.ts +1 -0
  18. data/app/components/primer/dropdown/menu.html.erb +25 -0
  19. data/app/components/primer/dropdown/menu.js +1 -0
  20. data/app/components/primer/dropdown/menu.rb +99 -0
  21. data/app/components/primer/dropdown/menu.ts +1 -0
  22. data/app/components/primer/heading_component.rb +1 -1
  23. data/app/components/primer/icon_button.rb +1 -1
  24. data/app/components/primer/navigation/tab_component.rb +2 -2
  25. data/app/components/primer/octicon_component.rb +3 -2
  26. data/app/components/primer/primer.d.ts +1 -0
  27. data/app/components/primer/primer.js +1 -0
  28. data/app/components/primer/primer.ts +1 -0
  29. data/app/components/primer/spinner_component.rb +2 -0
  30. data/lib/primer/view_components/linters/argument_mappers/button.rb +82 -0
  31. data/lib/primer/view_components/linters/argument_mappers/conversion_error.rb +10 -0
  32. data/lib/primer/view_components/linters/argument_mappers/system_arguments.rb +46 -0
  33. data/lib/primer/view_components/linters/button_component_migration_counter.rb +20 -1
  34. data/lib/primer/view_components/linters/flash_component_migration_counter.rb +1 -1
  35. data/lib/primer/view_components/linters/helpers.rb +7 -3
  36. data/lib/primer/view_components/version.rb +1 -1
  37. data/lib/tasks/docs.rake +111 -96
  38. data/lib/yard/docs_helper.rb +12 -2
  39. data/static/statuses.json +6 -4
  40. metadata +17 -8
  41. data/app/components/primer/button_marketing_component.rb +0 -68
  42. data/app/components/primer/dropdown/menu_component.html.erb +0 -12
  43. data/app/components/primer/dropdown/menu_component.rb +0 -46
  44. data/app/components/primer/dropdown_component.rb +0 -73
  45. data/app/components/primer/text_component.rb +0 -25
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ERBLint
4
+ module Linters
5
+ module ArgumentMappers
6
+ # Error when converting arguments.
7
+ class ConversionError < StandardError; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "conversion_error"
4
+
5
+ module ERBLint
6
+ module Linters
7
+ module ArgumentMappers
8
+ # Maps element attributes to system arguments.
9
+ class SystemArguments
10
+ STRING_PARAETERS = %w[aria- data-].freeze
11
+ TEST_SELECTOR_REGEX = /test_selector\((?<selector>.+)\)$/.freeze
12
+
13
+ attr_reader :attribute
14
+ def initialize(attribute)
15
+ @attribute = attribute
16
+ end
17
+
18
+ def to_args
19
+ if attribute.erb?
20
+ _, _, code_node = *attribute.node
21
+
22
+ raise ConversionError, "Cannot convert erb block" if code_node.nil?
23
+
24
+ code = code_node.loc.source.strip
25
+ m = code.match(TEST_SELECTOR_REGEX)
26
+
27
+ raise ConversionError, "Cannot convert erb block" if m.blank?
28
+
29
+ { test_selector: m[:selector].tr("'", '"') }
30
+ elsif attr_name == "data-test-selector"
31
+ { test_selector: attribute.value.to_json }
32
+ elsif attr_name.start_with?(*STRING_PARAETERS)
33
+ # if attribute has no value_node, it means it is a boolean attribute.
34
+ { "\"#{attr_name}\"" => attribute.value_node ? attribute.value.to_json : true }
35
+ else
36
+ raise ConversionError, "Cannot convert attribute \"#{attr_name}\""
37
+ end
38
+ end
39
+
40
+ def attr_name
41
+ attribute.name
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "helpers"
4
+ require_relative "argument_mappers/button"
4
5
 
5
6
  module ERBLint
6
7
  module Linters
@@ -9,8 +10,26 @@ module ERBLint
9
10
  include Helpers
10
11
 
11
12
  TAGS = %w[button summary a].freeze
12
- CLASS = "btn"
13
+ CLASSES = %w[btn btn-link].freeze
13
14
  MESSAGE = "We are migrating buttons to use [Primer::ButtonComponent](https://primer.style/view-components/components/button), please try to use that instead of raw HTML."
15
+
16
+ private
17
+
18
+ def map_arguments(tag)
19
+ ArgumentMappers::Button.new(tag).to_s
20
+ rescue ArgumentMappers::ConversionError
21
+ nil
22
+ end
23
+
24
+ def message(tag)
25
+ args = map_arguments(tag)
26
+
27
+ return MESSAGE if args.nil?
28
+
29
+ msg = "#{MESSAGE}\n\nTry using:\n\n<%= render Primer::ButtonComponent.new"
30
+ msg += "(#{args})" if args.present?
31
+ "#{msg} %>\n\nInstead of:\n"
32
+ end
14
33
  end
15
34
  end
16
35
  end
@@ -9,7 +9,7 @@ module ERBLint
9
9
  include Helpers
10
10
 
11
11
  TAGS = %w[div].freeze
12
- CLASS = "flash"
12
+ CLASSES = %w[flash].freeze
13
13
  MESSAGE = "We are migrating flashes to use [Primer::FlashComponent](https://primer.style/view-components/components/flash), please try to use that instead of raw HTML."
14
14
  end
15
15
  end
@@ -8,7 +8,7 @@ module ERBLint
8
8
  # Helper methods for linting ERB.
9
9
  module Helpers
10
10
  def self.included(base)
11
- base.include(LinterRegistry)
11
+ base.include(ERBLint::LinterRegistry)
12
12
 
13
13
  define_method "run" do |processed_source|
14
14
  tags(processed_source).each do |tag|
@@ -17,9 +17,9 @@ module ERBLint
17
17
 
18
18
  classes = tag.attributes["class"]&.value&.split(" ")
19
19
 
20
- next unless !self.class::CLASS || classes&.include?(self.class::CLASS)
20
+ next if self.class::CLASSES.any? && (classes & self.class::CLASSES).blank?
21
21
 
22
- generate_offense(self.class, processed_source, tag, self.class::MESSAGE)
22
+ generate_offense(self.class, processed_source, tag, message(tag))
23
23
  end
24
24
 
25
25
  counter_correct?(processed_source)
@@ -42,6 +42,10 @@ module ERBLint
42
42
 
43
43
  private
44
44
 
45
+ def message(_tag)
46
+ self.class::MESSAGE
47
+ end
48
+
45
49
  def tags(processed_source)
46
50
  processed_source.parser.nodes_with_type(:tag).map { |tag_node| BetterHtml::Tree::Tag.from_node(tag_node) }
47
51
  end
@@ -5,7 +5,7 @@ module Primer
5
5
  module VERSION
6
6
  MAJOR = 0
7
7
  MINOR = 0
8
- PATCH = 43
8
+ PATCH = 44
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH].join(".")
11
11
  end
data/lib/tasks/docs.rake CHANGED
@@ -43,12 +43,12 @@ namespace :docs do
43
43
  Primer::BreadcrumbComponent,
44
44
  Primer::ButtonComponent,
45
45
  Primer::ButtonGroup,
46
- Primer::ButtonMarketingComponent,
46
+ Primer::Alpha::ButtonMarketing,
47
47
  Primer::ClipboardCopy,
48
48
  Primer::CloseButton,
49
49
  Primer::CounterComponent,
50
50
  Primer::DetailsComponent,
51
- Primer::DropdownComponent,
51
+ Primer::Dropdown,
52
52
  Primer::DropdownMenuComponent,
53
53
  Primer::FlashComponent,
54
54
  Primer::FlexComponent,
@@ -69,7 +69,7 @@ namespace :docs do
69
69
  Primer::SubheadComponent,
70
70
  Primer::TabContainerComponent,
71
71
  Primer::TabNavComponent,
72
- Primer::TextComponent,
72
+ Primer::Beta::Text,
73
73
  Primer::TimeAgoComponent,
74
74
  Primer::TimelineItemComponent,
75
75
  Primer::Tooltip,
@@ -78,6 +78,7 @@ namespace :docs do
78
78
  ]
79
79
 
80
80
  js_components = [
81
+ Primer::Dropdown,
81
82
  Primer::LocalTime,
82
83
  Primer::ImageCrop,
83
84
  Primer::AutoComplete,
@@ -91,10 +92,11 @@ namespace :docs do
91
92
  all_components = Primer::Component.descendants - [Primer::BaseComponent]
92
93
  components_needing_docs = all_components - components
93
94
 
94
- components_without_examples = []
95
95
  args_for_components = []
96
96
  classes_found_in_examples = []
97
97
 
98
+ errors = []
99
+
98
100
  components.each do |component|
99
101
  documentation = registry.get(component.name)
100
102
 
@@ -113,6 +115,8 @@ namespace :docs do
113
115
  f.puts
114
116
  f.puts("import Example from '../../src/@primer/gatsby-theme-doctocat/components/example'")
115
117
 
118
+ initialize_method = documentation.meths.find(&:constructor?)
119
+
116
120
  if js_components.include?(component)
117
121
  f.puts("import RequiresJSFlash from '../../src/@primer/gatsby-theme-doctocat/components/requires-js-flash'")
118
122
  f.puts
@@ -124,108 +128,68 @@ namespace :docs do
124
128
  f.puts
125
129
  f.puts(view_context.render(inline: documentation.base_docstring))
126
130
 
127
- if documentation.tags(:accessibility).any?
131
+ if documentation.tags(:deprecated).any?
128
132
  f.puts
129
- f.puts("## Accessibility")
130
- documentation.tags(:accessibility).each do |tag|
133
+ f.puts("## Deprecation")
134
+ documentation.tags(:deprecated).each do |tag|
131
135
  f.puts
132
136
  f.puts view_context.render(inline: tag.text)
133
137
  end
134
138
  end
135
139
 
136
- if documentation.tags(:deprecated).any?
140
+ if documentation.tags(:accessibility).any?
137
141
  f.puts
138
- f.puts("## Deprecation")
139
- documentation.tags(:deprecated).each do |tag|
142
+ f.puts("## Accessibility")
143
+ documentation.tags(:accessibility).each do |tag|
140
144
  f.puts
141
145
  f.puts view_context.render(inline: tag.text)
142
146
  end
143
147
  end
144
148
 
145
- initialize_method = documentation.meths.find(&:constructor?)
149
+ params = initialize_method.tags(:param)
146
150
 
147
- if initialize_method.tags(:example).any?
148
- f.puts
149
- f.puts("## Examples")
150
- else
151
- components_without_examples << component
152
- end
151
+ errors << { component.name => { arguments: "No argument documentation found" } } unless params.any?
153
152
 
154
- initialize_method.tags(:example).each do |tag|
155
- name = tag.name
156
- description = nil
157
- code = nil
153
+ f.puts
154
+ f.puts("## Arguments")
155
+ f.puts
156
+ f.puts("| Name | Type | Default | Description |")
157
+ f.puts("| :- | :- | :- | :- |")
158
158
 
159
- if tag.text.include?("@description")
160
- splitted = tag.text.split(/@description|@code/)
161
- description = splitted.second.gsub(/^[ \t]{2}/, "").strip
162
- code = splitted.last.gsub(/^[ \t]{2}/, "").strip
163
- else
164
- code = tag.text
165
- end
159
+ docummented_params = params.map(&:name)
160
+ component_params = component.instance_method(:initialize).parameters.map { |p| p.last.to_s }
166
161
 
167
- f.puts
168
- f.puts("### #{name}")
169
- if description
170
- f.puts
171
- f.puts(description)
162
+ if (docummented_params & component_params).size != component_params.size
163
+ err = { arguments: {} }
164
+ (component_params - docummented_params).each do |arg|
165
+ err[:arguments][arg] = "Not documented"
172
166
  end
173
- f.puts
174
- html = view_context.render(inline: code)
175
- html.scan(/class="([^"]*)"/) do |classnames|
176
- classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
177
- end
178
- f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
179
- f.puts
180
- f.puts("```erb")
181
- f.puts(code.to_s)
182
- f.puts("```")
167
+
168
+ errors << { component.name => err }
183
169
  end
184
170
 
185
- params = initialize_method.tags(:param)
186
- if params.any?
187
- f.puts
188
- f.puts("## Arguments")
189
- f.puts
190
- f.puts("| Name | Type | Default | Description |")
191
- f.puts("| :- | :- | :- | :- |")
192
-
193
- args = []
194
- params.each do |tag|
195
- params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
196
-
197
- default =
198
- if params && params[1]
199
- constant_name = "#{component.name}::#{params[1]}"
200
- constant_value = constant_name.safe_constantize
201
- if constant_value.nil?
202
- pretty_value(params[1])
203
- else
204
- pretty_value(constant_value)
205
- end
206
- else
207
- "N/A"
208
- end
209
-
210
- args << {
211
- "name" => tag.name,
212
- "type" => tag.types.join(", "),
213
- "default" => default,
214
- "description" => view_context.render(inline: tag.text)
215
- }
216
-
217
- f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |")
218
- end
171
+ args = []
172
+ params.each do |tag|
173
+ default_value = pretty_default_value(tag, component)
219
174
 
220
- component_args = {
221
- "component" => short_name,
222
- "source" => "https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb",
223
- "parameters" => args
175
+ args << {
176
+ "name" => tag.name,
177
+ "type" => tag.types.join(", "),
178
+ "default" => default_value,
179
+ "description" => view_context.render(inline: tag.text)
224
180
  }
225
181
 
226
- args_for_components << component_args
182
+ f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default_value} | #{view_context.render(inline: tag.text)} |")
227
183
  end
228
184
 
185
+ component_args = {
186
+ "component" => short_name,
187
+ "source" => "https://github.com/primer/view_components/tree/main/app/components/primer/#{component.to_s.demodulize.underscore}.rb",
188
+ "parameters" => args
189
+ }
190
+
191
+ args_for_components << component_args
192
+
229
193
  # Slots V2 docs
230
194
  slot_v2_methods = documentation.meths.select { |x| x[:renders_one] || x[:renders_many] }
231
195
 
@@ -250,22 +214,61 @@ namespace :docs do
250
214
  end
251
215
 
252
216
  param_tags.each do |tag|
253
- params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
217
+ f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{pretty_default_value(tag, component)} | #{view_context.render(inline: tag.text)} |")
218
+ end
219
+ end
220
+ end
254
221
 
255
- default =
256
- if params && params[1]
257
- "`#{params[1]}`"
258
- else
259
- "N/A"
260
- end
222
+ errors << { component.name => { example: "No examples found" } } unless initialize_method.tags(:example).any?
261
223
 
262
- f.puts("| `#{tag.name}` | `#{tag.types.join(', ')}` | #{default} | #{view_context.render(inline: tag.text)} |")
263
- end
224
+ f.puts
225
+ f.puts("## Examples")
226
+
227
+ initialize_method.tags(:example).each do |tag|
228
+ name = tag.name
229
+ description = nil
230
+ code = nil
231
+
232
+ if tag.text.include?("@description")
233
+ splitted = tag.text.split(/@description|@code/)
234
+ description = splitted.second.gsub(/^[ \t]{2}/, "").strip
235
+ code = splitted.last.gsub(/^[ \t]{2}/, "").strip
236
+ else
237
+ code = tag.text
238
+ end
239
+
240
+ f.puts
241
+ f.puts("### #{name}")
242
+ if description
243
+ f.puts
244
+ f.puts(description)
245
+ end
246
+ f.puts
247
+ html = view_context.render(inline: code)
248
+ html.scan(/class="([^"]*)"/) do |classnames|
249
+ classes_found_in_examples.concat(classnames[0].split(" ").reject { |c| c.starts_with?("octicon", "js", "my-") }.map { ".#{_1}"})
264
250
  end
251
+ f.puts("<Example src=\"#{html.tr('"', "\'").delete("\n")}\" />")
252
+ f.puts
253
+ f.puts("```erb")
254
+ f.puts(code.to_s)
255
+ f.puts("```")
265
256
  end
266
257
  end
267
258
  end
268
259
 
260
+ unless errors.empty?
261
+ puts "==============================================="
262
+ puts "===================== ERRORS =================="
263
+ puts "===============================================\n\n"
264
+ puts JSON.pretty_generate(errors)
265
+ puts "\n\n==============================================="
266
+ puts "==============================================="
267
+ puts "==============================================="
268
+
269
+ raise
270
+ end
271
+
269
272
  File.open("static/classes.yml", "w") do |f|
270
273
  f.puts YAML.dump(classes_found_in_examples.sort.uniq)
271
274
  end
@@ -293,11 +296,6 @@ namespace :docs do
293
296
 
294
297
  puts "Markdown compiled."
295
298
 
296
- if components_without_examples.any?
297
- puts
298
- puts "The following components have no examples defined: #{components_without_examples.map(&:name).join(', ')}. Consider adding an example?"
299
- end
300
-
301
299
  if components_needing_docs.any?
302
300
  puts
303
301
  puts "The following components needs docs. Care to contribute them? #{components_needing_docs.map(&:name).join(', ')}"
@@ -307,6 +305,8 @@ namespace :docs do
307
305
  task :preview do
308
306
  registry = generate_yard_registry
309
307
 
308
+ FileUtils.rm_rf("demo/test/components/previews/primer/docs/")
309
+
310
310
  components = Primer::Component.descendants
311
311
 
312
312
  # Generate previews from documentation examples
@@ -361,6 +361,7 @@ namespace :docs do
361
361
  # Custom tags for yard
362
362
  YARD::Tags::Library.define_tag("Accessibility", :accessibility)
363
363
  YARD::Tags::Library.define_tag("Deprecation", :deprecation)
364
+ YARD::Tags::Library.define_tag("Parameter", :param, :with_types_name_and_default)
364
365
 
365
366
  puts "Building YARD documentation."
366
367
  Rake::Task["yard"].execute
@@ -369,4 +370,18 @@ namespace :docs do
369
370
  registry.load!(".yardoc")
370
371
  registry
371
372
  end
373
+
374
+ def pretty_default_value(tag, component)
375
+ params = tag.object.parameters.find { |param| [tag.name.to_s, tag.name.to_s + ":"].include?(param[0]) }
376
+ default = tag.defaults&.first || params&.second
377
+
378
+ return "N/A" unless default
379
+
380
+ constant_name = "#{component.name}::#{default}"
381
+ constant_value = default.safe_constantize || constant_name.safe_constantize
382
+
383
+ return pretty_value(default) if constant_value.nil?
384
+
385
+ pretty_value(constant_value)
386
+ end
372
387
  end