terrazzo 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ff3b38b0cdec57570baa990aef1998197eba301cfa0c297d0619ff7d4003374
4
- data.tar.gz: f0556e47ac6c647db017b2ebb0167a5f9585a5d3c9353bab740131fbd717b02b
3
+ metadata.gz: '0668700d10e6547d3366e74348a33674733828a834d3be9dd02ddd7effab6bd8'
4
+ data.tar.gz: d85d82e780f8820d1b306ac5f63c3d1a96802da1f56f301e11b8040903372601
5
5
  SHA512:
6
- metadata.gz: b46cb63e1fe4754a6938fec445f76aa90eba36f55c3fde38f08713db3e325f504b3fe985816625a07e5a8b77e7fe6d820c62f262dd20d443deaa6566111c75db
7
- data.tar.gz: '0974a53199cdbdb29b8e90096429ee853df3bbeb5414cb60d1ef2dceca0c6356a7533921a3ab383a19b04bc408e08658e8f5e4d58a55f7dbfb5caa06454ad39f'
6
+ metadata.gz: 84bff5afcc8deb6b423f7112593c318b80716c384d7e1140d596e50b816de4e8c651bb76d9cfa4653b5154ec1c28a7185052928f136fcdafa044ef3d8762f4c4
7
+ data.tar.gz: eaec2494d92552710c555f3f45db30688e29ca4a20af4d29c488aa8d15f62efaa42d7ae8e399c95d5c3cb291bb36f3001f9e96e5e4c47d02ae3009490b377f3c
@@ -14,6 +14,8 @@ module Terrazzo
14
14
 
15
15
  before_action :use_jsx_rendering_defaults
16
16
 
17
+ prepend Terrazzo::UsesSuperglue::TemplateLookupOverride
18
+
17
19
  helper_method :namespace, :dashboard, :resource_name, :resource_class, :application_title, :terrazzo_page_identifier
18
20
 
19
21
  def index
@@ -15,12 +15,12 @@ module Terrazzo
15
15
 
16
16
  def create_dashboard
17
17
  template "dashboard.rb.erb",
18
- "app/dashboards/#{file_name}_dashboard.rb"
18
+ "app/dashboards/#{class_path.join('/')}/#{file_name}_dashboard.rb".squeeze("/")
19
19
  end
20
20
 
21
21
  def create_controller
22
22
  template "controller.rb.erb",
23
- "app/controllers/#{options[:namespace]}/#{file_name.pluralize}_controller.rb"
23
+ "app/controllers/#{options[:namespace]}/#{class_path.join('/')}/#{file_name.pluralize}_controller.rb".squeeze("/")
24
24
  end
25
25
 
26
26
  def update_page_to_page_mapping
@@ -75,7 +75,7 @@ module Terrazzo
75
75
  associations.each do |assoc|
76
76
  case assoc.macro
77
77
  when :belongs_to
78
- types[assoc.name] = "Field::BelongsTo"
78
+ types[assoc.name] = assoc.options[:polymorphic] ? "Field::Polymorphic" : "Field::BelongsTo"
79
79
  when :has_many, :has_and_belongs_to_many
80
80
  types[assoc.name] = "Field::HasMany"
81
81
  when :has_one
@@ -50,6 +50,11 @@ module Terrazzo
50
50
  "app/javascript/#{namespace_name}/slices/flash.js"
51
51
  end
52
52
 
53
+ def create_stylesheet
54
+ copy_file "admin.css",
55
+ "app/assets/stylesheets/#{namespace_name}.css"
56
+ end
57
+
53
58
  def run_views_generator
54
59
  generate "terrazzo:views", "--namespace=#{namespace_name}"
55
60
  end
@@ -75,10 +80,16 @@ module Terrazzo
75
80
  end
76
81
 
77
82
  def application_models
78
- Rails.application.eager_load! if defined?(Rails)
79
- ApplicationRecord.descendants.reject(&:abstract_class?)
80
- rescue
81
- []
83
+ models_path = Rails.root.join("app", "models")
84
+ return [] unless models_path.exist?
85
+
86
+ Dir[models_path.join("**", "*.rb")].filter_map do |file|
87
+ relative = Pathname.new(file).relative_path_from(models_path).to_s
88
+ next if relative.start_with?("concerns/")
89
+ next if relative == "application_record.rb"
90
+
91
+ relative.delete_suffix(".rb").camelize.safe_constantize
92
+ end.select { |klass| klass < ApplicationRecord && !klass.abstract_class? }
82
93
  end
