terrazzo 0.5.2 → 0.5.4

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: c8d1fb21a6af1c708437891c1e3216a2749fb682513c40c4c583a950e92750f5
4
- data.tar.gz: 47a449b824bf9f5a2cf00e7619aa5d008526f7b8d764258022a3d3ff43109c58
3
+ metadata.gz: 100223520cc1a1dbedef6413a2ba4e1b45691f4773abddd9ee27d123e735c64d
4
+ data.tar.gz: eeee21e64365ca82bc7c6f11c2f80dd813b2960d96e1e3aa80839fd7acb2b2cf
5
5
  SHA512:
6
- metadata.gz: e3970087e358e8ef4988ff18d3dff5c9a28de0253f5358afb0b54aeada98878ac16ad2b1f85a057e0c101bea47a74d6c79a63c259781571cc1b7d95327712bf8
7
- data.tar.gz: c85f00757f7f54a86fa37d7f9509e109f672cf074eb9e88370132e74d5ccbc1e4bf0345c9410e396f85770a9b936658f5f637554e94bf7a7de46fc379c901e23
6
+ metadata.gz: f44edde4138e7348cfaa6abd5c790c8d295f40ed57fd7391c6e1af6a97a2f334bf73e147e4322e37138b5a72ef4743be7c0346448defef4c8d0becaf1daf8d17
7
+ data.tar.gz: 344144b82879ebe42857dbf048b9737e429fba63d06a06aa5ef5b2aa1ff3accef8633efb11c12835fca3a384ef06b162cf5c78892db4ffbc8e046598b2eaf311
@@ -44,7 +44,10 @@ module Terrazzo
44
44
 
45
45
  def show
46
46
  @resource = find_resource(params[:id])
47
- @page = Terrazzo::Page::Show.new(dashboard, @resource)
47
+ @page = Terrazzo::Page::Show.new(
48
+ dashboard, @resource,
49
+ has_many_params: Terrazzo::HasManyPagination.extract(params)
50
+ )
48
51
  end
49
52
 
50
53
  def new
@@ -9,6 +9,21 @@ module Terrazzo
9
9
  end
10
10
  end
11
11
 
12
+ def has_many_pagination_paths(field, resource)
13
+ param_key = Terrazzo::HasManyPagination.param_key(field.attribute)
14
+ base = request.query_parameters.merge(
15
+ only_path: true,
16
+ controller: controller_path,
17
+ action: :show,
18
+ id: resource.to_param,
19
+ format: nil
20
+ )
21
+ {
22
+ prevPagePath: (field.current_page > 1 ? url_for(base.merge(param_key => field.current_page - 1)) : nil),
23
+ nextPagePath: (field.current_page < field.total_pages ? url_for(base.merge(param_key => field.current_page + 1)) : nil)
24
+ }
25
+ end
26
+
12
27
  private
13
28
 
14
29
  def default_collection_item_actions(resource)
@@ -60,12 +60,12 @@ json.pagination do
60
60
  if @resources.next_page
61
61
  json.nextPagePath url_for(
62
62
  _page: @resources.next_page,
63
+ per_page: params[:per_page],
63
64
  search: @search_term,
64
65
  order: params[:order],
65
66
  direction: params[:direction],
66
67
  filter: @active_filter,
67
68
  filter_value: @filter_value,
68
- props_at: "data.pagination",
69
69
  only_path: true
70
70
  )
71
71
  else
@@ -74,12 +74,12 @@ json.pagination do
74
74
  if @resources.prev_page
75
75
  json.prevPagePath url_for(
76
76
  _page: @resources.prev_page,
77
+ per_page: params[:per_page],
77
78
  search: @search_term,
78
79
  order: params[:order],
79
80
  direction: params[:direction],
80
81
  filter: @active_filter,
81
82
  filter_value: @filter_value,
82
- props_at: "data.pagination",
83
83
  only_path: true
84
84
  )
85
85
  else
@@ -9,12 +9,15 @@ show_field_json = ->(json, field) do
9
9
 
10
10
  if field.class.associative? && field.data.present?
11
11
  if field.is_a?(Terrazzo::Field::HasMany)
