diff --git a/lib/Coocook/Controller/Permission.pm b/lib/Coocook/Controller/Permission.pm index 1f0745b2067a0fac1343cbc199a4418e396b38a8..eecf687208e3fa3722184026c2fff913ce400737 100644 --- a/lib/Coocook/Controller/Permission.pm +++ b/lib/Coocook/Controller/Permission.pm @@ -36,7 +36,8 @@ sub index : GET HEAD Chained('/project/submenu') PathPart('permissions') Args(0) } { - my $projects_users = $c->project->projects_users->prefetch('user'); + my $can_transfer_ownership_to_anyone = ''; + my $projects_users = $c->project->projects_users->prefetch('user'); while ( my $project_user = $projects_users->next ) { push @permissions, { @@ -46,21 +47,29 @@ sub index : GET HEAD Chained('/project/submenu') PathPart('permissions') Args(0) url => $c->uri_for_action( '/user/show', [ $project_user->user->name ] ), ), - edit_url => - $c->has_capability( edit_user_permission => { permission => $project_user, role => 'viewer' } ) - ? $c->project_uri( $self->action_for('edit'), $project_user->user->name ) - : undef, + edit_url => $c->project_uri_if_permitted( + $self->action_for('edit'), #perltidy + { permission => $project_user, role => 'viewer' }, + $project_user->user->name + ), - make_owner_url => - $c->has_capability( 'transfer_project_ownership', { permission => $project_user } ) - ? $c->project_uri( $self->action_for('make_owner'), $project_user->user->name ) - : undef, + make_owner_url => my $make_owner_url = $c->project_uri_if_permitted( + $self->action_for('make_owner'), + { permission => $project_user }, + $project_user->user->name + ), - revoke_url => $c->has_capability( 'revoke_project_permission', { permission => $project_user } ) - ? $c->project_uri( $self->action_for('revoke'), $project_user->user->name ) - : undef, + revoke_url => $c->project_uri_if_permitted( + $self->action_for('revoke'), + { permission => $project_user }, + $project_user->user->name + ), }; + + $make_owner_url and $can_transfer_ownership_to_anyone = 1; } + + $c->stash( can_transfer_ownership_to_anyone => $can_transfer_ownership_to_anyone ); } @permissions = sort { $a->{sort_key} cmp $b->{sort_key} } @permissions; diff --git a/lib/Coocook/Controller/PurchaseList.pm b/lib/Coocook/Controller/PurchaseList.pm index a1de59ccfb7640c62deb83c6b897f0a0d32340f1..2235f76a5300eeb50e3ea42a4d205892d7ae0145 100644 --- a/lib/Coocook/Controller/PurchaseList.pm +++ b/lib/Coocook/Controller/PurchaseList.pm @@ -35,13 +35,15 @@ sub index : GET HEAD Chained('/project/base') PathPart('purchase_lists') Args(0) for my $list (@lists) { $list->{date} = $lists->parse_date( $list->{date} ); - $list->{edit_url} = $c->project_uri( $self->action_for('edit'), $list->{id} ); - $list->{update_url} = $c->project_uri( $self->action_for('update'), $list->{id} ); + $list->{edit_url} = $c->project_uri( $self->action_for('edit'), $list->{id} ); + $list->{update_url} = $c->project_uri_if_permitted( $self->action_for('update'), $list->{id} ); $list->{make_default_url} = - !$list->{is_default} ? $c->project_uri( $self->action_for('make_default'), $list->{id} ) : undef; + !$list->{is_default} + ? $c->project_uri_if_permitted( $self->action_for('make_default'), $list->{id} ) + : undef; $list->{delete_url} = ( @lists == 1 or !$list->{is_default} ) - ? $c->project_uri( $self->action_for('delete'), $list->{id} ) + ? $c->project_uri_if_permitted( $self->action_for('delete'), $list->{id} ) : undef; } @@ -60,7 +62,7 @@ sub index : GET HEAD Chained('/project/base') PathPart('purchase_lists') Args(0) default_list => $default_list, min_date => $today, purchase_lists => \@lists, - create_url => $c->project_uri( $self->action_for('create') ), + create_url => $c->project_uri_if_permitted( $self->action_for('create') ), ); } @@ -89,6 +91,14 @@ sub edit : GET HEAD Chained('base') PathPart('') Args(0) RequiresCapability('vie $dish->{url} = $c->project_uri( '/dish/edit', $dish->{id} ); } + $c->stash( can_edit => !!$c->has_capability('edit_project') ); + $c->json_stash( + list_permissions => { + can_edit => !!$c->has_capability('edit_project'), + is_archived => !!$c->project->archived, + }, + ); + $c->has_capability('edit_project') or return; diff --git a/lib/Coocook/Helpers.pm b/lib/Coocook/Helpers.pm index 6b2d5538c59edccb94dc4c4d6694b4fc94118543..0d06c5774407a2a07d27231107cccf4acd7cd5b5 100644 --- a/lib/Coocook/Helpers.pm +++ b/lib/Coocook/Helpers.pm @@ -232,6 +232,40 @@ sub project_uri { ); } +=head2 $c->project_uri_if_permitted($action, @arguments, \%query_params?) + +Return URI for project-specific Catalyst action with the current project's C and C +plus any number of C<@arguments> and possibly C<\%query_params> if the current session has enough +capabilities to perform C<$action>. + + my $uri = $c->project_uri_if_permitted( '/purchase_list/update', $purchase_list->id, { key => 'value' } ); + # http://localhost/project/MyProject/purchase_list/42?key=value or undef if not enough capabilities + + my $uri = $c->project_uri( $self->action_for('update'), $purchase_list->id, { key => 'value' } ); + # the same + +=cut + +sub project_uri_if_permitted { + my $c = shift; + my $action = shift; + + my $project = $c->stash->{project} + or croak "Missing 'project' in stash"; + + my $fragment = ref $_[-1] eq 'SCALAR' ? pop @_ : undef; + + # if last argument is hashref that's the \%query_values argument + my $query = ref $_[-1] eq 'HASH' ? pop @_ : undef; + + return $c->uri_for_action_if_permitted( + $action, + [ $project->id, $project->url_name, @_ ], + $query || (), + $fragment || () + ); +} + sub project ($c) { $c->stash->{project} } =head2 $c->redirect_canonical_case( $args_index, $canonical_value ) diff --git a/root/common_templates/macros.tt b/root/common_templates/macros.tt index a33132ff490f22bd917be4fd95b863935c7f8da0..13f2e3837793d65e695fc75be975d17bfa50033c 100644 --- a/root/common_templates/macros.tt +++ b/root/common_templates/macros.tt @@ -20,7 +20,7 @@ END; MACRO display_project(project, opts) BLOCK ~%] [% project.is_public ? 'public' : 'lock' %]  - [% project.name | html; + [%~ project.name | html; END; MACRO display_tag(tag) BLOCK ~%] @@ -103,4 +103,53 @@ length=items.size ~%] [% END %] -[%~ END ~%] +[%~ END; + +# Arguments for button +# - variant: optional, str - one of the bootstrap variants, default: primary +# - disabled: bool, optional - triggering the "disabled" HTML attribute, default: false +# - href: str, optional - if set the button is rendered as Tag with the given href instead of + [% button( { + text => (faq.in_storage ? 'Update' : 'Create') _ ' Entry', + attrs => { + type => 'submit', + }, + } ) %] diff --git a/root/templates/admin/faq/index.tt b/root/templates/admin/faq/index.tt index 81b6a053dccceb9006349962af9f06ac965ca8ce..c3007a6e78de43894372c3af17b1850c02ccd2c7 100644 --- a/root/templates/admin/faq/index.tt +++ b/root/templates/admin/faq/index.tt @@ -14,13 +14,28 @@ USE Markdown; %] [% faq.question_md | markdown %] - edit + [% button( { + icon => 'edit', + tooltip => { + content => 'Edit FAQ entry', + }, + href => faq.url, + button_classes => 'btn-sm', + } ) %] [% END %] - add + [% button( { + variant => 'outline-primary', + icon => 'add', + tooltip => { + content => 'Create FAQ entry', + }, + href => new_faq_url, + container_classes => 'd-grid', + } ) %] diff --git a/root/templates/admin/terms/edit.tt b/root/templates/admin/terms/edit.tt index 463554c8ca39a71f9462de2d0f0db965bc917ef3..8afbe7db41fa697e002f1f4d9aa6b5da3ef7e84e 100644 --- a/root/templates/admin/terms/edit.tt +++ b/root/templates/admin/terms/edit.tt @@ -3,7 +3,7 @@

Valid from: - +

@@ -11,5 +11,12 @@
-

+
+ [% button( { + text => (terms.in_storage ? 'Update' : 'Create') _ ' terms', + attrs => { + type => 'submit', + }, + } ) %] +
diff --git a/root/templates/admin/terms/index.tt b/root/templates/admin/terms/index.tt index e7f4388b330d704f9921fef92ed5f81613832e6c..7c4287a3e4d39d808701eadabf5324024264aec4 100644 --- a/root/templates/admin/terms/index.tt +++ b/root/templates/admin/terms/index.tt @@ -5,22 +5,45 @@
[% IF item.edit_url %]
- + [% button( { + variant => 'outline-dark', + icon => 'edit', + tooltip => { + content => 'Edit terms', + }, + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
[% END; IF item.delete_url %]
- + [% button( { + variant => 'outline-danger', + icon => 'delete_forever', + tooltip => { + content => 'Delete terms', + }, + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
[% END %]
[% END; list(terms, 'term_item', { item_classes => 'd-flex gap-3 justify-content-between align-items-center parent'}) %] -
- add -
+[% button( { + variant => 'outline-primary', + icon => 'add', + tooltip => { + content => 'Create new terms', + }, + href => new_url, + container_classes => 'd-grid mt-4', + button_classes => 'btn-no-square', +} ) %] diff --git a/root/templates/article/edit.tt b/root/templates/article/edit.tt index 3316f862a425c58b9151aa446a0de25c4a5e3c15..ba092a9be41509477347cda939cdc36866e14d5c 100644 --- a/root/templates/article/edit.tt +++ b/root/templates/article/edit.tt @@ -116,7 +116,12 @@ IF article; escape_title( 'Article', article.name ); ELSE; title="New Article";
- + [% button( { + text => (article ? 'Update' : 'Create'), + attrs => { + type => 'submit', + }, + } ) %] diff --git a/root/templates/article/index.tt b/root/templates/article/index.tt index a91f84e6ae05c31c0bc1f8d683b680b4da4208d8..156be7baca804829be0a603517d64bfe84088508 100644 --- a/root/templates/article/index.tt +++ b/root/templates/article/index.tt @@ -1,7 +1,13 @@ [% title = "Articles" %] [% new = BLOCK %] -

add New article

+

+ [% button( { + icon => 'add', + text => 'New article', + href => new_url, + } ) %] +

[% END; new %] [% WRAPPER table length=articles.size classes='align-middle' %] @@ -42,17 +48,22 @@ - [% IF article.delete_url %]
- + [% article_name = article.name | html; + button( { + variant => 'outline-danger', + icon => 'delete_forever', + disabled => !article.delete_url, + tooltip => { + content => 'Delete ' _ article_name _ '', + disabled_content => 'This article is used in dishes/recipes', + }, + attrs => { + type => article.delete_url ? 'submit' : 'button', + }, + button_classes => 'btn-sm', + } ) %]
- [% ELSE %] - - [% END %] [% END %] diff --git a/root/templates/browse/recipe/import.tt b/root/templates/browse/recipe/import.tt index 275e8809ee8e068b38259687f1658805d6530ae6..b77551a51f8169564096f549eb7b7041400b108d 100644 --- a/root/templates/browse/recipe/import.tt +++ b/root/templates/browse/recipe/import.tt @@ -10,9 +10,14 @@
[% link_project(proj) %]
- + [% button( { + text => 'Import', + icon => 'east', + attrs => { + type => 'submit', + }, + icon_after_text => 1, + } ) %]
diff --git a/root/templates/browse/recipe/index.tt b/root/templates/browse/recipe/index.tt index 4b51a8865e8a93ca10282ebcbbecf07bf784b581..4b48a62dae10e67673f52365047067bf5b42d8e0 100644 --- a/root/templates/browse/recipe/index.tt +++ b/root/templates/browse/recipe/index.tt @@ -23,7 +23,17 @@ END %] [% IF item.import_url %]
- + [% button( { + icon => 'east', + text => 'Import', + tooltip => { + content => 'Import this recipe into one of your projects', + }, + attrs => { + type => 'submit', + }, + icon_after_text => 1, + } ) %]
[% END; END; diff --git a/root/templates/browse/recipe/show.tt b/root/templates/browse/recipe/show.tt index 306bc0bd03e842e2af15e1038cbdec07bae9ca73..0a64100c155c55e67e3052e7c2e8698be97197b8 100644 --- a/root/templates/browse/recipe/show.tt +++ b/root/templates/browse/recipe/show.tt @@ -3,7 +3,12 @@ js_push_template_path(); css.push('/css/print.css') %] - +[% button( { + icon => 'print', + tooltip => { content => 'Print' }, + attrs => { onclick => 'print()' }, + container_classes => 'd-inline-block my-3 no-print', +} ) %]
@@ -17,7 +22,17 @@ css.push('/css/print.css') %]

[% IF import_url %]
- + [% button( { + icon => 'east', + text => 'Import', + tooltip => { + content => 'Import this recipe into one of your projects', + }, + attrs => { + type => 'submit', + }, + icon_after_text => 1, + } ) %]
[% END %]
@@ -31,7 +46,13 @@ css.push('/css/print.css') %]
- + [% button( { + text => 'Show', + icon => 'calculate', + attrs => { + type => 'submit', + }, + } ) %]
diff --git a/root/templates/dashboard.tt b/root/templates/dashboard.tt index c95272a86e431b79ad7c08a3b263e336399568ea..e0aa0b256667dedfb23952d02d91fd1733f554ab 100644 --- a/root/templates/dashboard.tt +++ b/root/templates/dashboard.tt @@ -16,7 +16,9 @@
- + [% button( { + text => 'Create project', + } ) %]
diff --git a/root/templates/dish/edit.tt b/root/templates/dish/edit.tt index bff1ffa6f37dea10e8a4bf86e56230d456e7ab03..8f3d34b60d949edd3618403491ec6d9e2fc53764 100644 --- a/root/templates/dish/edit.tt +++ b/root/templates/dish/edit.tt @@ -17,9 +17,14 @@ js.push('/js/tagAutocomplete.js');
- + [% button( { + variant => 'danger', + text => 'Delete', + icon => 'delete_forever', + attrs => { + type => 'submit', + }, + } ) %]
@@ -85,7 +90,12 @@ js.push('/js/tagAutocomplete.js');
- + [% button( { + text => 'Update dish', + attrs => { + type => 'submit', + }, + } ) %]
@@ -110,7 +120,12 @@ class="col-sm-12 py-3">
- + [% button( { + text => 'Recalculate values', + attrs => { + type => 'submit', + }, + } ) %]
diff --git a/root/templates/faq/index.tt b/root/templates/faq/index.tt index fb53d2bd671dde24ed8f4d944f092bdb3a2b40e5..96167fb897d93d13f85bb6a823b010d3e74a73d6 100644 --- a/root/templates/faq/index.tt +++ b/root/templates/faq/index.tt @@ -2,10 +2,12 @@ html_title = 'Frequently Asked Questions' %] [% IF admin_faq_url %] -
-
- Edit FAQ -
+
+ [% button( { + text => 'Edit FAQ', + variant => 'outline-primary', + href => admin_faq_url, + } ) %]
[% END; @@ -21,7 +23,16 @@ FOR faq IN faqs %] [% IF faq.edit_url %]
- + [% tooltip_button( { + icon => 'edit', + tooltip => { + content => 'Edit', + }, + container_classes => 'ms-1', + attrs => { + type => 'submit', + }, + } ) %]
[% END %]
diff --git a/root/templates/organization/members.tt b/root/templates/organization/members.tt index 72b5d01f2722dab36ed329575dbb13f72632089b..86119529d31eb8e88baf69b0e1c16a6bd3d04996 100644 --- a/root/templates/organization/members.tt +++ b/root/templates/organization/members.tt @@ -31,7 +31,8 @@ has_admin = 0 %] [% END %] - + [%# We don't use the button macro here. Because when we would use the macro the styling would break %] + [% ELSE %] [% o_u.role %] @@ -42,12 +43,32 @@ has_admin = 0 %] [% IF o_u.transfer_ownership_url; has_admin = 1 %]
- + [% button( { + variant => 'outline-secondary', + icon => 'sync_alt', + tooltip => { + content => 'Transfer ownership', + }, + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
[% END %] [% IF o_u.remove_url %]
- + [% button( { + variant => 'outline-danger', + icon => 'delete_forever', + tooltip => { + content => 'Remove', + }, + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
[% END %] @@ -93,7 +114,12 @@ has_admin = 0 %]
- + [% button( { + text => 'Add', + attrs => { + type => 'submit', + }, + } ) %]
[% ELSE %] diff --git a/root/templates/organization/show.tt b/root/templates/organization/show.tt index e8eb8cd49f12d82fd2e9cec532962a8fed5d6e62..0718bce4f1838895b6c6002072e095cd388fb2f4 100644 --- a/root/templates/organization/show.tt +++ b/root/templates/organization/show.tt @@ -12,7 +12,12 @@ - + [% button( { + text => 'Change display name', + attrs => { + type => 'submit', + }, + } ) %] [% ELSE %]
@@ -35,7 +40,12 @@
- + [% button( { + text => 'Update description', + attrs => { + type => 'submit', + }, + } ) %] [% ELSE %] [% USE Markdown; organization.description_md | markdown %] @@ -56,15 +66,12 @@ BLOCK project_item ~%]

Members

- [% IF members_url %] - - manage_accounts Manage memberships - - [% ELSE %] - - manage_accounts Manage memberships - - [% END %] + [% button( { + text => 'Manage memberships', + icon => 'manage_accounts', + href => members_url, + disabled => members_url, + } ) %]
[% list(organizations_users, 'user_item', {list_classes => 'list-group-flush rounded'}) %] @@ -98,7 +105,13 @@ BLOCK project_item ~%]

Deleting an organization also removes all memberships and the organization’s permissions on projects.

- + [% button( { + variant => 'danger', + text => 'Delete organization', + attrs => { + type => 'submit', + }, + } ) %]
diff --git a/root/templates/print/day.tt b/root/templates/print/day.tt index 11da828739638297594351c9b98853bc65b830d7..ebe073dcdfbacbf96e0ed7f9b9c80fcfcc17234a 100644 --- a/root/templates/print/day.tt +++ b/root/templates/print/day.tt @@ -1,6 +1,12 @@ [% css.push('/css/print.css') %] - +
+ [% button( { + icon => 'print', + tooltip => { content => 'Print' }, + attrs => { onclick => 'print()' }, + } ) %] +
[% FOR meal IN meals %]

[% meal.name | html %]

diff --git a/root/templates/project/import.tt b/root/templates/project/import.tt index 1dcad3a8dd5abe8456d52658157de03ed5c2cacf..152ef155d7f477b0f692ae6eca69a5fd5b3acb2d 100644 --- a/root/templates/project/import.tt +++ b/root/templates/project/import.tt @@ -41,8 +41,14 @@ js_push_template_path() %] [% END %]
- - +
+ [% button( { + text => 'Import', + attrs => { + type => 'submit', + }, + } ) %] +
[% END %] diff --git a/root/templates/project/permissions.tt b/root/templates/project/permissions.tt index 043a3a54b866f9e31cce619799b015bf11e0d65e..c0c1a2b3aa58fc1ca048dc743ebf954b941f7c25 100644 --- a/root/templates/project/permissions.tt +++ b/root/templates/project/permissions.tt @@ -10,12 +10,12 @@ has_admin = 0 %] User/Organization Role - + [% FOR permission IN permissions %] - + [% IF permission.organization; link_organization(permission.organization); @@ -26,41 +26,56 @@ has_admin = 0 %] END %] - [% IF permission.edit_url %] -
-
-
+ [% IF permission.edit_url %] + +
-
-
- -
+ [% button( { + icon => 'check', + tooltip => { content => 'Save' }, + attrs => { + 'type' => 'submit', + }, + container_classes => 'action-btn', + } ) %]
- [% ELSE %] + [% ELSE %] [% permission.role %] - [% END %] + [% END %] -
- [% IF permission.revoke_url %] -
-
- -
-
- [% END %] - [% IF permission.make_owner_url %] -
-
- -
-
- [% END %] +
+ [% IF permission.make_owner_url %] +
+ [% button( { + variant => 'outline-primary', + icon => 'transfer_within_a_station', + tooltip => { content => 'Transfer ownership' }, + attrs => { + 'type' => 'submit', + }, + button_classes => 'btn-sm', + } ) %] +
+ [% END; + IF permission.revoke_url %] +
+ [% button( { + variant => 'outline-danger', + icon => 'delete_forever', + tooltip => { content => 'Revoke permission' }, + attrs => { + 'type' => 'submit', + }, + button_classes => 'btn-sm', + } ) %] +
+ [% END %]
@@ -103,7 +118,12 @@ has_admin = 0 %]
- + [% button( { + text => 'Add', + attrs => { + type => 'submit', + }, + } ) %]
[% ELSE %] diff --git a/root/templates/project/show.tt b/root/templates/project/show.tt index 7be8637390ad636b890fa05f4bf0ee71b99f6c29..091120e347b0367a68cfa83023870675ce18293f 100644 --- a/root/templates/project/show.tt +++ b/root/templates/project/show.tt @@ -57,7 +57,13 @@ IF project.archived;

[% project.name | html %]

- +
+ [% button( { + icon => 'print', + tooltip => { content => 'Print' }, + attrs => { onclick => 'print()' }, + } ) %] +
[% USE Markdown; @@ -71,9 +77,17 @@ IF project.archived; Meal Dishes -
- -
+ [% button( { + variant => 'outline-primary', + icon => 'add', + tooltip => { content => 'Add extra column' }, + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#addCol', + }, + button_classes => 'btn-sm btn-no-square', + container_classes => 'd-grid', + } ) %] diff --git a/root/templates/purchase_list/_edit_table.tt b/root/templates/purchase_list/_edit_table.tt index dcf7037b319b454ed60dabf0d0df9bb90b7aa99e..b227bd805003f9bd3c203d8dc5dacc388b3edb92 100644 --- a/root/templates/purchase_list/_edit_table.tt +++ b/root/templates/purchase_list/_edit_table.tt @@ -48,7 +48,17 @@ [% display_unit( item.unit, {html=>1} ) %]
- + [% button( { + variant => 'outline-primary', + icon => 'done', + tooltip => { + content => 'Update amount', + }, + attrs => { + type => 'submit', + }, + button_classes => 'ms-1 pl-form', + } ) %] [% ELSE; IF item.offset < 0; '⊖'; @@ -58,10 +68,20 @@ END; IF item.convertible_into.size %] -
+
[% FOREACH unit IN item.convertible_into %]
- + [% button( { + variant => 'outline-secondary', + text => display_value_unit(unit.total, unit, {html=>1}), + tooltip => { + content => 'Convert to ' _ display_value_unit(unit.total, unit, {print=>1}), + }, + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
[% END %]
@@ -125,7 +145,18 @@ [% IF item.update_offset_url %]
- + [% button( { + variant => 'outline-danger', + icon => 'undo', + tooltip => { + content => 'Reset rounding', + }, + attrs => { + type => 'submit', + name => 'offset', + value => '0', + }, + } ) %]
[% END %] diff --git a/root/templates/purchase_list/edit.tt b/root/templates/purchase_list/edit.tt index 721e57fc82a59b5eacf131a4a778ca6afa56445e..4b61e2d723867a57fcf9aef752091f59d99cab14 100644 --- a/root/templates/purchase_list/edit.tt +++ b/root/templates/purchase_list/edit.tt @@ -9,23 +9,47 @@ js.push('/lib/coocook-web-components/dist/autocomplete/autocomplete.es.js'); js_push_template_path(); %] - -[% IF purchase_lists.size > 1 %] - - -[% END %] - - +
+ [% button( { + icon => 'print', + tooltip => { content => 'Print' }, + attrs => { onclick => 'print()' }, + } ); -
+ IF purchase_lists.size > 1; + button( { + disabled => 1, + icon => 'drive_file_move', + text => 'Move items/ingredients to other purchase list', + tooltip => { + content => 'Move selected items/ingredients to different purchase list', + disabled_content => 'No items/ingredients selected to move', + }, + attrs => { + id => 'move-btn', + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#move-items', + }, + } ); + END; + + button( { + disabled => 1, + icon => 'move_to_inbox', + text => 'Assign articles to shop section', + tooltip => { + content => 'Assign selected articles to a shop section', + disabled_content => 'No articles selected to assign', + }, + attrs => { + id => 'assign-btn', + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#assign-articles', + }, + } ) %] +
+ +
[% INCLUDE purchase_list/_edit_table.tt %]
diff --git a/root/templates/purchase_list/index.tt b/root/templates/purchase_list/index.tt index 5b5edb91ae2d4d3cb1f7514b8ac17ecc4f9d7538..12202f084939ea9f97affd083107bc8344ab2181 100644 --- a/root/templates/purchase_list/index.tt +++ b/root/templates/purchase_list/index.tt @@ -5,9 +5,21 @@ js_push_template_path(); BLOCK new_list %] -
- -
+ [% button( { + variant => 'outline-primary', + icon => 'add', + disabled => !create_url, + tooltip => { + content => 'Create ' _ (purchase_lists.size > 0 ? 'another' : 'a') _ ' purchase list', + line_through => 1, + }, + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#addList', + }, + container_classes => 'd-grid', + button_classes => 'btn-no-square', + } ) %] [% END %] @@ -41,50 +53,62 @@ BLOCK new_list %] [% purchase_list.ingredients_count %]
- - - + [% button( { + variant => 'outline-dark', + disabled => !purchase_list.update_url, + icon => 'edit', + tooltip => { content => 'Rename purchase list', line_through => 1 }, + attrs => { + 'data-url' => purchase_list.update_url, + 'data-name' => purchase_list.name, + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#updateList', + }, + button_classes => 'btn-sm update-trigger', + } ); - [% IF purchase_lists.size > 1; - IF purchase_list.make_default_url %] + IF purchase_lists.size > 1 %]
- + [% button( { + variant => 'outline-dark', + disabled => !purchase_list.make_default_url, + icon => 'shopping_cart_checkout', + tooltip => { + content => purchase_list.is_default ? 'Is already the default purchase list' : 'Make purchase list the default purchase list', + line_through => purchase_list.is_default ? 0 : 1, + }, + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
- [% ELSE %] - - - - [% END; - END; + [% END; - IF purchase_list.delete_url; - IF is_in_use %] - - - - [% ELSE %] -
- -
- [% END; + button_classes = [ 'btn-sm' ]; + button_classes.push('delete-trigger') IF is_in_use; - ELSE %] - - - - [% END %] + button( { + button_classes => button_classes, + variant => 'outline-danger', + disabled => !purchase_list.delete_url || (purchase_lists.size > 1 && purchase_list.is_default), + icon => 'delete_forever', + tooltip => { + content => purchase_lists.size > 1 && purchase_list.is_default ? "Can’t delete default purchase list" : "Delete purchase list", + line_through => purchase_lists.size > 1 && purchase_list.is_default ? 0 : 1, + }, + attrs => is_in_use ? { + 'data-url' => purchase_list.delete_url, + 'data-name' => purchase_list.name, + 'data-count' => purchase_list.items_count, + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#delete-list', + } : { + form => 'delete-list-form-' _ purchase_list.id, + type => 'submit', + }, + } ) %] +
diff --git a/root/templates/recipe/edit.tt b/root/templates/recipe/edit.tt index 1297f9b3d9ce5bee399fdd0da04779c52e9508a5..cfb2f2f8af855726f576a852cf6952110be621e5 100644 --- a/root/templates/recipe/edit.tt +++ b/root/templates/recipe/edit.tt @@ -10,22 +10,50 @@ IF import_url %]
- + [% + button_label = 'Used in ' _ dishes.size _ ' dish' _ (dishes.size != 1 ? 'es' : ''); + button( { + text => button_label, + attrs => { + 'data-bs-toggle' => 'collapse', + 'data-bs-target' => '#usage', + 'aria-expanded' => 'false', + 'aria-controls' => 'usage', + }, + button_classes => 'dropdown-toggle align-items-center', + disabled => dishes.size == 0, + } ) + %]
[% IF public_url %] - - - - [% END; - IF import_url %]
- + [% button( { + text => 'Share link', + href => public_url, + variant => 'secondary', + attrs => { + 'data-bs-toggle' => 'collapse', + 'data-bs-target' => '#usage', + 'aria-expanded' => 'false', + 'aria-controls' => 'usage', + }, + } ) %]
- [% END %] + [% END; + IF import_url; + button( { + variant => 'secondary', + text => 'Import into another project', + icon => 'east', + icon_after_text => 1, + tooltip => { content => 'Import this recipe into another one of your projects' }, + attrs => { + type =>'submit', + form => 'import', + }, + container_classes => 'btn-group col-xxl-3 col-md-4 col-sm-6', + } ); + END %]
[% IF dishes.size > 0 %] @@ -86,7 +114,12 @@ IF import_url %]
- + [% button( { + text => 'Update recipe', + attrs => { + type => 'submit', + }, + } ) %]
diff --git a/root/templates/recipe/importable_recipes.tt b/root/templates/recipe/importable_recipes.tt index 27aa33085a48487d52cc8268ef030282144ff77d..0a200fddb4a6d9733f949ed5145574a6a3df53f0 100644 --- a/root/templates/recipe/importable_recipes.tt +++ b/root/templates/recipe/importable_recipes.tt @@ -13,9 +13,17 @@ [% IF recipe.import_url %]
- + [% + project_name = project.name | html; + button( { + text => 'Import', + icon => 'east', + tooltip => { content => 'Import this recipe into project "' _ project_name _ '"' }, + attrs => { + type => 'submit', + }, + } ) + %]
[% END %] diff --git a/root/templates/recipe/index.tt b/root/templates/recipe/index.tt index e2ec4f5775f24dd09f19a27c93833130e2709ac9..ef083b66ea8ff51d3cf1ebd2b56be7d753895365 100644 --- a/root/templates/recipe/index.tt +++ b/root/templates/recipe/index.tt @@ -4,12 +4,25 @@ js_push_template_path(); new = BLOCK %]

- - - east Import recipe - +[% + button( { + icon => 'add', + text => 'New recipe', + tooltip => { content => 'Add new recipe' }, + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#create-recipe-modal', + }, + } ); + + button( { + icon => 'east', + text => 'Import recipe', + variant => 'secondary', + href => import_recipe_url, + tooltip => { content => 'Import recipe' }, + } ); +%]

[% END; new %] @@ -32,13 +45,30 @@ new = BLOCK %]
- + [% + recipe_name = recipe.name | html; + button( { + icon => 'file_copy', + variant => 'outline-dark', + tooltip => { content => 'Duplicate "' _ recipe_name _ '"' }, + attrs => { + 'data-name' => recipe_name, + 'data-url' => recipe.duplicate_url, + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#duplicate-recipe-modal', + }, + button_classes => 'btn-sm duplicate-trigger', + } ) + %]
- + [% button( { + icon => 'delete_forever', + variant => 'outline-danger', + attrs => { + type => 'submit', + }, + button_classes => 'btn-sm', + } ) %]
diff --git a/root/templates/session/login.tt b/root/templates/session/login.tt index 7a607ec485178a20b9729074740c024efd29431b..467997ec51078e7c132835e84380e9225b9aa15b 100644 --- a/root/templates/session/login.tt +++ b/root/templates/session/login.tt @@ -20,7 +20,9 @@ - + [% button( { + text => 'Sign in', + } ) %] diff --git a/root/templates/settings/account.tt b/root/templates/settings/account.tt index 4d00b4a35e18e9b3a146974fe143f33e83de00e7..c1b2fdc9a2abdd5db2514c13d2ee2d981ddb7f33 100644 --- a/root/templates/settings/account.tt +++ b/root/templates/settings/account.tt @@ -5,19 +5,21 @@ js.push('/lib/zxcvbn.js'); %] [% IF confirm_email_change_url %] -
-
-
- You can confirm to change your email address to - [% user.new_email_fc | html %] - -
-
-
+
+ You can confirm to change your email address to + [% user.new_email_fc | html %] + [% button( { + text => 'Confirm email change', + } ) %] +
[% END %]

- View your profile + [% button( { + text => 'View your profile', + variant => 'outline-primary', + href => profile_url, + } ) %]

@@ -41,7 +43,9 @@ js.push('/lib/zxcvbn.js');
- + [% button( { + text => 'Change display name', + } ) %] @@ -67,7 +71,10 @@ js.push('/lib/zxcvbn.js'); - + [% button( { + text => 'Change password', + button_classes => 'mb-2', + } ) %]
If you don’t know your current password anymore, you can @@ -90,12 +97,12 @@ js.push('/lib/zxcvbn.js');
[% IF user.new_email_fc %] -
-
- You’ve requested change to [% user.new_email_fc | html %] - at [% display_datetime(user.token_created, {short=>1}) %] UTC - -
+ + You’ve requested change to [% user.new_email_fc | html %] + at [% display_datetime(user.token_created, {short=>1}) %] UTC + [% button( { + text => 'Cancel', + } ) %]
[% END %] @@ -105,7 +112,9 @@ js.push('/lib/zxcvbn.js');
- + [% button( { + text => 'Change email address', + } ) %]
diff --git a/root/templates/settings/organizations.tt b/root/templates/settings/organizations.tt index 8c5759c4de43a8881954ab668c74c6389bb57a1d..e47a669f0de56f707968fc658180876978dc1753 100644 --- a/root/templates/settings/organizations.tt +++ b/root/templates/settings/organizations.tt @@ -14,7 +14,12 @@ BLOCK new_org %]
- + [% button( { + text => 'Create organization', + attrs => { + name => 'create', + }, + } ) %]
@@ -28,21 +33,33 @@ BLOCK user_item %] [% link_organization(item.organization) %] ([% item.role %]) [% IF item.leave_url %]
- + [% button( { + text => 'Leave organization', + icon => 'logout', + variant => 'outline-danger', + } ) %]
-[% ELSE %] - -[% END; +[% ELSE; + button( { + text => 'Leave organization', + icon => 'logout', + variant => 'outline-danger', + tooltip => { + content => 'You are the owner of this organization', + }, + disabled => 1, + button_classes => 'action-btn', + } ); +END; END; IF organizations_users.size > 0 %]

Before you can leave an organization you need to transfer organization ownership to another organization member.

- [% list(organizations_users, 'user_item', {item_classes => 'd-flex gap-3 justify-content-between align-items-center flex-wrap parent'}) %] + [% list(organizations_users, 'user_item', { + list_classes => 'mb-3', + item_classes => 'd-flex gap-3 justify-content-between align-items-center flex-wrap parent', + }) %] [% ELSE %]

You are not member of any organization yet.

[% END; diff --git a/root/templates/shop_section/index.tt b/root/templates/shop_section/index.tt index e879e9a3bb48b8d1eeba53ad4fb884bd4604a580..36688fad1cc321063450b776e524e73cc647478e 100644 --- a/root/templates/shop_section/index.tt +++ b/root/templates/shop_section/index.tt @@ -5,9 +5,19 @@ js_push_template_path(); BLOCK new_section %] -
- -
+ [% button( { + variant => 'outline-primary', + icon => 'add', + tooltip => { + content => 'Create shop section', + }, + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#add-section', + }, + container_classes => 'd-grid', + button_classes => 'btn-no-square', + } ) %] [% END %] @@ -28,20 +38,33 @@ BLOCK new_section %] [% section.articles_count %]
- - [% IF section.delete_url %] -
- + [% + section_name = section.name | html; + button( { + variant => 'outline-dark', + icon => 'edit', + tooltip => { content => 'Rename shop section', line_through => 1 }, + attrs => { + 'data-url' => section.update_url, + 'data-name' => section_name, + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#update-section', + }, + button_classes => 'btn-sm update', + } ) + %] + + [% button( { + variant => 'outline-danger', + icon => 'delete_forever', + disabled => !section.delete_url, + tooltip => { + content => 'Delete shop section', + disabled_content => numerus(section.articles_count, 'article', 'articles') _ ' are assigned to this shop section', + }, + button_classes => 'btn-sm', + } ) %]
- [% ELSE %] - - [% END %]
diff --git a/root/templates/tag/edit.tt b/root/templates/tag/edit.tt index e034f5a9bbfcd40138002b0f6103b7945efff44c..1504d4be654a2028bd995668859fe3cde67239c5 100644 --- a/root/templates/tag/edit.tt +++ b/root/templates/tag/edit.tt @@ -2,20 +2,26 @@ js_push_template_path() %]
- - [% IF is_in_use %] - - [% ELSE %] + [% button( { + text => 'Edit', + icon => 'edit', + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#edit-tag', + }, + button_classes => 'update', + } ) %]
- + [% button( { + text => 'Delete', + icon => 'delete', + disabled => is_in_use, + variant => 'danger', + attrs => { + id => 'delete-btn', + }, + } ) %]
- [% END %]
diff --git a/root/templates/tag/index.tt b/root/templates/tag/index.tt index 23964924d9d115e924baa1c3245bcab779aedc2c..9c6046942e234433fb72b2f9f26e0ce14f610262 100644 --- a/root/templates/tag/index.tt +++ b/root/templates/tag/index.tt @@ -1,63 +1,83 @@ [% title = "Tags"; -js_push_template_path() %] +js_push_template_path(); -

- - -

+BLOCK new_element %] + +
+[% + button( { + text => 'New Tag', + icon => 'add', + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#add-tag', + }, + } ); + button( { + text => 'New Tag group', + icon => 'add', + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#add-tg', + }, + } ); +%] +
+[% END; + +PROCESS new_element; -[% FOR group IN groups %] +FOR group IN groups %]

Tag Group [% group.name | html %]

- - - [% IF group.tags.size %] -
- -
- [% ELSE %] + [% + group_name = group.name | html; + group_comment = group.comment | html; + button( { + variant => 'outline-dark', + icon => 'add', + tooltip => { + content => 'Add tag to tag group '_ group_name, + }, + attrs => { + 'data-id' => group.id, + 'data-name' => group_name, + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#add-to-group', + }, + button_classes => 'btn-sm atg', + } ); + button( { + variant => 'outline-dark', + icon => 'edit', + tooltip => { + content => 'Edit tag group '_ group_name, + }, + attrs => { + 'data-url' => group.update_url, + 'data-name' => group_name, + 'data-comment' => group_comment, + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#tg-edit', + }, + button_classes => 'btn-sm edit-tg', + } ) + %]
- + [% button( { + variant => 'outline-danger', + icon => 'delete', + disabled => group.tags.size, + tooltip => { + content => 'Delete tag group '_ group_name, + disabled_content => "Can't delete tag group which contains tags", + }, + button_classes => 'btn-sm', + } ) %]
- [% END %]
@@ -83,18 +103,11 @@ js_push_template_path() %] [% END %]
-[% END %] +[% END; -

- - -

+PROCESS new_element; -[% WRAPPER includes/infobox.tt %] +WRAPPER includes/infobox.tt %] Tags are keywords that can be attached to recipes, dishes and articles. They can be used to filter them during search or to highlight important aspects.

diff --git a/root/templates/terms/show.tt b/root/templates/terms/show.tt index 9f829f1192566717f67ab893e15a53b30c05c2cc..d18758b922004225d7b1832c075535d7c056846a 100644 --- a/root/templates/terms/show.tt +++ b/root/templates/terms/show.tt @@ -2,11 +2,11 @@
[% IF previous_url %] - + chevron_left [% ELSE %] - + chevron_left [% END %] @@ -15,11 +15,11 @@ ' until ' _ display_date( terms.valid_until, {html=>1} ) IF terms.valid_until %]

[% IF next_url %] - + chevron_right [% ELSE %] - + chevron_right [% END %] diff --git a/root/templates/unit/edit.tt b/root/templates/unit/edit.tt index 583985fcaf7e7d665ca9ce246cee44fd3217a0f5..2c2141bcfb6707a78281e8c4c2b4f05dbde7e49b 100644 --- a/root/templates/unit/edit.tt +++ b/root/templates/unit/edit.tt @@ -14,7 +14,9 @@ END %]
- + [% button( { + text => 'Update', + } ) %]

Conversions

@@ -40,7 +42,10 @@ END %]
- + [% button( { + text => 'Add', + button_classes => 'btn-sm', + } ) %]
@@ -53,7 +58,7 @@ END %]
1 [% unit.long_name | html %] is equal to
[% IF conversion.update_url %] -
+
[% ELSE; @@ -62,13 +67,23 @@ END %] [% conversion.long_name | html %]
- [% IF conversion.update_url %] - - [% END %] + [% IF conversion.update_url; + button( { + text => 'Update', + button_classes => 'btn-sm', + attrs => { + form => 'update-conversion-to-unit-' _ conversion.id, + }, + } ); + END; - [% IF conversion.delete_url %] + IF conversion.delete_url %]
- + [% button( { + text => 'Delete', + variant => 'danger', + button_classes => 'btn-sm', + } ) %]
[% END %] [% ELSE %] diff --git a/root/templates/unit/index.tt b/root/templates/unit/index.tt index f0f0a118a318c3a8425c3b2cf03f8a6420b223dd..13e12ace61c50b638e5b1435a77b91c398bd088c 100644 --- a/root/templates/unit/index.tt +++ b/root/templates/unit/index.tt @@ -4,9 +4,14 @@ js_push_template_path(); new = BLOCK %]

- + [% button( { + text => 'New unit', + icon => 'add', + attrs => { + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#create-unit-modal', + }, + } ) %]

[% END; new %] @@ -45,17 +50,18 @@ new = BLOCK %] [% END; IF loop.first %] - [% IF unit.delete_url %]
- + [% button( { + icon => 'delete', + variant => 'outline-danger', + disabled => !unit.delete_url, + tooltip => { + content => 'Delete unit', + disabled_content => 'This is used for dish/recipe ingredients or purchase list items', + }, + button_classes => 'btn-sm', + } ) %]
- [% ELSE %] - - [% END %] [% END %] diff --git a/root/templates/user/recover.tt b/root/templates/user/recover.tt index 69e808dd658f73deb99bca8607d56295771b1f0a..d156d24377b9987e0479b6b9e9d026d0cc35dc5d 100644 --- a/root/templates/user/recover.tt +++ b/root/templates/user/recover.tt @@ -14,7 +14,10 @@
- + + [% button( { + text => 'Send recovery link', + } ) %] diff --git a/root/templates/user/register.tt b/root/templates/user/register.tt index 4b133ed08d52e2121f123277db6c5836fe7fa266..c989dff982820a67b90e4aad24c92f3d70f89283 100644 --- a/root/templates/user/register.tt +++ b/root/templates/user/register.tt @@ -51,7 +51,10 @@ IF terms %] By clicking [% label %] you accept our terms as of [% display_date(terms.valid_from, {html=>1}) %]. [% END %] You will receive an email with a web link to verify your email address. - + + [% button( { + text => label, + } ) %] diff --git a/root/templates/user/reset_password.tt b/root/templates/user/reset_password.tt index bdab62d3ac76b3e0648709ce6c3cd36cdfa75184..43adcc6e51730f19dec76d225dee467d6decb251 100644 --- a/root/templates/user/reset_password.tt +++ b/root/templates/user/reset_password.tt @@ -23,7 +23,10 @@ js.push('/lib/zxcvbn.js');
- + + [% button( { + text => 'Reset password', + } ) %] diff --git a/root/templates/user/show.tt b/root/templates/user/show.tt index bab048182a1d3cf02492add8be5bf169b7da2c9f..0969b2970f57ef016b999884262c63696e60efb1 100644 --- a/root/templates/user/show.tt +++ b/root/templates/user/show.tt @@ -1,13 +1,23 @@ [% escape_title( 'User', user_object.display_name ) %]

- [% IF my_settings_url %] - edit Edit your profile - [% END %] + [% IF my_settings_url; + button( { + text => 'Edit your profile', + icon => 'edit', + variant => 'outline-primary', + href => my_settings_url, + } ); + END %] - [% IF profile_admin_url %] - supervisor_account View profile as admin - [% END %] + [% IF profile_admin_url; + button( { + text => 'View profile as admin', + icon => 'supervisor_account', + variant => 'outline-secondary', + href => profile_admin_url, + } ); + END %]

Registered: [% display_date(user_object.created) %]

diff --git a/t/controller_Article.t b/t/controller_Article.t index 45cd492d6d858ec8cde75f56e8d4e93dab8ccea0..a0ea3b4917570ed29de2e4615131750609c3eb30 100644 --- a/t/controller_Article.t +++ b/t/controller_Article.t @@ -13,7 +13,7 @@ $t->login_ok( 'john_doe', 'P@ssw0rd' ); $t->follow_link_ok( { text => 'public Test Project' } ); $t->follow_link_ok( { text => 'Articles' } ); -$t->follow_link_ok( { text => 'add New article' } ); +$t->follow_link_ok( { text => 'addNew article' } ); $t->submit_form_ok( { with_fields => { name => 'aether' } }, "create article" ); diff --git a/t/organization_lifecycle.t b/t/organization_lifecycle.t index 12d6d564e006ad9671f201ff6796f8ce7d0290d5..79e723f1e6a7ac1e497c6fb1c4eea97bfa7439f3 100644 --- a/t/organization_lifecycle.t +++ b/t/organization_lifecycle.t @@ -36,7 +36,7 @@ $t->text_lacks( my $display_name = 'Test Orga' ); $t->submit_form_ok( { with_fields => { display_name => $display_name } } ); $t->text_contains("Organization $display_name"); -$t->follow_link_ok( { text => 'manage_accounts Manage memberships' } ); +$t->follow_link_ok( { text => 'manage_accountsManage memberships' } ); $t->submit_form_ok( { form_id => 'add-member', with_fields => { name => 'other', role => 'member' } } ); diff --git a/t/template_macros.t b/t/template_macros.t new file mode 100644 index 0000000000000000000000000000000000000000..7fc05dd4e68595f199b3f72de84120eeaefd686e --- /dev/null +++ b/t/template_macros.t @@ -0,0 +1,105 @@ +use Coocook::Base; + +use Template; +use Test2::V0 -no_warnings => 1; +use Test::Builder; + +my $tt = Template->new( + ENCODING => 'utf8', + INCLUDE_PATH => 'root/common_templates/', + PLUGIN_BASE => 'Coocook::Filter', + PRE_PROCESS => 'macros.tt', +); + +sub is_tt; # declare name, implement below + +subtest button => sub { + is_tt '[% button({icon => "edit"}) %]', {} => <<~EOT, "with text"; + + EOT + + is_tt '[% button({text => "foo"}) %]', {} => <<~EOT, "with text"; + + EOT + + is_tt '[% button({text => "foo", tooltip => {content => "bar"} }) %]', {} => <<~EOT, "with tooltip"; + + EOT +}; + +is_tt '[% display_project({name => "My Project"}) %]', {} => <lock My Project +EOT + +subtest display_unit => sub { + my $stash = { unit => { short_name => 'kg', long_name => 'kilograms' } }; + + is_tt '[% display_unit(unit) %]', $stash => "kg (kilograms)", "default"; + is_tt '[% display_unit(unit, {print=>1}) %]', $stash => "kilograms", "print"; + is_tt '[% display_unit(unit, {html=>1}) %]', $stash => <<~HTML, "html"; + kg + HTML +}; + +subtest display_value => sub { + is_tt '[% display_value(123) %]', {} => "123"; + is_tt '[% display_value(-123) %]', {} => "\N{MINUS SIGN}123"; + is_tt '[% display_value(123, {force_sign=>1}) %]', {} => "\N{PLUS SIGN}123"; + is_tt '[% display_value(-123, {force_sign=>1}) %]', {} => "\N{MINUS SIGN}123"; + + is_tt '[% display_value(1.23456789, {significant_digits=>1}) %]', {} => "1.23"; + is_tt '[% display_value(-1.23456789, {significant_digits=>1}) %]', {} => "\N{MINUS SIGN}1.23"; + + is_tt '[% display_value(1.23456789, {force_sign=>1,significant_digits=>1}) %]', + {} => "\N{PLUS SIGN}1.23"; +}; + +subtest numerus => sub { + is_tt '[% numerus(0, "one", "many") %]', {} => "0 many"; + is_tt '[% numerus(1, "one", "many") %]', {} => "1 one"; + is_tt '[% numerus(2, "one", "many") %]', {} => "2 many"; + + is_tt '[% numerus(0, "one", "many", {infix=>"of my"}) %]', {} => "0 of my many"; + is_tt '[% numerus(1, "one", "many", {infix=>"of my"}) %]', {} => "1 of my one"; + is_tt '[% numerus(2, "one", "many", {infix=>"of my"}) %]', {} => "2 of my many"; + + is_tt '[% numerus(1.23456789, "one", "many" ) %]', {} => "1.23456789 many"; + is_tt '[% numerus(1.23456789, "one", "many", {significant_digits=>1}) %]', {} => "1.23 many"; +}; + +subtest table => sub { + is_tt '[% WRAPPER table length=1 classes="foo bar" %]rows[% END %]', {} => <<~HTML; + + rows +
+ HTML + + is_tt '[% WRAPPER table length=2 classes="foo bar" %]rows[% END %]', {} => <<~HTML; + + rows +
+ HTML + + is_tt '[% WRAPPER table length=4 classes="foo bar" %]rows[% END %]', {} => <<~HTML; + + rows +
+ HTML +}; + +done_testing; + +sub is_tt ( $tt_code, $stash, $expected_output, $name = undef ) { + local $Test::Builder::Level = $Test::Builder::Level + 1; + + $tt->process( \$tt_code, $stash, \my $output ) or die $tt->error; + + $expected_output =~ m/\n\z/ and $output .= " "; # heredocs always end with \n + + # TODO test HTML equivalence instead of string equivalence + for ( $output, $expected_output ) { + s/\s+/ /gm; # reduce multiple spaces to one (very basic normalization) + } + + is $output => $expected_output, $name; +}