83
94
  end
84
95
  end
@@ -0,0 +1,119 @@
1
+ @import "tailwindcss";
2
+
3
+ @custom-variant dark (&:is(.dark *));
4
+
5
+ :root {
6
+ --radius: 0.625rem;
7
+ --background: oklch(1 0 0);
8
+ --foreground: oklch(0.145 0 0);
9
+ --card: oklch(1 0 0);
10
+ --card-foreground: oklch(0.145 0 0);
11
+ --popover: oklch(1 0 0);
12
+ --popover-foreground: oklch(0.145 0 0);
13
+ --primary: oklch(0.205 0 0);
14
+ --primary-foreground: oklch(0.985 0 0);
15
+ --secondary: oklch(0.97 0 0);
16
+ --secondary-foreground: oklch(0.205 0 0);
17
+ --muted: oklch(0.97 0 0);
18
+ --muted-foreground: oklch(0.556 0 0);
19
+ --accent: oklch(0.97 0 0);
20
+ --accent-foreground: oklch(0.205 0 0);
21
+ --destructive: oklch(0.577 0.245 27.325);
22
+ --destructive-foreground: oklch(0.985 0 0);
23
+ --border: oklch(0.922 0 0);
24
+ --input: oklch(0.922 0 0);
25
+ --ring: oklch(0.708 0 0);
26
+ --chart-1: oklch(0.646 0.222 41.116);
27
+ --chart-2: oklch(0.6 0.118 184.704);
28
+ --chart-3: oklch(0.398 0.07 227.392);
29
+ --chart-4: oklch(0.828 0.189 84.429);
30
+ --chart-5: oklch(0.769 0.188 70.08);
31
+ --sidebar: oklch(0.985 0 0);
32
+ --sidebar-foreground: oklch(0.145 0 0);
33
+ --sidebar-primary: oklch(0.205 0 0);
34
+ --sidebar-primary-foreground: oklch(0.985 0 0);
35
+ --sidebar-accent: oklch(0.97 0 0);
36
+ --sidebar-accent-foreground: oklch(0.205 0 0);
37
+ --sidebar-border: oklch(0.922 0 0);
38
+ --sidebar-ring: oklch(0.708 0 0);
39
+ }
40
+
41
+ .dark {
42
+ --background: oklch(0.145 0 0);
43
+ --foreground: oklch(0.985 0 0);
44
+ --card: oklch(0.205 0 0);
45
+ --card-foreground: oklch(0.985 0 0);
46
+ --popover: oklch(0.269 0 0);
47
+ --popover-foreground: oklch(0.985 0 0);
48
+ --primary: oklch(0.922 0 0);
49
+ --primary-foreground: oklch(0.205 0 0);
50
+ --secondary: oklch(0.269 0 0);
51
+ --secondary-foreground: oklch(0.985 0 0);
52
+ --muted: oklch(0.269 0 0);
53
+ --muted-foreground: oklch(0.708 0 0);
54
+ --accent: oklch(0.371 0 0);
55
+ --accent-foreground: oklch(0.985 0 0);
56
+ --destructive: oklch(0.704 0.191 22.216);
57
+ --destructive-foreground: oklch(0.985 0 0);
58
+ --border: oklch(1 0 0 / 10%);
59
+ --input: oklch(1 0 0 / 15%);
60
+ --ring: oklch(0.556 0 0);
61
+ --chart-1: oklch(0.488 0.243 264.376);
62
+ --chart-2: oklch(0.696 0.17 162.48);
63
+ --chart-3: oklch(0.769 0.188 70.08);
64
+ --chart-4: oklch(0.627 0.265 303.9);
65
+ --chart-5: oklch(0.645 0.246 16.439);
66
+ --sidebar: oklch(0.205 0 0);
67
+ --sidebar-foreground: oklch(0.985 0 0);
68
+ --sidebar-primary: oklch(0.488 0.243 264.376);
69
+ --sidebar-primary-foreground: oklch(0.985 0 0);
70
+ --sidebar-accent: oklch(0.269 0 0);
71
+ --sidebar-accent-foreground: oklch(0.985 0 0);
72
+ --sidebar-border: oklch(1 0 0 / 10%);
73
+ --sidebar-ring: oklch(0.439 0 0);
74
+ }
75
+
76
+ @theme inline {
77
+ --color-background: var(--background);
78
+ --color-foreground: var(--foreground);
79
+ --color-card: var(--card);
80
+ --color-card-foreground: var(--card-foreground);
81
+ --color-popover: var(--popover);
82
+ --color-popover-foreground: var(--popover-foreground);
83
+ --color-primary: var(--primary);
84
+ --color-primary-foreground: var(--primary-foreground);
85
+ --color-secondary: var(--secondary);
86
+ --color-secondary-foreground: var(--secondary-foreground);
87
+ --color-muted: var(--muted);
88
+ --color-muted-foreground: var(--muted-foreground);
89
+ --color-accent: var(--accent);
90
+ --color-accent-foreground: var(--accent-foreground);
91
+ --color-destructive: var(--destructive);
92
+ --color-destructive-foreground: var(--destructive-foreground);
93
+ --color-border: var(--border);
94
+ --color-input: var(--input);
95
+ --color-ring: var(--ring);
96
+ --color-chart-1: var(--chart-1);
97
+ --color-chart-2: var(--chart-2);
98
+ --color-chart-3: var(--chart-3);
99
+ --color-chart-4: var(--chart-4);
100
+ --color-chart-5: var(--chart-5);
101
+ --color-sidebar: var(--sidebar);
102
+ --color-sidebar-foreground: var(--sidebar-foreground);
103
+ --color-sidebar-primary: var(--sidebar-primary);
104
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
105
+ --color-sidebar-accent: var(--sidebar-accent);
106
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
107
+ --color-sidebar-border: var(--sidebar-border);
108
+ --color-sidebar-ring: var(--sidebar-ring);
109
+ --radius-sm: calc(var(--radius) - 4px);
110
+ --radius-md: calc(var(--radius) - 2px);
111
+ --radius-lg: var(--radius);
112
+ --radius-xl: calc(var(--radius) + 4px);
113
+ }
114
+
115
+ *,
116
+ ::after,
117
+ ::before {
118
+ border-color: var(--color-border);
119
+ }
@@ -1,8 +1,50 @@
1
1
  import { visit, remote } from "@thoughtbot/superglue/action_creators"
