diff options
author | groditi <groditi@03d0b0b2-0e1a-0410-a411-fdb2f4bd65d7> | 2007-10-02 20:05:44 +0000 |
---|---|---|
committer | groditi <groditi@03d0b0b2-0e1a-0410-a411-fdb2f4bd65d7> | 2007-10-02 20:05:44 +0000 |
commit | e22de1011c40b639cc4b6e5586a9e0defe855285 (patch) | |
tree | 67e007d3eee18254d0f9e3111acaedafd80e80dd | |
parent | 6ab43711ccd779eedce107001b300043e2056a0c (diff) | |
download | reaction-e22de1011c40b639cc4b6e5586a9e0defe855285.tar.gz reaction-e22de1011c40b639cc4b6e5586a9e0defe855285.zip |
unstable switchover before template renaming. added searchpath for widgets trying to make list view work. added gridview
33 files changed, 904 insertions, 527 deletions
diff --git a/lib/ComponentUI/View/Site.pm b/lib/ComponentUI/View/Site.pm index cbb2d28..fa89c0a 100644 --- a/lib/ComponentUI/View/Site.pm +++ b/lib/ComponentUI/View/Site.pm @@ -7,6 +7,9 @@ class Site is TT, which { }; +1; + +__END__; use Class::MOP; diff --git a/lib/Reaction/Meta/Attribute.pm b/lib/Reaction/Meta/Attribute.pm index 38035d5..9f444df 100644 --- a/lib/Reaction/Meta/Attribute.pm +++ b/lib/Reaction/Meta/Attribute.pm @@ -30,6 +30,9 @@ around _process_options => sub { $options->{default} = $fail ? sub { confess "${name} must be provided before calling reader" } : sub{ shift->$builder }; + + $options->{clearer} ||= ($name =~ /^_/) ? "_clear${name}" : "clear_${name}" + if $build; } #we are using this everywhere so might as well move it here. diff --git a/lib/Reaction/UI/LayoutSet.pm b/lib/Reaction/UI/LayoutSet.pm index 1cdb0e9..b19c92b 100644 --- a/lib/Reaction/UI/LayoutSet.pm +++ b/lib/Reaction/UI/LayoutSet.pm @@ -10,15 +10,19 @@ class LayoutSet which { has 'name' => (is => 'ro', required => 1); has 'source_file' => (is => 'rw', lazy_fail => 1); + has 'file_extension'=> (isa => 'Str', is => 'rw', lazy_build => 1); + + implements build_file_extension => as { 'html' }; implements 'BUILD' => as { my ($self, $args) = @_; my @path = @{$args->{search_path}||[]}; confess "No search_path provided" unless @path; my $found; + my $ext = $self->file_extension; SEARCH: foreach my $path (@path) { - my $cand = $path->file($self->name); - print STDERR $cand,"\n"; + my $cand = $path->file($self->name . ".${ext}"); + #print STDERR $cand,"\n"; if ($cand->stat) { $self->_load_file($cand); $found = 1; @@ -48,7 +52,7 @@ class LayoutSet which { my $widget = join('', map { ucfirst($_) } split('_', $self->name)); $widget = join('::', map { ucfirst($_) } split('/', $widget)); - print STDERR "--- ", $self->name, " maps to widget $widget \n"; + #print STDERR "--- ", $self->name, " maps to widget $widget \n"; return $widget; }; diff --git a/lib/Reaction/UI/LayoutSet/TT.pm b/lib/Reaction/UI/LayoutSet/TT.pm index 02b3cde..b77d0d5 100644 --- a/lib/Reaction/UI/LayoutSet/TT.pm +++ b/lib/Reaction/UI/LayoutSet/TT.pm @@ -8,6 +8,8 @@ class TT is LayoutSet, which { has 'tt_view' => (is => 'rw', isa => View, lazy_fail => 1); + implements build_file_extension => as { 'tt' }; + implements 'BUILD' => as { my ($self, $args) = @_; diff --git a/lib/Reaction/UI/View.pm b/lib/Reaction/UI/View.pm index 987909c..081a16f 100644 --- a/lib/Reaction/UI/View.pm +++ b/lib/Reaction/UI/View.pm @@ -26,6 +26,7 @@ class View which { sub BUILD{ my $self = shift; my $skin_name = $self->skin_name; + #XXX i guess we will add the path to installed reaction templates here my $skin_path = $self->app->path_to('share','skin',$skin_name); confess("'${skin_path}' is not a valid path for skin '${skin_name}'") unless -d $skin_path; @@ -58,15 +59,21 @@ class View which { my ($self, $layout_set) = @_; my $base = $self->blessed; my $tail = $layout_set->widget_type; - my $class = join('::', $base, 'Widget', $tail); - eval { Class::MOP::load_class($class) }; - confess "Couldn't load widget '$class': $@" if $@; - return $class; + # eventually more stuff will go here i guess? + my $app_name = ref $self->app || $self->app; + + my @search_path = ($base, $app_name, 'Reaction::UI'); + my @haystack = map { join '::', $_, 'Widget', $tail } @search_path; + for my $class (@haystack){ + eval { Class::MOP::load_class($class) }; + $@ ? next : return $class; + } + confess "Couldn't load widget '$tail': tried: @haystack"; }; implements 'layout_set_for' => as { my ($self, $vp) = @_; - print STDERR "Getting layoutset for VP ".(ref($vp) || "SC:".$vp)."\n"; + #print STDERR "Getting layoutset for VP ".(ref($vp) || "SC:".$vp)."\n"; my $lset_name = eval { $vp->layout }; confess "Couldn't call layout method on \$vp arg ${vp}: $@" if $@; unless (length($lset_name)) { @@ -75,7 +82,7 @@ class View which { my @fragments = split('::', $last); $_ = join("_", split(/(?=[A-Z])/, $_)) for @fragments; $lset_name = lc(join('/', @fragments)); - print STDERR "--- $vp_class is rendered as $lset_name\n"; + #print STDERR "--- $vp_class is rendered as $lset_name\n"; } my $cache = $self->_layout_set_cache; return $cache->{$lset_name} ||= $self->create_layout_set($lset_name); diff --git a/lib/Reaction/UI/ViewPort/GridView.pm b/lib/Reaction/UI/ViewPort/GridView.pm new file mode 100644 index 0000000..542af62 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/GridView.pm @@ -0,0 +1,69 @@ +package Reaction::UI::ViewPort::GridView; + +use Reaction::Class; + +use aliased 'Reaction::UI::ViewPort::GridView::Row'; +use aliased 'Reaction::InterfaceModel::Collection'; + +class GridView is 'Reaction::UI::ViewPort', which { + + has exclude_columns => ( isa => 'ArrayRef', is => 'ro' ); + has column_names => ( isa => 'ArrayRef', is => 'ro', lazy_build => 1); + has rows => ( isa => 'ArrayRef', is => 'ro', lazy_build => 1); + has row_args => ( isa => 'HashRef', is => 'ro'); + + has collection => (isa => Collection, is => 'ro', required => 1); + has current_collection => (isa => Collection, is => 'rw', lazy_build => 1); + + implements build_rows => as{ + my $self = shift; + + my (@rows, $i); + for my $object ( $self->current_collection->members ){ + my $row = Row->new + ( + ctx => $self->ctx, + object => $object, + location => join('-', $self->location, 'row', ++$i), + column_order => $self->column_order, #XXX clean from ViewPort + exclude_fields => $self->exclude_columns || [], + $self->has_row_args ? %{ $self->row_args } : (), + + ); + push(@rows, $row); + } + return \@rows; + }; + + implements build_column_names => as { + my ($self) = @_; + my %excluded = map { $_ => undef } + @{ $self->has_exclude_columns ? $self->exclude_columns : [] }; + #XXX this abuse of '_im_class' needs to be fixed ASAP + my $object_class = $self->collection->_im_class; + my @fields = $object_class->meta->compute_all_applicable_attributes; + #eliminate excluded fields & treat names that start with an underscore as private + @fields = grep {$_->name !~ /^_/ && !exists $excluded{$_->name} } @fields; + #eliminate fields marked as collections, or fields that are arrayrefs + @fields = grep { + !($_->has_type_constraint && + ($_->type_constraint->is_a_type_of('ArrayRef') || + eval {$_->type_constraint->name->isa('Reaction::InterfaceModel::Collection')} || + eval { $_->_isa_metadata->isa('Reaction::InterfaceModel::Collection') } + ) + ) } @fields; + + #order the columns all nice and pretty, and only get fields with readers, duh + return $self->sort_by_spec + ( $self->column_order, [ map { (($_->get_read_method) || ()) } @fields] ); + }; + + implements build_current_collection => as { + shift->collection; + }; + +}; + + + +1; diff --git a/lib/Reaction/UI/ViewPort/GridView/Role/Order.pm b/lib/Reaction/UI/ViewPort/GridView/Role/Order.pm new file mode 100644 index 0000000..9684052 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/GridView/Role/Order.pm @@ -0,0 +1,29 @@ +package Reaction::UI::ViewPort::GridView::Role::Order; + +use Reaction::Role; + +role Order, which { + + has order_by => (isa => 'Str', is => 'rw', trigger_adopt('order_by')); + has order_by_desc => (isa => 'Int', is => 'rw', trigger_adopt('order_by'), lazy_build => 1); + + around build_current_collection => sub { + my $orig = shift; + my ($self) = @_; + my $collection = $orig->(@_); + my %attrs; + + #XXX DBICism that needs to be fixed + if ($self->has_order_by) { + $attrs{order_by} = $self->order_by; + $attrs{order_by} .= ' DESC' if ($self->order_by_desc); + } + + return $collection->where(undef, \%attrs); + }; + + around accept_events => sub { ('order_by', 'order_by_desc', shift->(@_)); }; + +}; + +1; diff --git a/lib/Reaction/UI/ViewPort/GridView/Role/Pager.pm b/lib/Reaction/UI/ViewPort/GridView/Role/Pager.pm new file mode 100644 index 0000000..276a413 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/GridView/Role/Pager.pm @@ -0,0 +1,36 @@ +package Reaction::UI::ViewPort::GridView::Role::Pager; + +use Reaction::Role; + +use aliased 'Reaction::InterfaceModel::Collection'; + +role Pager, which { + + has paged_collection => (isa => Collection, is => 'rw', lazy_build => 1); + + has pager => (isa => 'Data::Page', is => 'rw', lazy_build => 1); + has page => (isa => 'Int', is => 'rw', lazy_build => 1, trigger_adopt('page')); + has per_page => (isa => 'Int', is => 'rw', lazy_build => 1, trigger_adopt('page')); + + implements build_page => as { 1 }; + implements build_per_page => as { 10 }; + + implements build_pager => as { shift->paged_collection->pager }; + + implements adopt_page => as { + my ($self) = @_; + $self->clear_paged_collection; + $self->clear_pager; + }; + + around accept_events => sub { ('page', shift->(@_)); }; + + implements build_paged_collection => sub { + my ($self) = @_; + my $collection = $self->current_collection; + return $collection->where(undef, {rows => $self->per_page})->page($self->page); + }; + +}; + +1; diff --git a/lib/Reaction/UI/ViewPort/GridView/Row.pm b/lib/Reaction/UI/ViewPort/GridView/Row.pm new file mode 100644 index 0000000..2edb06a --- /dev/null +++ b/lib/Reaction/UI/ViewPort/GridView/Row.pm @@ -0,0 +1,110 @@ +package Reaction::UI::ViewPort::GridView::Row; + +use Reaction::Class; + +class Row is 'Reaction::UI::ViewPort::ObjectView', which { + + around build_fields_for_type_Num => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/number' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_Int => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/number' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_Bool => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/boolean' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + + around build_fields_for_type_Str => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/string' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_SimpleStr => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/string' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_Enum => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/string' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_DateTime => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/date_time' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_ArrayRef => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/list' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_Reaction_InterfaceModel_Collection => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/collection' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + + around build_fields_for_type_Reaction_InterfaceModel_Object => sub { + my ($orig, $self, $attr, $args) = @_; + $args->{Field}{$attr->name}{layout} = 'value/related_object' + unless( exists $args->{Field}{$attr->name} && + exists $args->{Field}{$attr->name}{layout} && + defined $args->{Field}{$attr->name}{layout} + ); + $orig->($self, $attr, $args); + }; + +}; + +1; diff --git a/lib/Reaction/UI/ViewPort/ListView.pm b/lib/Reaction/UI/ViewPort/ListView.pm index 08b7517..94e4de1 100644 --- a/lib/Reaction/UI/ViewPort/ListView.pm +++ b/lib/Reaction/UI/ViewPort/ListView.pm @@ -1,476 +1,12 @@ package Reaction::UI::ViewPort::ListView; use Reaction::Class; -use Data::Page; -use Text::CSV_XS; -use Scalar::Util qw/blessed/; -class ListView is 'Reaction::UI::ViewPort', which { - has collection => (isa => 'Reaction::InterfaceModel::Collection', - is => 'rw', required => 1); +class ListView is 'Reaction::UI::ViewPort::GridView', which { - has current_collection => ( - isa => 'Reaction::InterfaceModel::Collection', is => 'rw', - lazy_build => 1, clearer => 'clear_current_collection', - ); - - has current_page_collection => ( - isa => 'Reaction::InterfaceModel::Collection', is => 'rw', - lazy_build => 1, clearer => 'clear_current_page_collection', - ); - - has page => ( - isa => 'Int', is => 'rw', required => 1, - default => sub { 1 }, trigger_adopt('page'), - ); - - has pager => ( - isa => 'Data::Page', is => 'rw', - lazy_build => 1, clearer => 'clear_pager', - ); - - has per_page => ( - isa => 'Int', is => 'rw', predicate => 'has_per_page', - default => sub { 10 }, trigger_adopt('page'), - clearer => 'clear_per_page', - ); - - has field_names => (is => 'rw', isa => 'ArrayRef', lazy_build => 1); - - has field_label_map => (is => 'rw', isa => 'HashRef', lazy_build => 1); - - has order_by => ( - isa => 'Str', is => 'rw', predicate => 'has_order_by', - trigger_adopt('order_by') - ); - - has order_by_desc => ( - isa => 'Int', is => 'rw', default => sub { 0 }, - trigger_adopt('order_by') - ); - - has row_action_prototypes => (isa => 'ArrayRef', is => 'ro', lazy_build => 1); - - has exclude_columns => - ( is => 'rw', isa => 'ArrayRef', required => 1, default => sub{ [] } ); - - implements BUILD => as { - my ($self, $args) = @_; - if ($args->{unpaged}) { - $self->clear_per_page; - } - }; - - sub field_label { shift->field_label_map->{+shift}; } - - implements build_pager => as { - my ($self) = @_; - return $self->current_page_collection->pager; - }; - - implements adopt_page => as { - my ($self) = @_; - $self->clear_current_page_collection; - $self->clear_pager; - }; - - implements adopt_order_by => as { - my ($self) = @_; - $self->clear_current_collection; - $self->clear_current_page_collection; - }; - - implements build_current_collection => as { - my ($self) = @_; - my %attrs; - - #XXX DBICism that needs to be fixed - if ($self->has_order_by) { - $attrs{order_by} = $self->order_by; - if ($self->order_by_desc) { - $attrs{order_by} .= ' DESC'; - } - } - return $self->collection->where(undef, \%attrs); - }; - - implements build_current_page_collection => as { - my ($self) = @_; - my %attrs; - return $self->current_collection unless $self->has_per_page; - $attrs{rows} = $self->per_page; - return $self->current_collection->where(undef, \%attrs)->page($self->page); - }; - - implements all_current_rows => as { - return shift->current_collection->members; - }; - - implements current_rows => as { - return shift->current_page_collection->members; - }; - - implements build_field_names => as { - my ($self) = @_; - #XXX candidate for future optimization - my %excluded = map { $_ => undef } @{ $self->exclude_columns }; - - #XXX this abuse of '_im_class' needs to be fixed ASAP - my $object_class = $self->current_collection->_im_class; - my @fields = $object_class->meta->compute_all_applicable_attributes; - #eliminate excluded fields & treat names that start with an underscore as private - @fields = grep {$_->name !~ /^_/ && !exists $excluded{$_->name} } @fields; - #eliminate fields marked as collections, or fields that are arrayrefs - @fields = grep { - !($_->has_type_constraint && - ($_->type_constraint->is_a_type_of('ArrayRef') || - eval {$_->type_constraint->name->isa('Reaction::InterfaceModel::Collection')} || - eval { $_->_isa_metadata->isa('Reaction::InterfaceModel::Collection') } - ) - ) } @fields; - - #for(grep { $_->has_type_constraint } @fields){ - #my $tcname = $_->type_constraint->name; - #print STDERR $_->name, "\t", $tcname, "\n"; - #use Data::Dumper; - #print STDERR Dumper($_->type_constraint); - #} - - #order the columns all nice and pretty, and only get fields with readers, duh - return $self->sort_by_spec - ( $self->column_order, [ map { (($_->get_read_method) || ()) } @fields] ); - }; - - implements build_field_label_map => as { - my ($self) = @_; - my %labels; - foreach my $name (@{$self->field_names}) { - $labels{$name} = join(' ', map { ucfirst } split('_', $name)); - } - return \%labels; - }; - - #XXX this has to go soon, I recommend that Objects hold a registry of their actions - #and that they can be queried about it somehow - implements build_row_action_prototypes => as { - my $self = shift; - my $ctx = $self->ctx; - return [ - { label => 'View', action => sub { - [ '', 'view', [ @{$ctx->req->captures}, $_[0]->__id ] ] } }, - { label => 'Edit', action => sub { - [ '', 'update', [ @{$ctx->req->captures}, $_[0]->__id ] ] } }, - { label => 'Delete', action => sub { - [ '', 'delete', [ @{$ctx->req->captures}, $_[0]->__id ] ] } }, - ]; - }; - - implements row_actions_for => as { - my ($self, $row) = @_; - my @act; - my $c = $self->ctx; - foreach my $proto (@{$self->row_action_prototypes}) { - my %new = %$proto; - my ($c_name, $a_name, @rest) = @{delete($new{action})->($row)}; - $new{label} = delete($new{label})->($row) if ref $new{label} eq 'CODE'; - $new{uri} = $c->uri_for( - $c->controller($c_name)->action_for($a_name), - @rest - ); - push(@act, \%new); - } - return \@act; - }; - - implements export_to_csv => as { - my ($self) = @_; - my $csv = Text::CSV_XS->new( { binary => 1 } ); - my $output; - my $exporter = sub { - $csv->combine( @_ ); - $output .= $csv->string."\r\n"; - }; - $self->export_to_data($exporter); - my $res = $self->ctx->res; - $res->content_type('text/csv'); - my $path = $self->ctx->req->path; - my @parts = split(/\//, $path); - $res->header( - 'Content-disposition' => 'attachment; filename='.pop(@parts).'.csv' - ); - $res->body($output); - }; - - implements export_to_data => as { - my ($self, $exporter) = @_; - $self->export_header_data($exporter); - $self->export_body_data($exporter); - }; - - implements export_header_data => as { - my ($self, $exporter) = @_; - my @names = @{$self->field_names}; - my %labels = %{$self->field_label_map}; - $exporter->( map { $labels{$_} } @names ); - }; - - implements export_body_data => as { - my ($self, $exporter) = @_; - my @names = @{$self->field_names}; - foreach my $row ($self->all_current_rows) { - my @row_data; - foreach $_ (@names) { - my $data = $row->$_; - if (blessed($data) && $data->can("display_name")) { - $data = $data->display_name; - } - push(@row_data, $data); - } - $exporter->( @row_data ); - } - }; - - override accept_events => sub { ('page', 'order_by', 'order_by_desc', 'export_to_csv', super()); }; + does 'Reaction::UI::ViewPort::GridView::Role::Order'; + does 'Reaction::UI::ViewPort::GridView::Role::Pager'; }; 1; - -=head1 NAME - -Reaction::UI::ViewPort::ListView - Page layout block for rows of DBIx::Class::ResultSets - -=head1 SYNOPSIS - - # Create a new ListView - # $stack isa Reaction::UI::FocusStack object - # Assuming you have a DBIC model with an Actors table - my $lv = $stack->push_viewport( - 'Reaction::UI::ViewPort::ListView', - collection => $ctx->model('DBIC::Actors'), # a DBIx::Class::ResultSet - page => 1, # 1 is default - per_page => 10, # 10 is default - field_names => [qw/name age/], - field_label_map => { - 'name' => 'Name', - 'age' => 'Age', - }, - order_by => 'name', - ); - -=head1 DESCRIPTION - -Use this ViewPort to display the contents of a -L<DBIx::Class::ResultSet> as paged sets of rows. The default display -shows 10 rows per page, unsorted. - -TODO: Add a filter_by which allows us to restrict the content? -(Scenario: user has a paged display of data, user selects one value in -a column and clicks "filter by this value", and then only rows -containing that value are shown. - -=head1 ATTRIIBUTES - -=head2 collection - -This mandatory attribute must be an object derived from -L<DBIx::Class::ResultSet> representing the search result or result -source(Table) you wish to display in the ListView. - -The collection is used as the basis to create a refined set of data to -show in the current ListView, this is stored in -L<current_collection>. The data can further be refined and restricted -by passing in or later changing the L<order_by> or L<page> -attributes. The - -=head2 order_by - -A string representing the C<ORDER BY> part of the SQL statement, for -more info see L<DBIx::Class::ResultSet/Attributes> - -=head2 order_by_desc - -By default, sorting is done in ascending order, set this to true to -sort in descending order. Changing this attribute will cause the -L<current_collection> to be cleared and recreated on the next access . - -=head2 exclude_columns - - - -=head2 page - -The page number of the current search result, this will default to -1. If set explicitly on the ListView object, the current search result -and the pager will be cleared and recreated on the next access. - -=head2 per_page - -The number of rows of data to list on each page. Changing this value -on the ListView object will cause the L<current_page_collection> and -the L<pager> to be cleared and recreated on the next access. This will -default to 10 if unset. - -=head2 unpaged - -Set this to a true value if you really don't want your results shown -in pages. - -=head2 field_names - -An array reference of field names to show in the ListView. These must -exist as accessors in the L<DBIx::Class::ResultSource> describing the -L<DBIx::Class::ResultSet> passed to L<collection>. - -If not set, this will default to the list of attributes in the -L<DBIx::Class::ResultSource> which do not begin with an underscore, -and don't have a type of either ArrayRef or -C<DBIx::Class::ResultSet>. In short, all the non-private and -non-relation attributes. - -=head2 field_label_map - -A hash reference mapping the L<field_names> to the column labels used -to describe them in the ListView display. - -If not set, the label values will default to the L<field_names> with -the initial characters capitalised and underscores turned into spaces. - -=head2 row_action_prototypes - - row_action_prototypes => [ - { label => 'Edit', action => sub { [ '', 'update', [ $_[0]->id ] ] } }, - { label => 'Delete', action => sub { [ '', 'delete', [ $_[0]->id ] ] } }, - ]; - -Prototypes describing the actions that can be done on the rows of -ListView data. This is an array reference of hash refs describing the -name of each action with a C<label>, and the actual C<action> that -takes place. The code reference stored in the C<action > will be -called with a L<DBIx::Class::Row> object, it should return a list of a -L<Catalyst::Controller> name, the name of an action in that -controller, and any other parameters that need to be passed to -it. C<label> may be a scalar value or a code reference, in the later case -it will be called with the same parameters as C<action> and the return value -will be used as the C<label> value. - -The example above shows the default actions if this attribute is not set. - -=head2 current_collection - -This contains the currently used L<DBIx::Class::ResultSet> -representing the ListViews data, it is based on the L<collection> -ResultSet, refined using the L<order_by> and L<order_by_desc> attributes. - -The current_collection will be cleared and recreated if the -L<order_by> or L<order_by_desc> attributes are changed on the ListView -object. - -=head2 current_rows - -=head2 all_current_rows - -=head2 pager - -A L<Data::Page> object representing the data for the current search -result, it is cleared and reset when either L<page> or L<order_by> are -changed. - -=head2 current_page_collection - -This contains contains a single page of the contents of the -L<current_collection>, with the L<per_page> number of rows -requested. If the L<page>, L<per_page>, L_order_by> or -L<order_by_desc> attributes are changed on the ListView object, the -current_page_collection is cleared and recreated. - -=head1 METHODS - -=head2 row_actions_for - -=over 4 - -=item Arguments: none - -=back - -Returns an array reference of uris and labels representing the actions -set in L<row_action_prototypes>. L<Catalyst/uri_for> is used to -construct these. - -=head2 export_header_data - -=over 4 - -=item Arguments: $exporter - -=back - - $lv->export_head_data($exporter); - -C<$exporter> should be a code reference which will export lists of -data passed to it. This method calls the C<exporter> code reference -passing it the labels from the L<field_label_map> using the current -set of L<field_names>. - -=head2 export_body_data - -=over 4 - -=item Arguments: $exporter - -=back - - $lv->export_body_data($exporter); - -C<$exporter> should be a code reference which will export lists of -data passed to it. This method calls the C<exporter> code reference -with an array of rows containing the data values of each of the -current L<field_values>. - -=head2 export_to_data - -=over 4 - -=item Arguments: $exporter - -=back - - $lv->export_to_data($exporter); - -C<$exporter> should be a code reference which will export lists of -data passed to it. This method calls L<export_header_data> and -L<export_body_data> with C<exporter>. - -=head2 export_to_csv - -=over 4 - -=item Arguments: none - -=back - - $lv->export_to_csv(); - -Fills the L<Catalyst::Response> body with CSV data of the -L<current_collection> using L<export_to_data> and L<Text::CSV_XS>. - -=head2 field_label - -=over 4 - -=item Arguments: $field_name - -=back - -Returns the label for the given C<field_name>, using L<field_label_map>. - -=head1 AUTHORS - -See L<Reaction::Class> for authors. - -=head1 LICENSE - -See L<Reaction::Class> for the license. - -=cut diff --git a/lib/Reaction/UI/ViewPort/ObjectView.pm b/lib/Reaction/UI/ViewPort/ObjectView.pm index 7e1cac1..2dc7f63 100644 --- a/lib/Reaction/UI/ViewPort/ObjectView.pm +++ b/lib/Reaction/UI/ViewPort/ObjectView.pm @@ -10,11 +10,12 @@ use aliased 'Reaction::UI::ViewPort::DisplayField::DateTime'; use aliased 'Reaction::UI::ViewPort::DisplayField::RelatedObject'; use aliased 'Reaction::UI::ViewPort::DisplayField::List'; use aliased 'Reaction::UI::ViewPort::DisplayField::Collection'; +use aliased 'Reaction::InterfaceModel::Object'; + class ObjectView is 'Reaction::UI::ViewPort', which { - has object => ( - isa => 'Reaction::InterfaceModel::Object', is => 'ro', required => 1 - ); + has object => (isa => Object, is => 'ro', required => 1); + has ordered_fields => (is => 'rw', isa => 'ArrayRef', lazy_build => 1); has _field_map => ( isa => 'HashRef', is => 'rw', init_arg => 'fields', lazy_build => 1, @@ -23,7 +24,7 @@ class ObjectView is 'Reaction::UI::ViewPort', which { has exclude_fields => ( is => 'rw', isa => 'ArrayRef', required => 1, default => sub{ [] } ); - has ordered_fields => (is => 'rw', isa => 'ArrayRef', lazy_build => 1); + implements fields => as { shift->_field_map }; diff --git a/lib/Reaction/UI/Widget/GridView.pm b/lib/Reaction/UI/Widget/GridView.pm new file mode 100644 index 0000000..bb2f526 --- /dev/null +++ b/lib/Reaction/UI/Widget/GridView.pm @@ -0,0 +1,72 @@ +package Reaction::UI::Widget::GridView; + +use Reaction::UI::WidgetClass; + +class GridView, which { + widget renders [ qw/header rows footer/ + => { viewport => func('self', 'viewport') } + ]; + + header renders [ 'header_row' ]; + header_row renders [ header_cell over func('viewport', 'column_names') ]; + header_cell renders [ string { $_ } ]; + + footer renders [ 'footer_row' ]; + footer_row renders [ footer_cell over func('viewport', 'column_names') ]; + footer_cell renders [ string { $_ } ]; + + rows renders [ viewport over func('viewport','rows') ]; + +}; + +1; + + +=for layout widget +<table> + [% header %] +<tbody> + [% rows %] +</tbody> +<tfoot> + [% footer %] +</tfoot> +</table> + +=for layout header + +<thead> + [% content %] +</thead> + +=for layout header_row + +<tr> + [% content %] +</tr> + +=for layout header_cell + +<th> [% content %] </th> + +=for layout footer + +<tfoot> + [% content %] +</tfoot> + +=for layout footer_row + +<tr> [% content %] </tr> + +=for layout footer_cell + +<td> [% content %] </td> + +=for layout rows + +<tbody> + [% content %] +</tbody> + +=cut diff --git a/lib/Reaction/UI/Widget/GridView/Row.pm b/lib/Reaction/UI/Widget/GridView/Row.pm new file mode 100644 index 0000000..8ed46e0 --- /dev/null +++ b/lib/Reaction/UI/Widget/GridView/Row.pm @@ -0,0 +1,46 @@ +package Reaction::UI::Widget::ObjectView; + +use Reaction::UI::WidgetClass; + +class ObjectView, which { + widget renders [ cells => { viewport => func('self', 'viewport') } ]; + cells renders [ cell over func('viewport', 'ordered_fields') ]; + cell renders [ 'viewport' ]; +}; + +1; + +__END__; + + +=head1 NAME + +Reaction::UI::Widget::GridView::Row + +=head1 DESCRIPTION + +=head1 FRAGMENTS + +=head2 widget + +Additional variables available in topic hash: "viewport". + +Renders "cells" + +=head2 cells + +Sequentially renders the C<ordered_fields> of the viewport as "cell" + +=head2 cell + +renders the cell value + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/ListView.pm b/lib/Reaction/UI/Widget/ListView.pm index 7d9801c..9c525ee 100644 --- a/lib/Reaction/UI/Widget/ListView.pm +++ b/lib/Reaction/UI/Widget/ListView.pm @@ -1,56 +1,14 @@ package Reaction::UI::Widget::ListView; use Reaction::UI::WidgetClass; -use aliased 'Reaction::UI::ViewPort::ListView' => 'ListView_VP'; -class ListView, which { +class ListView is 'Reaction::UI::Widget::GridView', which { +# widget renders [ qw/pager header rows footer/ +# => { viewport => func('self', 'viewport') } +# ]; - has 'viewport' => (isa => ListView_VP, is => 'ro', required => 1); - - widget renders [ - qw(header body) => { viewport => func(self => 'viewport') } - ]; - - header renders [ header_entry over func(viewport => 'field_names') ]; - - header_entry renders [ string { $_{viewport}->field_label_map->{ $_ } } ]; - - body renders [ row over func(viewport => 'current_page_collection') ]; - - row renders [ - col_entry over func(viewport => 'field_names') => { row => $_ } - ]; - - col_entry renders [ - string { - my $proto = $_{row}->$_; - if (blessed($proto) && $proto->can('display_name')) { - return $proto->display_name; - } - return "${proto}"; - } - ]; +# header_cell renders [ string { $_ } ]; }; 1; - -__END__; - -=head1 NAME - -Reaction::UI::Widget::ListView - -=head1 DESCRIPTION - -=head2 viewport - -=head1 AUTHORS - -See L<Reaction::Class> for authors. - -=head1 LICENSE - -See L<Reaction::Class> for the license. - -=cut diff --git a/lib/Reaction/UI/Widget/Value.pm b/lib/Reaction/UI/Widget/Value.pm new file mode 100644 index 0000000..07db1c0 --- /dev/null +++ b/lib/Reaction/UI/Widget/Value.pm @@ -0,0 +1,40 @@ +package Reaction::UI::Widget::Value; + +use Reaction::UI::WidgetClass; + +class Value, which { + widget renders [ qw/value/ => { viewport => func(self => 'viewport') } ]; + value renders [ string { $_{viewport}->value } ]; +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value + +=head1 DESCRIPTION + +=head1 FRAGMENTS + +=head2 widget + +Additional variables available in topic hash: "viewport". + +Renders "label" and "field" + +=head2 field + + C<content> will contain the value, if any, of the field. + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/Boolean.pm b/lib/Reaction/UI/Widget/Value/Boolean.pm new file mode 100644 index 0000000..21a0f3f --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/Boolean.pm @@ -0,0 +1,35 @@ +package Reaction::UI::Widget::Value::Boolean; + +use Reaction::UI::WidgetClass; + +class Boolean is 'Reaction::UI::Widget::Value', which { + value renders [ string { $_{viewport}->value_string } ]; +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::Boolean + +=head1 DESCRIPTION + +See L<Reaction::UI::Widget::Value> + +=head1 FRAGMENTS + +=head2 value + +C<content> contains the viewport's value_string + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/Collection.pm b/lib/Reaction/UI/Widget/Value/Collection.pm new file mode 100644 index 0000000..073911d --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/Collection.pm @@ -0,0 +1,44 @@ +package Reaction::UI::Widget::Value::Collection; + +use Reaction::UI::WidgetClass; + +class Collection, which { + widget renders [ qw/list/ => { viewport => func(self => 'viewport') } ]; + list renders [ item over func('viewport', 'value_names') ]; + item renders [ string { $_ } ]; +}; + +1; + +__END__; + + +=head1 NAME + +Reaction::UI::Widget::Value::Collection + +=head1 DESCRIPTION + +=head1 FRAGMENTS + +=head2 widget + +renders C<label> and C<list> passing additional variable "viewport" + +=head2 list + +renders fragment item over the viewport's C<value_names> + +=head2 item + +C<content> contains the value of the current item ($_ / $_{_}) + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/DateTime.pm b/lib/Reaction/UI/Widget/Value/DateTime.pm new file mode 100644 index 0000000..63159c5 --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/DateTime.pm @@ -0,0 +1,35 @@ +package Reaction::UI::Widget::Value::DateTime; + +use Reaction::UI::WidgetClass; + +class DateTime is 'Reaction::UI::Widget::Value', which { + value renders [ string { $_{viewport}->value_string } ]; +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::DateTime + +=head1 DESCRIPTION + +See L<Reaction::UI::Widget::Value> + +=head1 FRAGMENTS + +=head2 value + +C<content> contains the viewport's value_string + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/List.pm b/lib/Reaction/UI/Widget/Value/List.pm new file mode 100644 index 0000000..bd713e4 --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/List.pm @@ -0,0 +1,43 @@ +package Reaction::UI::Widget::Value::List; + +use Reaction::UI::WidgetClass; + +class List, which { + widget renders [ qw/list item/ => { viewport => func(self => 'viewport') } ]; + list renders [ item over func('viewport', 'value_names') ]; + item renders [ string { $_{_} } ]; +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::List + +=head1 DESCRIPTION + +=head1 FRAGMENTS + +=head2 widget + +renders C<label> passing additional variable "viewport" + +=head2 list + +renders fragment item over the viewport's C<value_names> + +=head2 item + +C<content> contains the value of the current item ($_ / $_{_}) + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/Number.pm b/lib/Reaction/UI/Widget/Value/Number.pm new file mode 100644 index 0000000..4d895e1 --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/Number.pm @@ -0,0 +1,29 @@ +package Reaction::UI::Widget::Value::Number; + +use Reaction::UI::WidgetClass; + +class Number is 'Reaction::UI::Widget::Value', which { + +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::Number + +=head1 DESCRIPTION + +See L<Reaction::UI::Widget::Value> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/RelatedObject.pm b/lib/Reaction/UI/Widget/Value/RelatedObject.pm new file mode 100644 index 0000000..4617514 --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/RelatedObject.pm @@ -0,0 +1,35 @@ +package Reaction::UI::Widget::Value::RelatedObject; + +use Reaction::UI::WidgetClass; + +class RelatedObject is 'Reaction::UI::Widget::Value', which { + value renders [ string { $_{viewport}->value_string } ]; +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::RelatedObject + +=head1 DESCRIPTION + +See L<Reaction::UI::Widget::Value> + +=head1 FRAGMENTS + +=head2 value + +C<content> contains the viewport's value_string + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/String.pm b/lib/Reaction/UI/Widget/Value/String.pm new file mode 100644 index 0000000..366355d --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/String.pm @@ -0,0 +1,29 @@ +package Reaction::UI::Widget::Value::String; + +use Reaction::UI::WidgetClass; + +class String is 'Reaction::UI::Widget::Value', which { + +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::String + +=head1 DESCRIPTION + +See L<Reaction::UI::Widget::Value> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/Widget/Value/Text.pm b/lib/Reaction/UI/Widget/Value/Text.pm new file mode 100644 index 0000000..0fe4cdd --- /dev/null +++ b/lib/Reaction/UI/Widget/Value/Text.pm @@ -0,0 +1,29 @@ +package Reaction::UI::Widget::Value::Text; + +use Reaction::UI::WidgetClass; + +class Text is 'Reaction::UI::Widget::Value', which { + +}; + +1; + +__END__; + +=head1 NAME + +Reaction::UI::Widget::Value::Text + +=head1 DESCRIPTION + +See L<Reaction::UI::Widget::Value> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/share/skin/default/layout/grid_view b/share/skin/default/layout/grid_view new file mode 100644 index 0000000..d4826b0 --- /dev/null +++ b/share/skin/default/layout/grid_view @@ -0,0 +1,49 @@ +=for layout widget + +<table> + [% header %] +<tbody> + [% rows %] +</tbody> +<tfoot> + [% footer %] +</tfoot> +</table> + +=for layout header + +<thead> + [% content %] +</thead> + +=for layout header_row + +<tr> + [% content %] +</tr> + +=for layout header_cell + +<th> [% content %] </th> + +=for layout footer + +<tfoot> + [% content %] +</tfoot> + +=for layout footer_row + +<tr> [% content %] </tr> + +=for layout footer_cell + +<td> [% content %] </td> + +=for layout rows + +<tbody> + [% content %] +</tbody> + +=cut diff --git a/share/skin/default/layout/list_view b/share/skin/default/layout/list_view index e69de29..d4826b0 100644 --- a/share/skin/default/layout/list_view +++ b/share/skin/default/layout/list_view @@ -0,0 +1,49 @@ +=for layout widget + +<table> + [% header %] +<tbody> + [% rows %] +</tbody> +<tfoot> + [% footer %] +</tfoot> +</table> + +=for layout header + +<thead> + [% content %] +</thead> + +=for layout header_row + +<tr> + [% content %] +</tr> + +=for layout header_cell + +<th> [% content %] </th> + +=for layout footer + +<tfoot> + [% content %] +</tfoot> + +=for layout footer_row + +<tr> [% content %] </tr> + +=for layout footer_cell + +<td> [% content %] </td> + +=for layout rows + +<tbody> + [% content %] +</tbody> + +=cut diff --git a/share/skin/default/layout/value/boolean b/share/skin/default/layout/value/boolean new file mode 100644 index 0000000..1ce3367 --- /dev/null +++ b/share/skin/default/layout/value/boolean @@ -0,0 +1,9 @@ +=for layout widget + +[% content %] + +=for layout value + +[% content | html %] + +=cut
\ No newline at end of file diff --git a/share/skin/default/layout/value/collection b/share/skin/default/layout/value/collection new file mode 100644 index 0000000..d376ecc --- /dev/null +++ b/share/skin/default/layout/value/collection @@ -0,0 +1,15 @@ +=for layout widget + +[% list %] + +=for layout list + +<ul> +[% content %] +</ul> + +=for layout item + +<li>[% content | html %]</li> + +=cut diff --git a/share/skin/default/layout/value/date_time b/share/skin/default/layout/value/date_time new file mode 100644 index 0000000..e35741c --- /dev/null +++ b/share/skin/default/layout/value/date_time @@ -0,0 +1,9 @@ +=for layout widget + +[% content %] + +=for layout value + +[% content | html %] + +=cut diff --git a/share/skin/default/layout/value/list b/share/skin/default/layout/value/list new file mode 100644 index 0000000..eea4e02 --- /dev/null +++ b/share/skin/default/layout/value/list @@ -0,0 +1,15 @@ +=for layout widget + +[% list %] + +=for layout list + +<ul> +[% content %] +</ul> + +=for layout item + +<li>[% content | html %]</li> + +=cut
\ No newline at end of file diff --git a/share/skin/default/layout/value/number b/share/skin/default/layout/value/number new file mode 100644 index 0000000..e35741c --- /dev/null +++ b/share/skin/default/layout/value/number @@ -0,0 +1,9 @@ +=for layout widget + +[% content %] + +=for layout value + +[% content | html %] + +=cut diff --git a/share/skin/default/layout/value/related_object b/share/skin/default/layout/value/related_object new file mode 100644 index 0000000..e35741c --- /dev/null +++ b/share/skin/default/layout/value/related_object @@ -0,0 +1,9 @@ +=for layout widget + +[% content %] + +=for layout value + +[% content | html %] + +=cut diff --git a/share/skin/default/layout/value/string b/share/skin/default/layout/value/string new file mode 100644 index 0000000..1ce3367 --- /dev/null +++ b/share/skin/default/layout/value/string @@ -0,0 +1,9 @@ +=for layout widget + +[% content %] + +=for layout value + +[% content | html %] + +=cut
\ No newline at end of file diff --git a/share/skin/default/layout/value/text b/share/skin/default/layout/value/text new file mode 100644 index 0000000..1ce3367 --- /dev/null +++ b/share/skin/default/layout/value/text @@ -0,0 +1,9 @@ +=for layout widget + +[% content %] + +=for layout value + +[% content | html %] + +=cut
\ No newline at end of file |