From 0caabb0bb7f32792c6834526325bea89fa807f2c Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Mon, 16 Sep 2024 15:31:51 +0200
Subject: [PATCH 01/33] Add macro for button
Implements #312
---
root/common_templates/macros.tt | 49 +++++++++++++++++++++++++++++++++
root/static/css/print.css | 3 +-
root/static/css/style.css | 14 +++++++++-
3 files changed, 64 insertions(+), 2 deletions(-)
diff --git a/root/common_templates/macros.tt b/root/common_templates/macros.tt
index 4654ceb25..1abd02f43 100644
--- a/root/common_templates/macros.tt
+++ b/root/common_templates/macros.tt
@@ -107,4 +107,53 @@ length=items.size ~%]
[% END %]
+[%~ END;
+
+# Arguments for button and tooltip_button
+# opts:
+# - 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
+# - icon: optional, str - name of a Material Icons icon
+# - text: optional, str - label text shown on the button
+# - tooltip: hashref, required for tooltip_button - details for Bootstrap tooltip, see below
+# - attrs: optional, hashref - additional HTML attributes and values for the button element
+# - container_classes: optional, string | string[] - additional CSS classes for the outer container
+# - button_classes: optional, string | string[] - additional CSS classes for the button element itself
+# - icon_after_text: optional, bool - if true the icon is displayed after the button text
+#
+# tooltip:
+# - content: required, string - content of the tooltip, could be html markup
+# - line_through: optional, bool - when button disabled show tooltip with linethrough, default: false
+# - disabled_content: optional, string - content of the tooltip when button disabled, could be html markup
+
+MACRO button(opts) BLOCK;
+ IF opts.icon;
+ icon = '' _ opts.icon _ ' ';
+ END ~%]
+ <[% opts.href ? 'a' : 'button' %]
+ class="btn btn-[% opts.variant || 'primary' %] [% 'btn-quad' IF !opts.text %] d-inline-flex justify-content-center gap-1 [% opts.button_classes.list.join(' ') %]"
+ [%~ ' disabled ' IF opts.disabled;
+ FOR attr IN opts.attrs;
+ attr.key %]="[% attr.value | html %]"
+ [%~ END;
+ 'href="' _ opts.href _ '"' IF opts.href ~%]
+ >
+ [% icon IF !opts.icon_after_text; opts.text; icon IF opts.icon_after_text %]
+ [% opts.href ? 'a' : 'button' %]>
+[%~ END;
+
+MACRO tooltip_button(opts) BLOCK;
+ IF opts.disabled;
+ IF opts.tooltip.line_through;
+ msg = '' _ (opts.tooltip.disabled_content || opts.tooltip.content) _ ' ';
+ ELSE;
+ msg = (opts.tooltip.disabled_content || opts.tooltip.content);
+ END;
+ ELSE;
+ msg = (opts.tooltip.content);
+ END ~%]
+
+ [% button( opts ) %]
+
[%~ END ~%]
diff --git a/root/static/css/print.css b/root/static/css/print.css
index 7f2ce0306..b82f0775f 100644
--- a/root/static/css/print.css
+++ b/root/static/css/print.css
@@ -1,7 +1,8 @@
@media print {
/* hide browser navigation elements */
header,
- footer {
+ footer,
+ .tooltip {
display: none;
}
diff --git a/root/static/css/style.css b/root/static/css/style.css
index 16916cd04..b5b0b7150 100644
--- a/root/static/css/style.css
+++ b/root/static/css/style.css
@@ -17,7 +17,19 @@ textarea {
resize: vertical;
}
-.btn-sm.btn-quad {
+.btn-quad {
+ aspect-ratio: 1;
+}
+
+.btn-no-quad {
+ aspect-ratio: unset;
+}
+
+.btn.btn-quad {
+ padding: 0.375em;
+}
+
+.btn.btn-sm.btn-quad {
padding: 0.25rem;
}
--
GitLab
From aa41bb72080f4a101a1ead449c91c33d07f30a13 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Mon, 16 Sep 2024 16:22:02 +0200
Subject: [PATCH 02/33] Add helper project_uri_if_permitted
as shortcut for uri_for_action_if_permitted which
automatically sets the project path parameter
---
lib/Coocook/Helpers.pm | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/lib/Coocook/Helpers.pm b/lib/Coocook/Helpers.pm
index 6b2d5538c..0d06c5774 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 )
--
GitLab
From 0fc202f26d11d2d437a762907bbe6d6d38c66708 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20B=C3=B6hmer?=
Date: Mon, 16 Sep 2024 18:09:35 +0200
Subject: [PATCH 03/33] Add unit test for TT macros
---
root/common_templates/macros.tt | 2 +-
t/template_macros.t | 87 +++++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+), 1 deletion(-)
create mode 100644 t/template_macros.t
diff --git a/root/common_templates/macros.tt b/root/common_templates/macros.tt
index 1abd02f43..bf5ba0a67 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 ~%]
diff --git a/t/template_macros.t b/t/template_macros.t
new file mode 100644
index 000000000..113489fb5
--- /dev/null
+++ b/t/template_macros.t
@@ -0,0 +1,87 @@
+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
+
+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;
+
+ HTML
+
+ is_tt '[% WRAPPER table length=2 classes="foo bar" %]rows[% END %]', {} => <<~HTML;
+
+ HTML
+
+ is_tt '[% WRAPPER table length=4 classes="foo bar" %]rows[% END %]', {} => <<~HTML;
+
+ 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;
+
+ # TODO test HTML equivalence instead of string equivalence
+ $output =~ s/\S\K +/ /g; # reduce multiple spaces to one
+ $expected_output =~ m/\n\z/ and $output .= "\n"; # heredocs always end with \n
+ is $output => $expected_output, $name;
+}
--
GitLab
From 8542439a065a0270cee9b1c7b648518f5de1ad06 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Mon, 16 Sep 2024 18:00:47 +0200
Subject: [PATCH 04/33] Respect project state und capabilities in purchase list
index
---
lib/Coocook/Controller/PurchaseList.pm | 10 ++-
root/templates/purchase_list/index.tt | 108 +++++++++++++++----------
2 files changed, 72 insertions(+), 46 deletions(-)
diff --git a/lib/Coocook/Controller/PurchaseList.pm b/lib/Coocook/Controller/PurchaseList.pm
index 301660e56..610ab6f10 100644
--- a/lib/Coocook/Controller/PurchaseList.pm
+++ b/lib/Coocook/Controller/PurchaseList.pm
@@ -40,13 +40,15 @@ sub index : GET HEAD Chained('/project/base') PathPart('purchase_lists') Args(0)
$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;
}
diff --git a/root/templates/purchase_list/index.tt b/root/templates/purchase_list/index.tt
index 3bf5a0cfb..2a5f72375 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 %]
-
- add
-
+ [% tooltip_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-quad',
+ } ) %]
[% END %]
@@ -41,50 +53,62 @@ BLOCK new_list %]
[% purchase_list.ingredients_count %]
-
-
- edit
-
-
+ [% tooltip_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 %]
- [% ELSE %]
-
-
- shopping_cart_checkout
-
-
- [% END;
- END;
+ [% END;
- IF purchase_list.delete_url;
- IF is_in_use %]
-
-
- delete_forever
-
-
- [% ELSE %]
-
- [% END;
+ button_classes = [ 'btn-sm' ];
+ button_classes.push('delete-trigger') IF is_in_use;
- ELSE %]
-
-
- delete_forever
-
-
- [% END %]
+ tooltip_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',
+ },
+ } ) %]
+
--
GitLab
From 80f4284ff2da8713c577ee446d13c6d02de80466 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Mon, 16 Sep 2024 20:52:24 +0200
Subject: [PATCH 05/33] Respect project state und capabilities in purchase list
editor
---
lib/Coocook/Controller/PurchaseList.pm | 8 +
root/static/css/style.css | 5 -
root/static/js/purchase_list/edit.js | 343 +++++++++++---------
root/static/js/script.js | 30 ++
root/templates/purchase_list/_edit_table.tt | 39 ++-
root/templates/purchase_list/edit.tt | 56 +++-
6 files changed, 298 insertions(+), 183 deletions(-)
diff --git a/lib/Coocook/Controller/PurchaseList.pm b/lib/Coocook/Controller/PurchaseList.pm
index 610ab6f10..81903f84d 100644
--- a/lib/Coocook/Controller/PurchaseList.pm
+++ b/lib/Coocook/Controller/PurchaseList.pm
@@ -98,6 +98,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/root/static/css/style.css b/root/static/css/style.css
index b5b0b7150..48de23d01 100644
--- a/root/static/css/style.css
+++ b/root/static/css/style.css
@@ -390,11 +390,6 @@ cc-autocomplete {
border-width: 0;
}
-.tt-disabled-btn {
- pointer-events: auto !important;
- cursor: default;
-}
-
.purchase-list-table .selected {
--bs-table-accent-bg: var(--bs-table-striped-bg);
color: var(--bs-table-striped-color);
diff --git a/root/static/js/purchase_list/edit.js b/root/static/js/purchase_list/edit.js
index 749124fdc..7bbdaa9c9 100644
--- a/root/static/js/purchase_list/edit.js
+++ b/root/static/js/purchase_list/edit.js
@@ -1,14 +1,20 @@
// Purchase list moving constants
-const moveBtn = document.getElementById("move-btn");
-const moveBtnDisabled = document.getElementById("move-btn-disabled");
+const moveBtn = getBtn(
+ "#move-btn",
+ "Move selected items/ingredients to different purchase list",
+ "No items/ingredients selected to move"
+);
const moveItemsForm = document.getElementById("move-items-form");
const moveModal = document.getElementById("move-items");
const selectPl = document.getElementById("move-items-target");
const currentPurchaseListId = parseInt(location.pathname.split("/").pop());
// Shop section moving constants
-const assignBtn = document.getElementById("assign-btn");
-const assignBtnDisabled = document.getElementById("assign-btn-disabled");
+const assignBtn = getBtn(
+ "#assign-btn",
+ "Assign selected articles to a shop section",
+ "No articles selected to assign"
+);
const assignArticlesForm = document.getElementById("assign-articles-form");
const assignModal = document.getElementById("assign-articles");
const selectShopSection = document.getElementById("assign-articles-target");
@@ -21,13 +27,183 @@ const articleSelectList = document.getElementById("article-select-list");
const messages = document.getElementById("messages");
const purchaseLists = getJsonData("purchase_lists");
-const singleList = purchaseLists.length === 1;
+const singleList = (purchaseLists || []).length === 1;
let shopSections = getJsonData("shop_sections");
let articles = {};
let preselectedPurchaseList = null;
+const permissions = getJsonData("list_permissions");
let SKIP = false;
+(() => {
+ if (permissions.is_archived || !permissions.can_edit) {
+ moveBtn.disable(`Can't move items because ${getDisabledReason()}`);
+ assignBtn.disable(
+ `Can't assign articles because ${getDisabledReason()}`
+ );
+ return;
+ }
+
+ init();
+
+ // Purchase lists
+ buildPurchaseListOptions();
+
+ moveModal.addEventListener("shown.bs.modal", () => selectPl.focus());
+ moveModal.addEventListener("hidden.bs.modal", () => {
+ if (preselectedPurchaseList !== null) {
+ selectPl.value = preselectedPurchaseList;
+ selectPl.setCustomValidity("");
+ } else {
+ selectPl.value = "";
+ selectPl.setCustomValidity("Please select a target purchase list");
+ }
+ });
+
+ moveItemsForm.addEventListener("submit", async (e) => {
+ e.preventDefault();
+ const items = document.querySelectorAll(
+ `input[id^="move-item"]:checked`
+ );
+ const ingredients = document.querySelectorAll(
+ `input[id^="move-ingredient"]:checked`
+ );
+ const data = `target_purchase_list=${selectPl.value}${Array.from(items)
+ .map((elem) => `&item=${elem.id.split("-").pop()}`)
+ .join("")}${Array.from(ingredients)
+ .map((elem) => `&ingredient=${elem.id.split("-").pop()}`)
+ .join("")}`;
+ const url =
+ location.pathname +
+ (location.pathname.endsWith("/") ? "" : "/") +
+ "move_items_ingredients";
+ const res = await fetch(url, {
+ method: "post",
+ headers: {
+ Accept: "text/html",
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: data,
+ });
+
+ if (!res.ok) {
+ showMessage(
+ "An error occured during moving the selected items/ingredients",
+ "danger"
+ );
+ } else {
+ document.getElementById("list-container").innerHTML =
+ await res.text();
+ showMessage(
+ "Successfully moved the selected items/ingredients",
+ "success"
+ );
+ init();
+ }
+
+ bootstrap.Modal.getInstance(moveModal).hide();
+ });
+
+ // Shop sections
+ initAutocomplete();
+
+ assignModal.addEventListener("shown.bs.modal", () =>
+ selectShopSection.focus()
+ );
+ assignModal.addEventListener("show.bs.modal", () => {
+ const items = document.querySelectorAll(
+ `input[id^="move-item"]:checked`
+ );
+ const selectedArticles = mapArticleAmount(items);
+ const error = [];
+
+ for (const [id, data] of Object.entries(selectedArticles)) {
+ if (articles[id].num !== data.num) {
+ error.push({
+ name: data.name,
+ selected: data.num,
+ total: articles[id].num,
+ });
+ }
+ }
+
+ if (error.length === 0) {
+ articleSelectWarning.classList.add("d-none");
+ articleSelectWarning.classList.remove("d-flex");
+ } else {
+ articleSelectNum.innerText = `article${
+ error.length === 1 ? "" : "s"
+ }`;
+ articleSelectList.innerHTML = error
+ .map(
+ (elem) =>
+ `${elem.name} - ${elem.selected} of ${elem.total} items selected `
+ )
+ .join("");
+ articleSelectWarning.classList.remove("d-none");
+ articleSelectWarning.classList.add("d-flex");
+ }
+ });
+ assignModal.addEventListener("hidden.bs.modal", () => {
+ selectShopSection.clear();
+ });
+
+ assignArticlesForm.addEventListener("submit", async (e) => {
+ e.preventDefault();
+
+ const section = String(
+ new FormData(e.target).get("target-shop-section")
+ ).trim();
+ const numSection = parseInt(section);
+
+ const shopSection = `${
+ !(section === "null" || Number.isInteger(numSection)) ? "new_" : ""
+ }shop_section=${section}`;
+
+ const items = document.querySelectorAll(
+ `input[id^="move-item"]:checked`
+ );
+
+ const data = `${shopSection}${Array.from(items)
+ .map((elem) => `&article=${elem.dataset.articleId}`)
+ .join("")}`;
+ const url =
+ location.pathname +
+ (location.pathname.endsWith("/") ? "" : "/") +
+ "assign_articles_to_shop_section";
+ const res = await fetch(url, {
+ method: "post",
+ headers: {
+ Accept: "text/html",
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: data,
+ });
+
+ if (!res.ok) {
+ showMessage(
+ "An error occured during moving the selected articles",
+ "danger"
+ );
+ } else {
+ document.getElementById("list-container").innerHTML =
+ await res.text();
+ showMessage("Successfully moved the selected articles", "success");
+ init();
+ selectShopSection.options = getShopSectionOptions();
+ selectShopSection.clear();
+ }
+
+ bootstrap.Modal.getInstance(assignModal).hide();
+ });
+})();
+
+function getDisabledReason() {
+ return permissions.is_archived
+ ? "the project is archived"
+ : "you don't have enough permissions for this action";
+}
+
function showMessage(msg, type) {
const msgElem = document.createElement("div");
msgElem.className = `alert alert-${type}`;
@@ -111,12 +287,6 @@ function mapArticleAmount(items) {
}
function init() {
- if (shopSections === undefined || purchaseLists === undefined) {
- if (!singleList) moveBtnDisabled.classList.add("d-none");
- assignBtnDisabled.classList.add("d-none");
- return;
- }
-
const items = document.querySelectorAll(`input[id^="move-item"]`);
const ingredients = document.querySelectorAll(
`input[id^="move-ingredient"]`
@@ -186,26 +356,20 @@ function checkDisabled() {
}
if (items > 0 && !singleIngredientSelected) {
- assignBtn?.classList.remove("d-none");
- assignBtnDisabled?.classList.add("d-none");
+ assignBtn?.enable();
} else {
- assignBtn?.classList.add("d-none");
- assignBtnDisabled?.classList.remove("d-none");
+ assignBtn?.disable();
}
if (!singleList) {
if (items > 0 || ingredients.length > 0) {
- moveBtn?.classList.remove("d-none");
- moveBtnDisabled?.classList.add("d-none");
+ moveBtn?.enable();
} else {
- moveBtn?.classList.add("d-none");
- moveBtnDisabled?.classList.remove("d-none");
+ moveBtn?.disable();
}
}
}
-init();
-
// Purchase lists
function buildPurchaseListOptions() {
for (const pl of purchaseLists) {
@@ -244,60 +408,6 @@ function buildPurchaseListOptions() {
});
}
-buildPurchaseListOptions();
-
-moveModal.addEventListener("shown.bs.modal", () => selectPl.focus());
-moveModal.addEventListener("hidden.bs.modal", () => {
- if (preselectedPurchaseList !== null) {
- selectPl.value = preselectedPurchaseList;
- selectPl.setCustomValidity("");
- } else {
- selectPl.value = "";
- selectPl.setCustomValidity("Please select a target purchase list");
- }
-});
-
-moveItemsForm.addEventListener("submit", async (e) => {
- e.preventDefault();
- const items = document.querySelectorAll(`input[id^="move-item"]:checked`);
- const ingredients = document.querySelectorAll(
- `input[id^="move-ingredient"]:checked`
- );
- const data = `target_purchase_list=${selectPl.value}${Array.from(items)
- .map((elem) => `&item=${elem.id.split("-").pop()}`)
- .join("")}${Array.from(ingredients)
- .map((elem) => `&ingredient=${elem.id.split("-").pop()}`)
- .join("")}`;
- const url =
- location.pathname +
- (location.pathname.endsWith("/") ? "" : "/") +
- "move_items_ingredients";
- const res = await fetch(url, {
- method: "post",
- headers: {
- Accept: "text/html",
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: data,
- });
-
- if (!res.ok) {
- showMessage(
- "An error occured during moving the selected items/ingredients",
- "danger"
- );
- } else {
- document.getElementById("list-container").innerHTML = await res.text();
- showMessage(
- "Successfully moved the selected items/ingredients",
- "success"
- );
- init();
- }
-
- bootstrap.Modal.getInstance(moveModal).hide();
-});
-
// Shop sections
function getShopSectionOptions() {
return [
@@ -321,86 +431,3 @@ function initAutocomplete() {
selectShopSection.clear();
}, 1_000);
}
-
-initAutocomplete();
-
-assignModal.addEventListener("shown.bs.modal", () => selectShopSection.focus());
-assignModal.addEventListener("show.bs.modal", () => {
- const items = document.querySelectorAll(`input[id^="move-item"]:checked`);
- const selectedArticles = mapArticleAmount(items);
- const error = [];
-
- for (const [id, data] of Object.entries(selectedArticles)) {
- if (articles[id].num !== data.num) {
- error.push({
- name: data.name,
- selected: data.num,
- total: articles[id].num,
- });
- }
- }
-
- if (error.length === 0) {
- articleSelectWarning.classList.add("d-none");
- articleSelectWarning.classList.remove("d-flex");
- } else {
- articleSelectNum.innerText = `article${error.length === 1 ? "" : "s"}`;
- articleSelectList.innerHTML = error
- .map(
- (elem) =>
- `${elem.name} - ${elem.selected} of ${elem.total} items selected `
- )
- .join("");
- articleSelectWarning.classList.remove("d-none");
- articleSelectWarning.classList.add("d-flex");
- }
-});
-assignModal.addEventListener("hidden.bs.modal", () => {
- selectShopSection.clear();
-});
-
-assignArticlesForm.addEventListener("submit", async (e) => {
- e.preventDefault();
-
- const section = String(
- new FormData(e.target).get("target-shop-section")
- ).trim();
- const numSection = parseInt(section);
-
- const shopSection = `${
- !(section === "null" || Number.isInteger(numSection)) ? "new_" : ""
- }shop_section=${section}`;
-
- const items = document.querySelectorAll(`input[id^="move-item"]:checked`);
-
- const data = `${shopSection}${Array.from(items)
- .map((elem) => `&article=${elem.dataset.articleId}`)
- .join("")}`;
- const url =
- location.pathname +
- (location.pathname.endsWith("/") ? "" : "/") +
- "assign_articles_to_shop_section";
- const res = await fetch(url, {
- method: "post",
- headers: {
- Accept: "text/html",
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: data,
- });
-
- if (!res.ok) {
- showMessage(
- "An error occured during moving the selected articles",
- "danger"
- );
- } else {
- document.getElementById("list-container").innerHTML = await res.text();
- showMessage("Successfully moved the selected articles", "success");
- init();
- selectShopSection.options = getShopSectionOptions();
- selectShopSection.clear();
- }
-
- bootstrap.Modal.getInstance(assignModal).hide();
-});
diff --git a/root/static/js/script.js b/root/static/js/script.js
index cb87d3161..d4c3338d1 100644
--- a/root/static/js/script.js
+++ b/root/static/js/script.js
@@ -170,3 +170,33 @@ function getJsonData(id) {
return undefined;
}
}
+
+function getBtn(selector, enabledMsg, disabledMsg) {
+ const btn = document.querySelector(selector);
+
+ if (btn === null) {
+ return null;
+ }
+
+ const tt = btn.parentElement;
+
+ const changeTooltip = (msg) => {
+ const bsTt = bootstrap.Tooltip.getInstance(tt);
+ bsTt.dispose();
+ tt.dataset.bsTitle = msg;
+ bootstrap.Tooltip.getOrCreateInstance(tt);
+ };
+
+ return {
+ elem: btn,
+ disable: (msg = disabledMsg) => {
+ btn.disabled = true;
+ changeTooltip(msg);
+ },
+ enable: (msg = enabledMsg) => {
+ btn.disabled = false;
+ changeTooltip(msg);
+ },
+ changeTooltip,
+ };
+}
diff --git a/root/templates/purchase_list/_edit_table.tt b/root/templates/purchase_list/_edit_table.tt
index dcf7037b3..bb3320066 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} ) %]
- done
+ [% tooltip_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 %]
[% END %]
@@ -125,7 +145,18 @@
[% IF item.update_offset_url %]
[% END %]
diff --git a/root/templates/purchase_list/edit.tt b/root/templates/purchase_list/edit.tt
index 80c0eca1c..f067940b2 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();
%]
-
print
-[% IF purchase_lists.size > 1 %]
-
- drive_file_move Move items/ingredients to other purchase list
-
-
- drive_file_move Move items/ingredients to other purchase list
-
-[% END %]
-
- move_to_inbox Assign articles to shop section
-
-
- move_to_inbox Assign articles to shop section
-
+
+ [% tooltip_button( {
+ icon => 'print',
+ tooltip => { content => 'Print' },
+ attrs => { onclick => 'print()' },
+ } );
-
+ IF purchase_lists.size > 1;
+ tooltip_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;
+
+ tooltip_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 %]
--
GitLab
From ae7bbd8ece82c38187bea8e2c7cd8daaa15a57c1 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 11:23:10 +0200
Subject: [PATCH 06/33] Use button macro for faq pages
---
root/templates/admin/faq/edit.tt | 13 +++++++++++--
root/templates/admin/faq/index.tt | 19 +++++++++++++++++--
root/templates/faq/index.tt | 11 ++++++++++-
3 files changed, 38 insertions(+), 5 deletions(-)
diff --git a/root/templates/admin/faq/edit.tt b/root/templates/admin/faq/edit.tt
index f465b63e6..22d8fc7cc 100644
--- a/root/templates/admin/faq/edit.tt
+++ b/root/templates/admin/faq/edit.tt
@@ -3,7 +3,11 @@
[% IF faq %]
-
Show the FAQ entry
+ [% button( {
+ text => 'Show the FAQ entry',
+ variant => 'outline-primary',
+ href => faq.url,
+ } ) %]
[% END %]
@@ -34,7 +38,12 @@
- [% faq.in_storage ? 'Update' : 'Create' %] Entry
+ [% 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 81b6a053d..66c410bdd 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
+ [% tooltip_button( {
+ icon => 'edit',
+ tooltip => {
+ content => 'Edit FAQ entry',
+ },
+ href => faq.url,
+ button_classes => 'btn-sm',
+ } ) %]
[% END %]
- add
+ [% tooltip_button( {
+ variant => 'outline-primary',
+ icon => 'add',
+ tooltip => {
+ content => 'Create FAQ entry',
+ },
+ href => new_faq_url,
+ container_classes => 'd-grid',
+ } ) %]
diff --git a/root/templates/faq/index.tt b/root/templates/faq/index.tt
index fb53d2bd6..0766303ec 100644
--- a/root/templates/faq/index.tt
+++ b/root/templates/faq/index.tt
@@ -21,7 +21,16 @@ FOR faq IN faqs %]
[% IF faq.edit_url %]
[% END %]
--
GitLab
From 6aa2cc42547a111553d3e7ee33dd7b536c47bc77 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 12:01:23 +0200
Subject: [PATCH 07/33] Use button macro on admin term pages
---
root/templates/admin/terms/edit.tt | 11 ++++++--
root/templates/admin/terms/index.tt | 41 ++++++++++++++++++++++-------
2 files changed, 41 insertions(+), 11 deletions(-)
diff --git a/root/templates/admin/terms/edit.tt b/root/templates/admin/terms/edit.tt
index 463554c8c..8afbe7db4 100644
--- a/root/templates/admin/terms/edit.tt
+++ b/root/templates/admin/terms/edit.tt
@@ -3,7 +3,7 @@
diff --git a/root/templates/admin/terms/index.tt b/root/templates/admin/terms/index.tt
index e7f4388b3..5918f923e 100644
--- a/root/templates/admin/terms/index.tt
+++ b/root/templates/admin/terms/index.tt
@@ -5,22 +5,45 @@
[% IF item.edit_url %]
[% END;
IF item.delete_url %]
[% END %]
[% END;
list(terms, 'term_item', { item_classes => 'd-flex gap-3 justify-content-between align-items-center parent'}) %]
-
+[% tooltip_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',
+} ) %]
--
GitLab
From 0c4aa39f5fb7e51959b70c90fdbb4abe3eb9247f Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 13:23:21 +0200
Subject: [PATCH 08/33] Use button macro on article pages
---
root/templates/article/edit.tt | 7 ++++++-
root/templates/article/index.tt | 31 +++++++++++++++++++++----------
t/controller_Article.t | 2 +-
3 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/root/templates/article/edit.tt b/root/templates/article/edit.tt
index 7adab80d6..f51d9afd0 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 eceb1194d..bb9368163 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 %]
- [% ELSE %]
-
- delete_forever
-
- [% END %]
[% END %]
diff --git a/t/controller_Article.t b/t/controller_Article.t
index 701a8c1e1..364b1d665 100644
--- a/t/controller_Article.t
+++ b/t/controller_Article.t
@@ -14,7 +14,7 @@ $t->follow_link_ok( { text => 'public Test Project' } );
$t->follow_link_ok( { text => 'Articles' } );
$t->content_contains('https://localhost/project/1/Test-Project/tag/1');
-$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" );
--
GitLab
From dd137aaa342e84426062f17ec4e3f61f0a8ee3ec Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 14:25:46 +0200
Subject: [PATCH 09/33] Use button macro on browse pages
---
root/templates/browse/recipe/import.tt | 11 ++++++++---
root/templates/browse/recipe/index.tt | 12 +++++++++++-
root/templates/browse/recipe/show.tt | 27 +++++++++++++++++++++++---
3 files changed, 43 insertions(+), 7 deletions(-)
diff --git a/root/templates/browse/recipe/import.tt b/root/templates/browse/recipe/import.tt
index 275e8809e..b77551a51 100644
--- a/root/templates/browse/recipe/import.tt
+++ b/root/templates/browse/recipe/import.tt
@@ -10,9 +10,14 @@
diff --git a/root/templates/browse/recipe/index.tt b/root/templates/browse/recipe/index.tt
index 4b51a8865..a4fd7b0a7 100644
--- a/root/templates/browse/recipe/index.tt
+++ b/root/templates/browse/recipe/index.tt
@@ -23,7 +23,17 @@ END %]
[% IF item.import_url %]
[% END;
END;
diff --git a/root/templates/browse/recipe/show.tt b/root/templates/browse/recipe/show.tt
index 306bc0bd0..95f2f1cc6 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') %]
-print
+[% tooltip_button( {
+ icon => 'print',
+ tooltip => { content => 'Print' },
+ attrs => { onclick => 'print()' },
+ container_classes => 'd-inline-block my-3 no-print',
+} ) %]
- calculate Show
+ [% button( {
+ text => 'Show',
+ icon => 'calculate',
+ attrs => {
+ type => 'submit',
+ },
+ } ) %]
--
GitLab
From 395a85bdf96fd15b049e0d7b82c302df3e1edb02 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 14:41:50 +0200
Subject: [PATCH 10/33] Use button macro on dish page
---
root/templates/dish/edit.tt | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/root/templates/dish/edit.tt b/root/templates/dish/edit.tt
index d0872d66a..58ebfdcd5 100644
--- a/root/templates/dish/edit.tt
+++ b/root/templates/dish/edit.tt
@@ -18,9 +18,14 @@ js.push('/js/tagAutocomplete.js');
@@ -86,7 +91,12 @@ js.push('/js/tagAutocomplete.js');
- Update dish
+ [% button( {
+ text => 'Update dish',
+ attrs => {
+ type => 'submit',
+ },
+ } ) %]
@@ -111,7 +121,12 @@ class="col-sm-12 py-3">
- Recalculate values
+ [% button( {
+ text => 'Recalculate values',
+ attrs => {
+ type => 'submit',
+ },
+ } ) %]
--
GitLab
From 101e2911e1393704a7a3fef83ba0c7b16f224464 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 15:20:29 +0200
Subject: [PATCH 11/33] Use button macro organization pages
---
root/templates/organization/members.tt | 34 +++++++++++++++++++---
root/templates/organization/show.tt | 37 ++++++++++++++++--------
root/templates/settings/organizations.tt | 7 ++++-
t/organization_lifecycle.t | 2 +-
4 files changed, 62 insertions(+), 18 deletions(-)
diff --git a/root/templates/organization/members.tt b/root/templates/organization/members.tt
index 3348f5afd..a39db3db9 100644
--- a/root/templates/organization/members.tt
+++ b/root/templates/organization/members.tt
@@ -31,7 +31,8 @@ has_admin = 0 %]
[% role %]
[% END %]
- done
+ [%# We don't use the button macro here. Because when we would use the macro the styling would break %]
+ done
[% ELSE %]
[% o_u.role %]
@@ -42,12 +43,32 @@ has_admin = 0 %]
[% IF o_u.transfer_ownership_url;
has_admin = 1 %]
[% END %]
[% IF o_u.remove_url %]
[% 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 e8eb8cd49..0718bce4f 100644
--- a/root/templates/organization/show.tt
+++ b/root/templates/organization/show.tt
@@ -12,7 +12,12 @@
Display name
-
+ [% 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 ~%]
[% 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.
diff --git a/root/templates/settings/organizations.tt b/root/templates/settings/organizations.tt
index 8c5759c4d..dd9a0711d 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'
+ }
+ } ) %]
diff --git a/t/organization_lifecycle.t b/t/organization_lifecycle.t
index 12d6d564e..79e723f1e 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' } } );
--
GitLab
From 0882609537dae697c87023449317865f160ed3d4 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 14 Nov 2024 11:08:29 +0100
Subject: [PATCH 12/33] Add button macro to print day page
---
root/templates/print/day.tt | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/root/templates/print/day.tt b/root/templates/print/day.tt
index 11da82873..d31cb15d6 100644
--- a/root/templates/print/day.tt
+++ b/root/templates/print/day.tt
@@ -1,6 +1,12 @@
[% css.push('/css/print.css') %]
-print
+
+ [% tooltip_button( {
+ icon => 'print',
+ tooltip => { content => 'Print' },
+ attrs => { onclick => 'print()' },
+ } ) %]
+
[% FOR meal IN meals %]
[% meal.name | html %]
--
GitLab
From e3344874b5bc3f8eb2d819a15f2b320bfbcd9305 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sat, 1 Mar 2025 07:50:02 +0100
Subject: [PATCH 13/33] Use button macro on project pages
---
lib/Coocook/Controller/Permission.pm | 10 ++-
root/static/css/style.css | 9 +-
root/static/js/project/show.js | 29 ++++--
root/templates/project/import.tt | 10 ++-
root/templates/project/permissions.tt | 121 ++++++++++++++++----------
root/templates/project/show.tt | 22 ++++-
6 files changed, 138 insertions(+), 63 deletions(-)
diff --git a/lib/Coocook/Controller/Permission.pm b/lib/Coocook/Controller/Permission.pm
index e8b6b5a91..c47233cbc 100644
--- a/lib/Coocook/Controller/Permission.pm
+++ b/lib/Coocook/Controller/Permission.pm
@@ -39,6 +39,9 @@ sub index : GET HEAD Chained('/project/submenu') PathPart('permissions') Args(0)
my $projects_users = $c->project->projects_users->prefetch('user');
while ( my $project_user = $projects_users->next ) {
+ my $can_transfer_onership =
+ $c->has_capability( 'transfer_project_ownership', { permission => $project_user } );
+
push @permissions, {
role => $project_user->role,
sort_key => $project_user->user->name_fc,
@@ -51,8 +54,7 @@ sub index : GET HEAD Chained('/project/submenu') PathPart('permissions') Args(0)
? $c->project_uri( $self->action_for('edit'), $project_user->user->name )
: undef,
- make_owner_url =>
- $c->has_capability( 'transfer_project_ownership', { permission => $project_user } )
+ make_owner_url => $can_transfer_onership
? $c->project_uri( $self->action_for('make_owner'), $project_user->user->name )
: undef,
@@ -60,6 +62,10 @@ sub index : GET HEAD Chained('/project/submenu') PathPart('permissions') Args(0)
? $c->project_uri( $self->action_for('revoke'), $project_user->user->name )
: undef,
};
+
+ if ($can_transfer_onership) {
+ $c->stash( can_transfer_ownership => 1 );
+ }
}
}
diff --git a/root/static/css/style.css b/root/static/css/style.css
index 48de23d01..e95fba649 100644
--- a/root/static/css/style.css
+++ b/root/static/css/style.css
@@ -367,13 +367,20 @@ cc-autocomplete {
.extra-col button {
font-weight: bold;
- padding: 1px;
+}
+
+.extra-col > * {
+ height: 34px;
}
.print {
display: none;
}
+.role-form select {
+ width: unset;
+}
+
/* Show/hide action buttons on hover/no hover */
.parent:hover .action-btn,
.purchase-list-table:hover .pl-form {
diff --git a/root/static/js/project/show.js b/root/static/js/project/show.js
index 0995be311..5cfebd4a8 100644
--- a/root/static/js/project/show.js
+++ b/root/static/js/project/show.js
@@ -18,23 +18,34 @@ const openEdit = (e) => {
const addCol = (e) => {
e.preventDefault();
- let name = document.getElementById("col-name").value;
- let head = table.querySelector("th:last-child");
- let btn = document.createElement("button");
+ const name = document.getElementById("col-name").value;
+ const head = table.querySelector("th:last-child");
+
+ const btn = document.createElement("button");
btn.innerText = name;
- btn.className = "btn btn-outline-dark";
- btn.title = "Edit column";
+ btn.className =
+ "btn btn-outline-dark btn-sm d-inline-flex justify-content-center align-items-center";
btn.onclick = openEdit;
- let th = document.createElement("th");
+
+ const btnContainer = document.createElement("span");
+ btnContainer.className = "d-grid";
+ btnContainer.append(btn);
+
+ const th = document.createElement("th");
th.className = "extra-col";
- th.append(btn);
+ th.append(btnContainer);
+
head.parentElement.insertBefore(th, head);
addModal.hide();
- for (let row of table.querySelectorAll("tbody tr")) {
- let tmp = document.createElement("td");
+ for (const row of table.querySelectorAll("tbody tr")) {
+ const tmp = document.createElement("td");
tmp.innerHTML = " ";
row.append(tmp);
}
+
+ bootstrap.Tooltip.getOrCreateInstance(btnContainer, {
+ title: "Edit column",
+ });
};
const saveCol = (e) => {
diff --git a/root/templates/project/import.tt b/root/templates/project/import.tt
index 1dcad3a8d..152ef155d 100644
--- a/root/templates/project/import.tt
+++ b/root/templates/project/import.tt
@@ -41,8 +41,14 @@ js_push_template_path() %]
[% END %]
-
- Import
+
+ [% button( {
+ text => 'Import',
+ attrs => {
+ type => 'submit',
+ },
+ } ) %]
+
[% END %]
diff --git a/root/templates/project/permissions.tt b/root/templates/project/permissions.tt
index 7798a281d..ae5d4ab3c 100644
--- a/root/templates/project/permissions.tt
+++ b/root/templates/project/permissions.tt
@@ -10,13 +10,12 @@ has_admin = 0 %]
User/Organization
Role
-
-
+
[% FOR permission IN permissions %]
-
+
[% IF permission.organization;
link_organization(permission.organization);
@@ -27,64 +26,91 @@ has_admin = 0 %]
END %]
- [% IF permission.edit_url %]
-
-
- [% IF permission.make_owner_url %]
-
-
-
-
-
- [% END %]
+ [% END %]
- [% IF permission.revoke_url %]
-
-
- delete_forever
-
-
- [% END %]
+
+ [% IF permission.make_owner_url %]
+
+ [% tooltip_button( {
+ variant => 'outline-primary',
+ icon => 'transfer_within_a_station',
+ tooltip => { content => 'Transfer ownership' },
+ attrs => {
+ 'type' => 'submit',
+ 'onsubmit' => 'return confirm("Do you really want to transfer the project ownership? You cannot undo this action.");',
+ },
+ button_classes => 'btn-sm',
+ } ) %]
+
+ [% END;
+ IF permission.revoke_url %]
+
+ [% tooltip_button( {
+ variant => 'outline-danger',
+ icon => 'delete_forever',
+ tooltip => { content => 'Revoke permission' },
+ attrs => {
+ 'type' => 'submit',
+ },
+ button_classes => 'btn-sm',
+ } ) %]
+
+ [% END %]
+
[% END %]
-
+
public anyone
[% project.is_public ? "viewer (project is public)" : "— (project is private)" %]
-
-
- [% IF project.is_public %]
- lock
- [% ELSE %]
+
+ [% IF project.is_public %]
+ [% tooltip_button( {
+ variant => 'outline-warning',
+ icon => 'lock',
+ tooltip => { content => 'Make project private' },
+ attrs => {
+ 'type' => 'submit',
+ },
+ button_classes => 'btn-sm',
+ } ) %]
+ [% ELSE %]
- public
- [% END %]
+ [% tooltip_button( {
+ variant => 'outline-warning',
+ icon => 'public',
+ tooltip => { content => 'Make project public' },
+ attrs => {
+ 'type' => 'submit',
+ },
+ button_classes => 'btn-sm',
+ } ) %]
+ [% END %]
+
@@ -125,7 +151,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 7be863739..bdf24102d 100644
--- a/root/templates/project/show.tt
+++ b/root/templates/project/show.tt
@@ -57,7 +57,13 @@ IF project.archived;
[% project.name | html %]
-print
+
+ [% tooltip_button( {
+ icon => 'print',
+ tooltip => { content => 'Print' },
+ attrs => { onclick => 'print()' },
+ } ) %]
+
[% USE Markdown;
@@ -71,9 +77,17 @@ IF project.archived;
Meal
Dishes
-
- add
-
+ [% tooltip_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-quad',
+ container_classes => 'd-grid',
+ } ) %]
--
GitLab
From 7c57930c289ec38743862432ad5d2cb7342d9704 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 10:19:20 +0100
Subject: [PATCH 14/33] Fix formatting
---
root/static/js/recipe/index.js | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/root/static/js/recipe/index.js b/root/static/js/recipe/index.js
index 663524c41..fa186decd 100644
--- a/root/static/js/recipe/index.js
+++ b/root/static/js/recipe/index.js
@@ -8,5 +8,13 @@ const setDuplicateFormValues = (actionURL, name) => {
recipeNameBodyElement.textContent = name;
}
-document.getElementById("duplicate-recipe-modal").addEventListener("shown.bs.modal", () => document.getElementById("new-name").focus());
-document.getElementById("create-recipe-modal").addEventListener("shown.bs.modal", () => document.getElementById("create-name").focus());
+document
+ .getElementById("duplicate-recipe-modal")
+ .addEventListener("shown.bs.modal", () =>
+ document.getElementById("new-name").focus()
+ );
+document
+ .getElementById("create-recipe-modal")
+ .addEventListener("shown.bs.modal", () =>
+ document.getElementById("create-name").focus()
+ );
--
GitLab
From a0837ee557b60496f46bf586aaf7f66c5fe7520c Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 10:20:02 +0100
Subject: [PATCH 15/33] Use button macro on recipe pages
---
root/static/js/recipe/index.js | 13 +++--
root/templates/recipe/edit.tt | 59 ++++++++++++++++-----
root/templates/recipe/importable_recipes.tt | 14 +++--
root/templates/recipe/index.tt | 54 ++++++++++++++-----
4 files changed, 109 insertions(+), 31 deletions(-)
diff --git a/root/static/js/recipe/index.js b/root/static/js/recipe/index.js
index fa186decd..8d98d25c7 100644
--- a/root/static/js/recipe/index.js
+++ b/root/static/js/recipe/index.js
@@ -2,11 +2,18 @@ const formElement = document.getElementById("duplicate-form");
const recipeNameTitleElement = document.getElementById("recipe-name-title");
const recipeNameBodyElement = document.getElementById("recipe-name-body");
-const setDuplicateFormValues = (actionURL, name) => {
- formElement.setAttribute("action", actionURL);
+const setDuplicateFormValues = (e) => {
+ const url = e.currentTarget.dataset.url;
+ const name = e.currentTarget.dataset.name;
+
+ formElement.setAttribute("action", url);
recipeNameTitleElement.textContent = name;
recipeNameBodyElement.textContent = name;
-}
+};
+
+document
+ .querySelectorAll(".duplicate-trigger")
+ .forEach((elem) => elem.addEventListener("click", setDuplicateFormValues));
document
.getElementById("duplicate-recipe-modal")
diff --git a/root/templates/recipe/edit.tt b/root/templates/recipe/edit.tt
index db31b1401..f696f79dd 100644
--- a/root/templates/recipe/edit.tt
+++ b/root/templates/recipe/edit.tt
@@ -10,22 +10,50 @@ IF import_url %]
-
- Used in [% dishes.size %] dish[% dishes.size != 1 ? "es" : "" %]
-
+ [%
+ 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 %]
-
- Share link
-
- [% END;
- IF import_url %]
-
- Import into another project east
-
+ [% 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;
+ tooltip_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 %]
- Update Recipe
+ [% button( {
+ text => 'Update recipe',
+ attrs => {
+ type => 'submit',
+ },
+ } ) %]
diff --git a/root/templates/recipe/importable_recipes.tt b/root/templates/recipe/importable_recipes.tt
index 27aa33085..1033f0667 100644
--- a/root/templates/recipe/importable_recipes.tt
+++ b/root/templates/recipe/importable_recipes.tt
@@ -13,9 +13,17 @@
[% IF recipe.import_url %]
-
- east Import
-
+ [%
+ project_name = project.name | html;
+ tooltip_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 dbcb46de0..34662f37c 100644
--- a/root/templates/recipe/index.tt
+++ b/root/templates/recipe/index.tt
@@ -4,12 +4,25 @@ js_push_template_path();
new = BLOCK %]
-
- add New recipe
-
-
- east Import recipe
-
+[%
+ tooltip_button( {
+ icon => 'add',
+ text => 'New recipe',
+ tooltip => { content => 'Add new recipe' },
+ attrs => {
+ 'data-bs-toggle' => 'modal',
+ 'data-bs-target' => '#create-recipe-modal',
+ },
+ } );
+
+ tooltip_button( {
+ icon => 'east',
+ text => 'Import recipe',
+ variant => 'secondary',
+ href => import_recipe_url,
+ tooltip => { content => 'Import recipe' },
+ } );
+%]
[% END; new %]
@@ -34,13 +47,30 @@ new = BLOCK %]
-
- file_copy
-
+ [%
+ recipe_name = recipe.name | html;
+ tooltip_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',
+ } )
+ %]
-
- delete_forever
-
+ [% button( {
+ icon => 'delete_forever',
+ variant => 'outline-danger',
+ attrs => {
+ type => 'submit',
+ },
+ button_classes => 'btn-sm',
+ } ) %]
--
GitLab
From 862554472f2cef1f4cff67dfdedf751bdd2f564f Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 10:23:03 +0100
Subject: [PATCH 16/33] Use button macro on sign in page
---
root/templates/session/login.tt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/root/templates/session/login.tt b/root/templates/session/login.tt
index 7a607ec48..467997ec5 100644
--- a/root/templates/session/login.tt
+++ b/root/templates/session/login.tt
@@ -20,7 +20,9 @@
Remember username for next login
-
+ [% button( {
+ text => 'Sign in',
+ } ) %]
--
GitLab
From d7ef4b8bcd0f11778ccb42225cd8abc65839c386 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 11:06:12 +0100
Subject: [PATCH 17/33] Use button macro on setting pages
---
root/templates/settings/account.tt | 47 ++++++++++++++----------
root/templates/settings/organizations.tt | 34 +++++++++++------
2 files changed, 51 insertions(+), 30 deletions(-)
diff --git a/root/templates/settings/account.tt b/root/templates/settings/account.tt
index 4d00b4a35..c1b2fdc9a 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');
Display name
-
+ [% button( {
+ text => 'Change display name',
+ } ) %]
@@ -67,7 +71,10 @@ js.push('/lib/zxcvbn.js');
Confirm new password
-
+ [% 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');
New email address
-
+ [% button( {
+ text => 'Change email address',
+ } ) %]
diff --git a/root/templates/settings/organizations.tt b/root/templates/settings/organizations.tt
index dd9a0711d..6dba001ba 100644
--- a/root/templates/settings/organizations.tt
+++ b/root/templates/settings/organizations.tt
@@ -17,8 +17,8 @@ BLOCK new_org %]
[% button( {
text => 'Create organization',
attrs => {
- name => 'create'
- }
+ name => 'create',
+ },
} ) %]
@@ -33,21 +33,33 @@ BLOCK user_item %]
[% link_organization(item.organization) %] ([% item.role %])
[% IF item.leave_url %]
-
- logout Leave organization
-
+ [% button( {
+ text => 'Leave organization',
+ icon => 'logout',
+ variant => 'outline-danger',
+ } ) %]
-[% ELSE %]
-
- logout Leave organization
-
-[% END;
+[% ELSE;
+ tooltip_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;
--
GitLab
From aef467221d1fd9bef9208b2bfba269dab3570f91 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 11:24:50 +0100
Subject: [PATCH 18/33] Use button macro on shop section page
---
root/templates/shop_section/index.tt | 55 ++++++++++++++++++++--------
1 file changed, 39 insertions(+), 16 deletions(-)
diff --git a/root/templates/shop_section/index.tt b/root/templates/shop_section/index.tt
index e879e9a3b..284956459 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 %]
-
- add
-
+ [% tooltip_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-quad',
+ } ) %]
[% END %]
@@ -28,20 +38,33 @@ BLOCK new_section %]
[% section.articles_count %]
-
- edit
-
- [% IF section.delete_url %]
-
-
- delete_forever
-
+ [%
+ section_name = section.name | html;
+ tooltip_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',
+ } )
+ %]
+
+ [% tooltip_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 %]
-
- delete_forever
-
- [% END %]
--
GitLab
From 694588510f6636d4bc796fe11d60572f6a94e0e7 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 12:09:14 +0100
Subject: [PATCH 19/33] Use button macro on tag pages
---
root/templates/tag/edit.tt | 30 ++++----
root/templates/tag/index.tt | 133 ++++++++++++++++++++----------------
2 files changed, 91 insertions(+), 72 deletions(-)
diff --git a/root/templates/tag/edit.tt b/root/templates/tag/edit.tt
index d500619e4..23310e3d1 100644
--- a/root/templates/tag/edit.tt
+++ b/root/templates/tag/edit.tt
@@ -10,20 +10,26 @@ js_push_template_path() %]
-
- edit Edit
-
- [% IF is_in_use %]
-
- delete_forever Delete
-
- [% ELSE %]
+ [% button( {
+ text => 'Edit',
+ icon => 'edit',
+ attrs => {
+ 'data-bs-toggle' => 'modal',
+ 'data-bs-target' => '#edit-tag',
+ },
+ button_classes => 'update',
+ } ) %]
-
- delete_forever Delete
-
+ [% 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 23964924d..02c73c14a 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();
-
-
- add New Tag
-
-
- add New Tag group
-
-
+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 %]
@@ -83,18 +103,11 @@ js_push_template_path() %]
[% END %]
-[% END %]
+[% END;
-
-
- add New Tag
-
-
- add New Tag group
-
-
+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.
--
GitLab
From 8468724f83faf64ea339bf3a2bd4a414d1a073d2 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 13:04:33 +0100
Subject: [PATCH 20/33] Use button macro on unit pages
---
root/templates/unit/edit.tt | 31 +++++++++++++++++++++++--------
root/templates/unit/index.tt | 30 ++++++++++++++++++------------
2 files changed, 41 insertions(+), 20 deletions(-)
diff --git a/root/templates/unit/edit.tt b/root/templates/unit/edit.tt
index 583985fca..2c2141bcf 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 %]
-
Update
- [% 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 %]
- Delete
+ [% 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 ad0a6ff45..e99acf51e 100644
--- a/root/templates/unit/index.tt
+++ b/root/templates/unit/index.tt
@@ -4,9 +4,14 @@ js_push_template_path();
new = BLOCK %]
-
- add New unit
-
+ [% button( {
+ text => 'New unit',
+ icon => 'add',
+ attrs => {
+ 'data-bs-toggle' => 'modal',
+ 'data-bs-target' => '#create-unit-modal',
+ },
+ } ) %]
[% END; new %]
@@ -47,17 +52,18 @@ new = BLOCK %]
[% END;
IF loop.first %]
- [% IF unit.delete_url %]
-
- delete_forever
-
+ [% tooltip_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 %]
-
- delete_forever
-
- [% END %]
[% END %]
--
GitLab
From e85209167f564e4c6dd58c1cc6fe7d1e54dbc6ca Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 13:24:04 +0100
Subject: [PATCH 21/33] Use button macro on user pages
---
root/templates/user/recover.tt | 5 ++++-
root/templates/user/register.tt | 5 ++++-
root/templates/user/reset_password.tt | 5 ++++-
root/templates/user/show.tt | 22 ++++++++++++++++------
4 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/root/templates/user/recover.tt b/root/templates/user/recover.tt
index 69e808dd6..d156d2437 100644
--- a/root/templates/user/recover.tt
+++ b/root/templates/user/recover.tt
@@ -14,7 +14,10 @@
Email address
-
+
+ [% button( {
+ text => 'Send recovery link',
+ } ) %]
diff --git a/root/templates/user/register.tt b/root/templates/user/register.tt
index 4b133ed08..c989dff98 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 bdab62d3a..43adcc6e5 100644
--- a/root/templates/user/reset_password.tt
+++ b/root/templates/user/reset_password.tt
@@ -23,7 +23,10 @@ js.push('/lib/zxcvbn.js');
Confirm new password
-
+
+ [% button( {
+ text => 'Reset password',
+ } ) %]
diff --git a/root/templates/user/show.tt b/root/templates/user/show.tt
index bab048182..0969b2970 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) %]
--
GitLab
From 108247bc30eda08f68235adc7bc1c07494c6e335 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Sun, 2 Mar 2025 13:25:54 +0100
Subject: [PATCH 22/33] Use button macro on dashboard page
---
root/templates/dashboard.tt | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/root/templates/dashboard.tt b/root/templates/dashboard.tt
index c95272a86..e0aa0b256 100644
--- a/root/templates/dashboard.tt
+++ b/root/templates/dashboard.tt
@@ -16,7 +16,9 @@
lock private
-
+ [% button( {
+ text => 'Create project',
+ } ) %]
--
GitLab
From 3b290d84c084accee9e190874a2f7b4bfaffb50c Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Mon, 24 Mar 2025 17:11:30 +0100
Subject: [PATCH 23/33] Rename btn-quad to btn-square and btn-no-quad to
btn-no-square
---
root/common_templates/macros.tt | 2 +-
root/static/css/style.css | 8 ++++----
root/templates/faq/index.tt | 10 ++++++----
root/templates/organization/members.tt | 2 +-
root/templates/project/show.tt | 2 +-
root/templates/purchase_list/index.tt | 2 +-
root/templates/shop_section/index.tt | 2 +-
root/templates/terms/show.tt | 8 ++++----
8 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/root/common_templates/macros.tt b/root/common_templates/macros.tt
index bf5ba0a67..741b419f0 100644
--- a/root/common_templates/macros.tt
+++ b/root/common_templates/macros.tt
@@ -132,7 +132,7 @@ MACRO button(opts) BLOCK;
icon = '' _ opts.icon _ ' ';
END ~%]
<[% opts.href ? 'a' : 'button' %]
- class="btn btn-[% opts.variant || 'primary' %] [% 'btn-quad' IF !opts.text %] d-inline-flex justify-content-center gap-1 [% opts.button_classes.list.join(' ') %]"
+ class="btn btn-[% opts.variant || 'primary' %] [% 'btn-square' IF !opts.text %] d-inline-flex justify-content-center gap-1 [% opts.button_classes.list.join(' ') %]"
[%~ ' disabled ' IF opts.disabled;
FOR attr IN opts.attrs;
attr.key %]="[% attr.value | html %]"
diff --git a/root/static/css/style.css b/root/static/css/style.css
index e95fba649..9d3576ab9 100644
--- a/root/static/css/style.css
+++ b/root/static/css/style.css
@@ -17,19 +17,19 @@ textarea {
resize: vertical;
}
-.btn-quad {
+.btn-square {
aspect-ratio: 1;
}
-.btn-no-quad {
+.btn-no-square {
aspect-ratio: unset;
}
-.btn.btn-quad {
+.btn.btn-square {
padding: 0.375em;
}
-.btn.btn-sm.btn-quad {
+.btn.btn-sm.btn-square {
padding: 0.25rem;
}
diff --git a/root/templates/faq/index.tt b/root/templates/faq/index.tt
index 0766303ec..96167fb89 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 %]
-
-
+
+ [% button( {
+ text => 'Edit FAQ',
+ variant => 'outline-primary',
+ href => admin_faq_url,
+ } ) %]
[% END;
diff --git a/root/templates/organization/members.tt b/root/templates/organization/members.tt
index a39db3db9..9968c5db1 100644
--- a/root/templates/organization/members.tt
+++ b/root/templates/organization/members.tt
@@ -32,7 +32,7 @@ has_admin = 0 %]
[% END %]
[%# We don't use the button macro here. Because when we would use the macro the styling would break %]
-
done
+
done
[% ELSE %]
[% o_u.role %]
diff --git a/root/templates/project/show.tt b/root/templates/project/show.tt
index bdf24102d..23619e0b5 100644
--- a/root/templates/project/show.tt
+++ b/root/templates/project/show.tt
@@ -85,7 +85,7 @@ IF project.archived;
'data-bs-toggle' => 'modal',
'data-bs-target' => '#addCol',
},
- button_classes => 'btn-sm btn-no-quad',
+ button_classes => 'btn-sm btn-no-square',
container_classes => 'd-grid',
} ) %]
diff --git a/root/templates/purchase_list/index.tt b/root/templates/purchase_list/index.tt
index 2a5f72375..d77603849 100644
--- a/root/templates/purchase_list/index.tt
+++ b/root/templates/purchase_list/index.tt
@@ -18,7 +18,7 @@ BLOCK new_list %]
'data-bs-target' => '#addList',
},
container_classes => 'd-grid',
- button_classes => 'btn-no-quad',
+ button_classes => 'btn-no-square',
} ) %]
diff --git a/root/templates/shop_section/index.tt b/root/templates/shop_section/index.tt
index 284956459..0917613bd 100644
--- a/root/templates/shop_section/index.tt
+++ b/root/templates/shop_section/index.tt
@@ -16,7 +16,7 @@ BLOCK new_section %]
'data-bs-target' => '#add-section',
},
container_classes => 'd-grid',
- button_classes => 'btn-no-quad',
+ button_classes => 'btn-no-square',
} ) %]
diff --git a/root/templates/terms/show.tt b/root/templates/terms/show.tt
index 9f829f119..d18758b92 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 %]
--
GitLab
From 5a7a6848808834981f9899f355bfccfb01893039 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20B=C3=B6hmer?=
Date: Sat, 14 Sep 2024 15:02:51 +0200
Subject: [PATCH 24/33] Move global nav links to 1st bar and project nav links
to 2nd bar
---
root/templates/wrapper.tt | 55 +++++++++++++++------------------------
1 file changed, 21 insertions(+), 34 deletions(-)
diff --git a/root/templates/wrapper.tt b/root/templates/wrapper.tt
index 3ff1b4c68..49fef2315 100644
--- a/root/templates/wrapper.tt
+++ b/root/templates/wrapper.tt
@@ -59,23 +59,8 @@
Home
- [% IF project_urls %]
- Project
- Recipes [% inventory.recipes %]
- Articles [% inventory.articles %]
- Tags [% inventory.tags %]
- Purchase Lists [% inventory.purchase_lists %]
- Units [% inventory.units %]
- [% IF user %]
-
- [% IF project_urls.import %]
- Import
- [% ELSE %]
- Import
- [% END %]
-
- [% END %]
- [% END %]
+ Recipes
+ Projects
[% IF submenu_items.size > 0 %]
@@ -141,6 +126,25 @@
@@ -197,14 +192,6 @@
-
-
Get Help
--
GitLab
From b98c38a4dc81c1e628c3d16144c80e73f4c97014 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20B=C3=B6hmer?=
Date: Sat, 14 Sep 2024 15:40:13 +0200
Subject: [PATCH 25/33] Move project main navigation to project homepage
---
lib/Coocook/Controller/Dish.pm | 2 +-
lib/Coocook/Controller/Permission.pm | 2 +-
lib/Coocook/Controller/Project.pm | 24 +++++++-----------------
root/templates/project/edit.tt | 2 ++
root/templates/project/show.tt | 4 ++++
5 files changed, 15 insertions(+), 19 deletions(-)
diff --git a/lib/Coocook/Controller/Dish.pm b/lib/Coocook/Controller/Dish.pm
index 0a380a1fb..c26a991c3 100644
--- a/lib/Coocook/Controller/Dish.pm
+++ b/lib/Coocook/Controller/Dish.pm
@@ -16,7 +16,7 @@ Catalyst Controller.
=cut
-sub base : Chained('/project/submenu') PathPart('dish') CaptureArgs(1) {
+sub base : Chained('/project/base') PathPart('dish') CaptureArgs(1) {
my ( $self, $c, $id ) = @_;
$c->stash( dish => $c->project->dishes->find( $id, { prefetch => [ 'meal', 'recipe' ] } )
diff --git a/lib/Coocook/Controller/Permission.pm b/lib/Coocook/Controller/Permission.pm
index c47233cbc..d9162f67c 100644
--- a/lib/Coocook/Controller/Permission.pm
+++ b/lib/Coocook/Controller/Permission.pm
@@ -4,7 +4,7 @@ use Coocook::Base qw(Moose);
BEGIN { extends 'Coocook::Controller' }
-sub index : GET HEAD Chained('/project/submenu') PathPart('permissions') Args(0)
+sub index : GET HEAD Chained('/project/base') PathPart('permissions') Args(0)
RequiresCapability('view_project_permissions') {
my ( $self, $c ) = @_;
diff --git a/lib/Coocook/Controller/Project.pm b/lib/Coocook/Controller/Project.pm
index 1e6ecd03b..734bcbb31 100644
--- a/lib/Coocook/Controller/Project.pm
+++ b/lib/Coocook/Controller/Project.pm
@@ -113,21 +113,7 @@ sub base : Chained('/base') PathPart('project') CaptureArgs(2) {
}
}
-sub submenu : Chained('base') PathPart('') CaptureArgs(0) {
- my ( $self, $c ) = @_;
-
- $c->stash(
- submenu_items => [
- { text => "Show project", action => 'project/show' },
- { text => "Meals & Dishes", action => 'project/edit' },
- { text => "Permissions", action => 'permission/index' },
- { text => "Shop sections", action => 'shop_section/index' },
- { text => "Settings", action => 'project/settings' },
- ],
- );
-}
-
-sub show : GET HEAD Chained('submenu') PathPart('') Args(0) RequiresCapability('view_project') {
+sub show : GET HEAD Chained('base') PathPart('') Args(0) RequiresCapability('view_project') {
my ( $self, $c ) = @_;
my $days = $c->model('Plan')->project( $c->project );
@@ -144,13 +130,17 @@ sub show : GET HEAD Chained('submenu') PathPart('') Args(0) RequiresCapability('
}
$c->stash(
+ permissions_url => $c->project_uri('/permission/index'),
+ settings_url => $c->project_uri( $self->action_for('settings') ),
+ shop_sections_url => $c->project_uri('/shop_section/index'),
+
can_edit => !!$c->has_capability('edit_project'),
can_unarchive => !!$c->has_capability('unarchive_project'),
days => $days,
);
}
-sub edit : GET HEAD Chained('submenu') PathPart('edit') Args(0) RequiresCapability('edit_project') {
+sub edit : GET HEAD Chained('base') PathPart('edit') Args(0) RequiresCapability('edit_project') {
my ( $self, $c ) = @_;
my $default_date = DateTime->today;
@@ -195,7 +185,7 @@ sub edit : GET HEAD Chained('submenu') PathPart('edit') Args(0) RequiresCapabili
);
}
-sub settings : GET HEAD Chained('submenu') PathPart('settings') Args(0)
+sub settings : GET HEAD Chained('base') PathPart('settings') Args(0)
RequiresCapability('view_project_settings') {
my ( $self, $c ) = @_;
diff --git a/root/templates/project/edit.tt b/root/templates/project/edit.tt
index 2b42bb505..b93eb3cf4 100644
--- a/root/templates/project/edit.tt
+++ b/root/templates/project/edit.tt
@@ -4,6 +4,8 @@ css_push_template_path();
js.push('/lib/coocook-web-components/dist/meals-dishes-editor/meals-dishes-editor.es.js');
%]
+edit_off
+
[% WRAPPER includes/infobox.tt %]
diff --git a/root/templates/project/show.tt b/root/templates/project/show.tt
index 23619e0b5..33712a0bf 100644
--- a/root/templates/project/show.tt
+++ b/root/templates/project/show.tt
@@ -58,11 +58,15 @@ IF project.archived;
[% project.name | html %]
+
edit
[% tooltip_button( {
icon => 'print',
tooltip => { content => 'Print' },
attrs => { onclick => 'print()' },
} ) %]
+
list
+
manage_accounts
+
settings
--
GitLab
From 7da58b0f516e9307c575a127ecf42130dc9c6a48 Mon Sep 17 00:00:00 2001
From: Kurt Roscher
Date: Thu, 24 Oct 2024 19:49:18 +0200
Subject: [PATCH 26/33] Design improvements for new navigation
---
lib/Coocook/Controller/Admin.pm | 16 +-----
root/static/css/style.css | 39 +++++---------
root/templates/wrapper.tt | 90 ++++++++++++++++++++-------------
3 files changed, 71 insertions(+), 74 deletions(-)
diff --git a/lib/Coocook/Controller/Admin.pm b/lib/Coocook/Controller/Admin.pm
index bcd4a98de..a3345b158 100644
--- a/lib/Coocook/Controller/Admin.pm
+++ b/lib/Coocook/Controller/Admin.pm
@@ -7,21 +7,7 @@ BEGIN { extends 'Coocook::Controller' }
sub base : Chained('/base') PathPart('admin') CaptureArgs(0) {
my ( $self, $c ) = @_;
- my $admin_urls = $c->stash->{admin_urls};
-
- my @items = (
- { url => $c->stash->{admin_url}, text => "Admin" },
- { url => $admin_urls->{faq}, text => "FAQ" },
- { url => $admin_urls->{organizations}, text => "Organizations" },
- { url => $admin_urls->{projects}, text => "Projects" },
- { url => $admin_urls->{recipes}, text => "Recipes" },
- { url => $admin_urls->{terms}, text => "Terms" },
- { url => $admin_urls->{users}, text => "Users" },
- );
-
- $_->{url} or $_->{forbidden} = 1 for @items;
-
- $c->stash( submenu_items => \@items );
+ $c->stash( on_admin_pages => 1 );
}
sub index : GET HEAD Chained('base') PathPart('') Args(0) RequiresCapability('admin_view') {
diff --git a/root/static/css/style.css b/root/static/css/style.css
index 9d3576ab9..066fe664f 100644
--- a/root/static/css/style.css
+++ b/root/static/css/style.css
@@ -138,43 +138,32 @@ header h1 {
font-size: 2.5em;
}
-/*** project nav ***/
-#project_nav .nav-link:not(.disabled-import) {
+/*** main nav ***/
+#main_nav .nav-link:not(.disabled-import) {
color: var(--bs-light);
}
-.nav-link.disabled-import {
- color: #6c757d;
- user-select: none;
- cursor: pointer;
+.disabled-import {
+ opacity: 0.7;
}
-#project_nav a:hover,
-#project_nav a:focus {
- color: var(--bs-primary);
+#main_nav .nav-link:hover,
+#main_nav .nav-link:focus {
+ background: var(--bs-gray-700);
}
/*** submenu nav ***/
-#submenu_nav .nav-link:hover,
-#submenu_nav .nav-link.active,
-#submenu_nav .nav-link:focus {
+#sub_nav .nav-link:hover,
+#sub_nav .nav-link.active,
+#sub_nav .nav-link:focus {
color: var(--bs-light) !important;
background: var(--cc-green-dark);
}
-/*** login logout section ***/
-.login-logout-btn {
- background-color: transparent;
- border: 0px;
- -webkit-box-shadow: 0px 0px 0px rgba(255, 255, 255, 0) !important;
- -moz-box-shadow: 0px 0px 0px rgba(255, 255, 255, 0) !important;
- box-shadow: 0px 0px 0px rgba(255, 255, 255, 0) !important;
-}
-
-.login-logout-btn:active,
-.login-logout-btn:hover,
-.login-logout-btn:focus {
- color: var(--bs-light);
+.project-pill {
+ background: #d8fccf;
+ display: inline-block;
+ margin-left: 0.25em;
}
/******** end header part ********/
diff --git a/root/templates/wrapper.tt b/root/templates/wrapper.tt
index 49fef2315..ab50c6a78 100644
--- a/root/templates/wrapper.tt
+++ b/root/templates/wrapper.tt
@@ -57,37 +57,56 @@
-
+
Home
- Recipes
- Projects
+ Recipes
+ Projects
- [% IF submenu_items.size > 0 %]
+
+
+ [% IF project_urls || on_admin_pages %]
[% END %]
+
[% IF user %]
@@ -123,35 +142,38 @@
-