2
2
 
3
- export function buildVisitAndRemote(ref, store, pageToPageMapping) {
4
- const appVisit = visit(ref, store, pageToPageMapping)
5
- const appRemote = remote(ref, store, pageToPageMapping)
3
+ export const buildVisitAndRemote = (ref, store) => {
4
+ const appRemote = (path, { dataset, ...options }) => {
5
+ return store.dispatch(remote(path, options))
6
+ }
7
+
8
+ const appVisit = (path, { dataset, ...options } = {}) => {
9
+ return store
10
+ .dispatch(visit(path, options))
11
+ .then((meta) => {
12
+ if (meta.needsRefresh) {
13
+ window.location.href = meta.pageKey
14
+ return meta
15
+ }
16
+
17
+ const navigationAction = !!dataset?.sgReplace
18
+ ? "replace"
19
+ : meta.navigationAction
20
+ ref.current?.navigateTo(meta.pageKey, {
21
+ action: navigationAction,
22
+ })
23
+
24
+ return meta
25
+ })
26
+ .catch((err) => {
27
+ const response = err.response
28
+
29
+ if (!response) {
30
+ console.error(err)
31
+ return
32
+ }
33
+
34
+ if (response.ok) {
35
+ window.location = response.url
36
+ } else {
37
+ if (response.status >= 400 && response.status < 500) {
38
+ window.location.href = "/400.html"
39
+ return
40
+ }
41
+ if (response.status >= 500) {
42
+ window.location.href = "/500.html"
43
+ return
44
+ }
45
+ }
46
+ })
47
+ }
6
48
 
7
49
  return { visit: appVisit, remote: appRemote }
8
50
  }
@@ -10,28 +10,61 @@ module Terrazzo
10
10
 
11
11
  def insert_routes
12
12
  namespace_name = options[:namespace]
13
- route_content = <<~RUBY.indent(2)
13
+ models = application_models
14
+
15
+ # Group models by their module namespace
16
+ namespaced = {}
17
+ top_level = []
18
+
19
+ models.each do |model|
20
+ parts = model.name.split("::")
21
+ if parts.size > 1
22
+ ns = parts[0..-2].join("::").underscore
23
+ resource = parts.last.underscore.pluralize
24
+ namespaced[ns] ||= []
25
+ namespaced[ns] << resource
26
+ else
27
+ top_level << model.model_name.plural
28
+ end
29
+ end
30
+
31
+ lines = []
32
+ top_level.sort.each { |r| lines << " resources :#{r}" }
33
+
34
+ namespaced.sort.each do |ns, resources|
35
+ lines << ""
36
+ lines << " namespace :#{ns} do"
37
+ resources.sort.each { |r| lines << " resources :#{r}" }
38
+ lines << " end"
39
+ end
40
+
41
+ first_resource = top_level.sort.first || namespaced.values.flatten.first || "dashboard"
42
+
43
+ route_block = <<~RUBY.indent(2)
14
44
  namespace :#{namespace_name} do
45
+ #{lines.join("\n")}
46
+
15
47
  root to: "#{first_resource}#index"
16
48
  end
17
49
  RUBY
18
50
 
19
- route route_content.strip
51
+ route route_block.strip
20
52
  end
21
53
 
22
54
  private
23
55
 
24
- def first_resource
25
- # Default to "customers" if no models found
26
- models = application_models
27
- models.any? ? models.first.model_name.plural : "dashboard"
28
- end
29
-
30
56
  def application_models
31
- Rails.application.eager_load! if defined?(Rails)
32
- ApplicationRecord.descendants.reject(&:abstract_class?)
33
- rescue
34
- []
57
+ models_path = Rails.root.join("app", "models")
58
+ return [] unless models_path.exist?
59
+
60
+ Dir[models_path.join("**", "*.rb")].filter_map do |file|
61
+ relative = Pathname.new(file).relative_path_from(models_path).to_s
62
+ next if relative.start_with?("concerns/")
63
+ next if relative == "application_record.rb"
64
+
65
+ relative.delete_suffix(".rb").camelize.safe_constantize
66
+ end.select { |klass| klass < ApplicationRecord && !klass.abstract_class? }
67
+ .sort_by { |klass| klass.name }
35
68
  end
36
69
  end
37
70
  end
@@ -21,3 +21,5 @@ export function Layout({ navigation, title, actions, children }) {
21
21
  </SidebarProvider>);
22
22
 
23
23
  }