12
- json.itemShowPaths(field.data.each_with_object({}) do |record, paths|
13
- paths[record.id.to_s] = polymorphic_path([namespace, record]) rescue nil
14
- end)
15
- json.collectionItemActions(field.data.each_with_object({}) do |record, hash|
16
- hash[record.id.to_s] = collection_item_actions(record)
12
+ render_actions = field.options.fetch(:render_actions, true)
13
+
14
+ json.hasManyRowExtras(field.page_records.each_with_object({}) do |record, hash|
15
+ extras = { showPath: (polymorphic_path([namespace, record]) rescue nil) }
16
+ extras[:collectionItemActions] = collection_item_actions(record) if render_actions
17
+ hash[record.id.to_s] = extras
17
18
  end)
19
+
20
+ json.paginationPaths has_many_pagination_paths(field, @resource)
18
21
  else
19
22
  json.showPath polymorphic_path([namespace, field.data]) rescue nil
20
23
  end
@@ -1,85 +1,36 @@
1
- import React, { useState, useContext } from "react";
2
- import { NavigationContext } from "@thoughtbot/superglue";
1
+ import React, { useState } from "react";
3
2
 
4
- import {
5
- Table,
6
- TableHeader,
7
- TableBody,
8
- TableRow,
9
- TableHead,
10
- TableCell,
11
- } from "terrazzo/ui";
3
+ import { ResourceTable } from "terrazzo/components";
12
4
  import { Badge } from "terrazzo/ui";
13
5
  import { Button } from "terrazzo/ui";
14
- import { CollectionItemActions } from "terrazzo/components";
15
- import { FieldRenderer } from "../FieldRenderer";
16
6
 
17
- export function ShowField({ value, itemShowPaths, collectionItemActions }) {
7
+ export function ShowField({ value, hasManyRowExtras, options }) {
18
8
  if (!value) return <span className="text-muted-foreground">None</span>;
19
9
 
20
- const { items, headers, total, initialLimit } = value;
10
+ const { rows, headers, total, initialLimit, items } = value;
21
11
  const [expanded, setExpanded] = useState(false);
22
- const { visit } = useContext(NavigationContext);
23
12
 
24
- if (!items || items.length === 0) {
25
- return <span className="text-muted-foreground">None</span>;
26
- }
13
+ // Table mode: collection_attributes specified
14
+ if (headers && rows) {
15
+ if (rows.length === 0) {
16
+ return <span className="text-muted-foreground">None</span>;
17
+ }
27
18
 
28
- const pathFor = (id) => itemShowPaths?.[String(id)];
29
- const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
30
- const visibleItems = expanded || !hasMore ? items : items.slice(0, initialLimit);
19
+ const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
20
+ const visibleRows = expanded || !hasMore ? rows : rows.slice(0, initialLimit);
31
21
 
32
- const handleRowClick = (e, showPath) => {
33
- if (!showPath) return;
34
- if (e.target.closest("a, button, form")) return;
35
- if (window.getSelection().toString()) return;
36
- visit(showPath, {});
37
- };
22
+ const enrichedRows = visibleRows.map((row) => {
23
+ const extras = hasManyRowExtras?.[String(row.id)] || {};
24
+ return {
25
+ ...row,
26
+ showPath: extras.showPath,
27
+ collectionItemActions: extras.collectionItemActions,
28
+ };
29
+ });
38
30
 
39
- // Table mode: collection_attributes specified
40
- if (headers) {
41
31
  return (
42
32
  <div>
43
- <div className="rounded-md border">
44
- <Table>
45
- <TableHeader>
46
- <TableRow>
47
- {headers.map((header) =>
48
- <TableHead key={header.attribute}>{header.label}</TableHead>
49
- )}
50
- {collectionItemActions && <TableHead></TableHead>}
51
- </TableRow>
52
- </TableHeader>
53
- <TableBody>
54
- {visibleItems.map((item) => {
55
- const showPath = pathFor(item.id);
56
- return (
57
- <TableRow
58
- key={item.id}
59
- className={showPath ? "cursor-pointer" : ""}
60
- onClick={(e) => handleRowClick(e, showPath)}>
61
- {item.columns.map((col, colIndex) =>
62
- <TableCell key={col.attribute}>
63
- {showPath && colIndex === 0 ? (
64
- <a href={showPath} data-sg-visit className="hover:underline">
65
- <FieldRenderer mode="index" {...col} />
66
- </a>
67
- ) : (
68
- <FieldRenderer mode="index" {...col} />
69
- )}
70
- </TableCell>
71
- )}
72
- {collectionItemActions && (
73
- <TableCell>
74
- <CollectionItemActions actions={collectionItemActions?.[String(item.id)]} />
75
- </TableCell>
76
- )}
77
- </TableRow>
78
- );
79
- })}
80
- </TableBody>
81
- </Table>
82
- </div>
33
+ <ResourceTable headers={headers} rows={enrichedRows} showActions={options?.renderActions !== false} />
83
34
  {hasMore && (
84
35
  <Button
85
36
  variant="link"
@@ -93,12 +44,19 @@ export function ShowField({ value, itemShowPaths, collectionItemActions }) {
93
44
  );
94
45
  }
95
46
 
96
- // Simple list mode
47
+ // Simple list mode (no collection_attributes)
48
+ if (!items || items.length === 0) {
49
+ return <span className="text-muted-foreground">None</span>;
50
+ }
51
+
52
+ const hasMore = initialLimit && initialLimit > 0 && total > initialLimit;
53
+ const visibleItems = expanded || !hasMore ? items : items.slice(0, initialLimit);
54
+
97
55
  return (
98
56
  <div>
99
57
  <div className="flex flex-wrap items-center gap-1.5">
100
58
  {visibleItems.map((item) => {
101
- const showPath = pathFor(item.id);
59
+ const showPath = hasManyRowExtras?.[String(item.id)]?.showPath;
102
60
  return showPath ? (
103
61
  <a key={item.id} href={showPath} data-sg-visit>
104
62
  <Badge variant="secondary" className="cursor-pointer">{item.display}</Badge>
@@ -1,55 +1,11 @@
1
- import React, { useContext } from "react";
2
- import { NavigationContext } from "@thoughtbot/superglue";
1
+ import React from "react";
3
2
 
4
- import { SortableHeader, CollectionItemActions } from "terrazzo/components";
5
- import { FieldRenderer } from "terrazzo/fields";
6
- import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "terrazzo/ui";
3
+ import { ResourceTable } from "terrazzo/components";
7
4
 
8
5
  export function AdminCollection({ table }) {
9
- const { visit } = useContext(NavigationContext);
10
-
11
- const handleRowClick = (e, showPath) => {
12
- if (!showPath) return;
13
- if (e.target.closest("a, button, form")) return;
14
- if (window.getSelection().toString()) return;
15
- visit(showPath, {});
16
- };
17
-
18
6
  return (
19
- <div className="overflow-x-auto rounded-md border">
20
- <Table>
21
- <TableHeader>
22
- <TableRow>
23
- {table.headers.map((header) =>
24
- <SortableHeader key={header.attribute} {...header} />
25
- )}
26
- <TableHead className="w-[120px]">Actions</TableHead>
27
- </TableRow>
28
- </TableHeader>
29
- <TableBody>
30
- {table.rows.map((row) =>
31
- <TableRow
32
- key={row.id}
33
- className={row.showPath ? "cursor-pointer" : ""}
34
- onClick={(e) => handleRowClick(e, row.showPath)}>
35
- {row.cells.map((cell) =>
36
- <TableCell key={cell.attribute}>
37
- {cell.showPath ? (
38
- <a href={cell.showPath} data-sg-visit className="hover:underline">
39
- <FieldRenderer mode="index" {...cell} />
40
- </a>
41
- ) : (
42
- <FieldRenderer mode="index" {...cell} />
43
- )}
44
- </TableCell>
45
- )}
46
- <TableCell>
47
- <CollectionItemActions actions={row.collectionItemActions} />
48
- </TableCell>
49
- </TableRow>
50
- )}
51
- </TableBody>
52
- </Table>
7
+ <div className="overflow-x-auto">
8
+ <ResourceTable headers={table.headers} rows={table.rows} />
53
9
  </div>
54
10
  );
55
11
  }
@@ -2,6 +2,14 @@ module Terrazzo
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace Terrazzo
4
4
 
5
+ initializer "terrazzo.props_searcher_partial" do
6
+ Props::Searcher.class_eval do
7
+ def partial!(**options)
8
+ @context.render options
9
+ end
10
+ end
11
+ end
12
+
5
13
  initializer "terrazzo.i18n" do
6
14
  Terrazzo::Engine.root.glob("config/locales/**/*.yml").each do |locale|
7
15
  I18n.load_path << locale unless I18n.load_path.include?(locale)
@@ -20,8 +20,14 @@ module Terrazzo
20
20
  end
21
21
 
22
22
  def serializable_options(page = nil)
23
- return {} unless page == :form && resource
24
- { resourceOptions: resource_options }
23
+ opts = {}
24
+ if page == :form && resource
25
+ opts[:resourceOptions] = resource_options
26
+ end
27
+ if options.key?(:render_actions)
28
+ opts[:renderActions] = options[:render_actions]
29
+ end
30
+ opts
25
31
  end
26
32
 
27
33
  class << self
@@ -30,7 +36,7 @@ module Terrazzo
30
36
  end
31
37
 
32
38
  def default_options
33
- { limit: 5 }
39
+ {}
34
40
  end
35
41
 
36
42
  def permitted_attribute(attr, _options = {})
@@ -38,22 +44,53 @@ module Terrazzo
38
44
  end
39
45
  end
40
46
 
47
+ def per_page
48
+ (options[:per_page] || options[:limit] || 5).to_i
49
+ end
50
+
51
+ def current_page
52
+ p = options[:_page].to_i
53
+ p < 1 ? 1 : p
54
+ end
55
+
56
+ def total_count
57
+ paginated.total_count
58
+ end
59
+
60
+ def total_pages
61
+ [paginated.total_pages, 1].max
62
+ end
63
+
64
+ def page_records
65
+ paginated.to_a
66
+ end
67
+
41
68
  private
42
69
 
70
+ def paginated
71
+ @paginated ||= begin
72
+ scope = apply_sorting(data)
73
+ scope = Kaminari.paginate_array(scope.to_a) unless scope.respond_to?(:page)
74
+ scope.page(current_page).per(per_page)
75
+ end
76
+ end
77
+
43
78
  def serialize_show_value
44
- limit = options.fetch(:limit, 5)
45
- records = apply_sorting(data)
46
- all_records = records.to_a
47
- total = all_records.size
48
79
  col_attrs = options[:collection_attributes] || resolve_default_collection_attributes
49
80
 
81
+ meta = {
82
+ total: total_count,
83
+ perPage: per_page,
84
+ currentPage: current_page,
85
+ totalPages: total_pages,
86
+ }
87
+
50
88
  if col_attrs
51
- serialize_with_collection_attributes(all_records, col_attrs, total, limit)
89
+ { **serialize_with_collection_attributes(page_records, col_attrs), **meta }
52
90
  else
53
91
  {
54
- items: all_records.map { |r| { id: r.id.to_s, display: display_name(r) } },
55
- total: total,
56
- initialLimit: limit
92
+ items: page_records.map { |r| { id: r.id.to_s, display: display_name(r) } },
93
+ **meta,
57
94
  }
58
95
  end
59
96
  end
@@ -65,15 +102,15 @@ module Terrazzo
65
102
  nil
66
103
  end
67
104
 
68
- def serialize_with_collection_attributes(records, col_attrs, total, limit)
105
+ def serialize_with_collection_attributes(records, col_attrs)
69
106
  dashboard_class = find_associated_dashboard
70
107
 
71
108
  headers = col_attrs.map do |attr|
72
109
  { attribute: attr.to_s, label: attr.to_s.humanize }
73
110
  end
74
111
 
75
- items = records.map do |record|
76
- columns = col_attrs.map do |attr|
112
+ rows = records.map do |record|
113
+ cells = col_attrs.map do |attr|
77
114
  field = dashboard_class.new.attribute_type_for(attr).new(attr, nil, :index, resource: record)
78
115
  {
79
116
  attribute: attr.to_s,
@@ -81,15 +118,10 @@ module Terrazzo
81
118
  value: field.serialize_value(:index)
82
119
  }
83
120
  end
84
- { id: record.id.to_s, columns: columns }
121
+ { id: record.id.to_s, cells: cells }
85
122
  end
86
123
 
87
- {
88
- headers: headers,
89
- items: items,
90
- total: total,
91
- initialLimit: limit
92
- }
124
+ { headers: headers, rows: rows }
93
125
  end
94
126
 
95
127
  def resource_options
@@ -0,0 +1,24 @@
1
+ module Terrazzo
2
+ module HasManyPagination
3
+ PARAM_PREFIX = "hm_".freeze
4
+ PARAM_SUFFIX = "_page".freeze
5
+
6
+ module_function
7
+
8
+ def extract(params)
9
+ result = {}
10
+ params.each do |key, value|
11
+ key_s = key.to_s
12
+ next unless key_s.start_with?(PARAM_PREFIX) && key_s.end_with?(PARAM_SUFFIX)
13
+ attr = key_s[PARAM_PREFIX.length...-PARAM_SUFFIX.length]
14
+ next if attr.empty?
15
+ result[attr.to_sym] = { _page: value.to_i }
16
+ end
17
+ result
18
+ end
19
+
20
+ def param_key(attribute)
21
+ "#{PARAM_PREFIX}#{attribute}#{PARAM_SUFFIX}"
22
+ end
23
+ end
24
+ end
@@ -3,9 +3,10 @@ module Terrazzo
3
3
  class Show < Base
4
4
  attr_reader :resource
5
5
 
6
- def initialize(dashboard, resource)
6
+ def initialize(dashboard, resource, has_many_params: {})
7
7
  super(dashboard, resource.class)
8
8
  @resource = resource
9
+ @has_many_params = has_many_params || {}
9
10
  end
10
11
 
11
12
  def page_title
@@ -20,24 +21,29 @@ module Terrazzo
20
21
  def attributes
21
22
  attrs = dashboard.show_page_attributes
22
23
  dashboard.flatten_attributes(attrs).map do |attr|
23
- dashboard.attribute_type_for(attr).new(attr, nil, :show, resource: resource)
24
+ build_field(attr, :show)
24
25
  end
25
26
  end
26
27
 
27
28
  private
28
29
 
30
+ def build_field(attr, mode)
31
+ extra = @has_many_params[attr.to_sym] || {}
32
+ dashboard.attribute_type_for(attr).new(attr, nil, mode, resource: resource, options: extra)
33
+ end
34
+
29
35
  def normalize_groups(attrs, mode)
30
36
  if attrs.is_a?(Hash)
31
37
  attrs.map do |group_name, fields|
32
38
  {
33
39
  name: group_name,
34
- fields: fields.map { |attr| dashboard.attribute_type_for(attr).new(attr, nil, mode, resource: resource) }
40
+ fields: fields.map { |attr| build_field(attr, mode) }
35
41
  }
36
42
  end
37
43
  else
38
44
  [{
39
45
  name: "",
40
- fields: attrs.map { |attr| dashboard.attribute_type_for(attr).new(attr, nil, mode, resource: resource) }
46
+ fields: attrs.map { |attr| build_field(attr, mode) }
41
47
  }]
42
48
  end
43
49
  end
@@ -1,3 +1,3 @@
1
1
  module Terrazzo
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.4"
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 :HasManyPagination, "terrazzo/has_many_pagination"
16
17
  autoload :UsesSuperglue, "terrazzo/uses_superglue"
17
18
 
18
19
  module Field
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.5.2
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Terrazzo Contributors
@@ -273,6 +273,7 @@ files:
273
273
  - lib/terrazzo/field/url.rb
274
274
  - lib/terrazzo/filter.rb
275
275
  - lib/terrazzo/generator_helpers.rb
276
+ - lib/terrazzo/has_many_pagination.rb
276
277
  - lib/terrazzo/namespace.rb
277
278
  - lib/terrazzo/namespace/resource.rb
278
279
  - lib/terrazzo/not_authorized_error.rb