24
+
25
+ export default Layout;
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
2
+ import { ChevronUp, ChevronDown, ChevronsUpDown } from "lucide-react";
3
3
 
4
4
  import { TableHead } from "./ui/table";
5
5
 
@@ -17,11 +17,11 @@ export function SortableHeader({ label, sortable, sortUrl, sortDirection }) {
17
17
 
18
18
  {label}
19
19
  {sortDirection === "asc" ?
20
- <ArrowUp className="h-4 w-4" /> :
20
+ <ChevronUp className="h-4 w-4" /> :
21
21
  sortDirection === "desc" ?
22
- <ArrowDown className="h-4 w-4" /> :
22
+ <ChevronDown className="h-4 w-4" /> :
23
23
 
24
- <ArrowUpDown className="h-4 w-4" />
24
+ <ChevronsUpDown className="h-4 w-4" />
25
25
  }
26
26
  </a>
27
27
  </TableHead>);
@@ -63,7 +63,9 @@ module Terrazzo
63
63
  end
64
64
 
65
65
  def collection_includes
66
+ collection_attr_set = Set.new(collection_attributes)
66
67
  attribute_types.each_with_object([]) do |(attr, type), includes|
68
+ next unless collection_attr_set.include?(attr)
67
69
  next unless type.eager_load?
68
70
  next unless self.class.model.reflect_on_association(attr)
69
71
  includes << attr
@@ -17,5 +17,11 @@ module Terrazzo
17
17
  app.config.superglue = ActiveSupport::OrderedOptions.new unless app.config.respond_to?(:superglue)
18
18
  app.config.superglue.auto_include = false
19
19
  end
20
+
21
+ initializer "terrazzo.uses_superglue" do
22
+ ActiveSupport.on_load(:action_controller_base) do
23
+ extend Terrazzo::UsesSuperglue
24
+ end
25
+ end
20
26
  end
21
27
  end
@@ -107,7 +107,7 @@ module Terrazzo
107
107
  private
108
108
 
109
109
  def resolve_data
110
- resource.public_send(attribute)
110
+ resource.try(attribute)
111
111
  end
112
112
  end
113
113
  end
@@ -19,11 +19,11 @@ module Terrazzo
19
19
  end
20
20
 
21
21
  def searchable?
22
- deferred_class.searchable?
22
+ options.key?(:searchable) ? options[:searchable] : deferred_class.searchable?
23
23
  end
24
24
 
25
25
  def sortable?
26
- deferred_class.sortable?
26
+ options.key?(:sortable) ? options[:sortable] : deferred_class.sortable?
27
27
  end
28
28
 
29
29
  def eager_load?
@@ -4,12 +4,6 @@ module Terrazzo
4
4
  def serialize_value(_mode)
5
5
  data
6
6
  end
7
-
8
- class << self
9
- def searchable?
10
- true
11
- end
12
- end
13
7
  end
14
8
  end
15
9
  end
@@ -14,10 +14,6 @@ module Terrazzo
14
14
  end
15
15
 
16
16
  class << self
17
- def searchable?
18
- true
19
- end
20
-
21
17
  def default_options
22
18
  { truncate: 50 }
23
19
  end
@@ -14,10 +14,6 @@ module Terrazzo
14
14
  end
15
15
 
16
16
  class << self
17
- def searchable?
18
- true
19
- end
20
-
21
17
  def default_options
22
18
  { truncate: 100 }
23
19
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Terrazzo
4
+ module UsesSuperglue
5
+ # Prepended to fix a template shadowing problem.
6
+ #
7
+ # When a controller inherits a long view prefix chain (e.g. from Devise or
8
+ # other engines), Superglue's _render_template calls template_exists? with
9
+ # all those prefixes and may find a gem-supplied HTML template, causing it
10
+ # to render the unstyled ERB instead of the React page. This override
11
+ # restricts the template existence check to the app's own view path only.
12
+ module TemplateLookupOverride
13
+ def _render_template(options = {})
14
+ if @_capture_options_before_render
15
+ @_capture_options_before_render = false
16
+ @_render_options = options
17
+ _ensure_react_page!(options[:template], options[:prefixes])
18
+
19
+ app_views = Rails.root.join("app/views").to_s
20
+ app_resolver = view_paths.to_a.find { |vp| vp.to_s == app_views && !vp.is_a?(Superglue::Resolver) }
21
+ prefixes = Array(options[:prefixes]).compact
22
+ html_template_exist = app_resolver&.find_all(options[:template], prefixes, false, { formats: [:html], locale: [], handlers: [], variants: [] }, nil, [])&.any?
23
+ if !html_template_exist
24
+ super(options.merge(template: _superglue_template, prefixes: []))
25
+ else
26
+ super
27
+ end
28
+ else
29
+ super
30
+ end
31
+ end
32
+ end
33
+
34
+ def uses_superglue
35
+ include Superglue::Controller
36
+ prepend_view_path(Superglue::Resolver.new(Rails.root.join("app/views")))
37
+ before_action :use_jsx_rendering_defaults
38
+ prepend TemplateLookupOverride
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,3 @@
1
1
  module Terrazzo
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
data/lib/terrazzo.rb CHANGED
@@ -13,6 +13,7 @@ module Terrazzo
13
13
  autoload :Filter, "terrazzo/filter"
14
14
  autoload :Namespace, "terrazzo/namespace"
15
15
  autoload :GeneratorHelpers, "terrazzo/generator_helpers"
16
+ autoload :UsesSuperglue, "terrazzo/uses_superglue"
16
17
 
17
18
  module Field
18
19
  autoload :Base, "terrazzo/field/base"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: terrazzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Terrazzo Contributors
@@ -132,6 +132,7 @@ files:
132
132
  - lib/generators/terrazzo/field/templates/ShowField.jsx.erb
133
133
  - lib/generators/terrazzo/field/templates/field.rb.erb
134
134
  - lib/generators/terrazzo/install/install_generator.rb
135
+ - lib/generators/terrazzo/install/templates/admin.css
135
136
  - lib/generators/terrazzo/install/templates/application.js.erb
136
137
  - lib/generators/terrazzo/install/templates/application.json.props
137
138
  - lib/generators/terrazzo/install/templates/application.json.props.erb
@@ -271,6 +272,7 @@ files:
271
272
  - lib/terrazzo/page/show.rb
272
273
  - lib/terrazzo/resource_resolver.rb
273
274
  - lib/terrazzo/search.rb
275
+ - lib/terrazzo/uses_superglue.rb
274
276
  - lib/terrazzo/version.rb
275
277
  - terrazzo.gemspec
276
278
  homepage: https://github.com/wengzilla/terrazzo