From dcbcf8148d9022d5427e1ac37b3f92bdf18dda27 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:33:36 +0100 Subject: [PATCH 1/7] Show a notification on Manage Jenkins page save --- core/src/main/java/hudson/Functions.java | 6 +++ .../security/GlobalSecurityConfiguration.java | 2 +- core/src/main/java/hudson/util/FormApply.java | 52 +++++++++++++++++++ .../main/java/jenkins/agents/CloudSet.java | 2 +- .../AppearanceGlobalConfiguration.java | 2 +- core/src/main/java/jenkins/model/Jenkins.java | 2 +- .../tools/GlobalToolConfiguration.java | 2 +- .../main/resources/lib/layout/layout.jelly | 5 +- src/main/js/app.js | 14 +++++ .../java/jenkins/bugs/Jenkins64991Test.java | 2 +- 10 files changed, 82 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 391ed0389ea7..08c1cce3c327 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -86,6 +86,7 @@ import hudson.tasks.Publisher; import hudson.tasks.UserAvatarResolver; import hudson.util.Area; +import hudson.util.FormApply; import hudson.util.FormValidation.CheckMethod; import hudson.util.HudsonIsLoading; import hudson.util.HudsonIsRestarting; @@ -217,6 +218,11 @@ public class Functions { public Functions() { } + @Restricted(NoExternalUse.class) + public @CheckForNull FormApply.Notification getFormApplyNotification() { + return FormApply.getAndClearNotification(Stapler.getCurrentRequest2()); + } + /** * Generates an unique ID. */ diff --git a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java index ae90b0428c7b..d1fd4bdcca6d 100644 --- a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java +++ b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java @@ -108,7 +108,7 @@ public synchronized void doConfigure(StaplerRequest2 req, StaplerResponse2 rsp) boolean result = configure(req, json); LOGGER.log(Level.FINE, "security saved: " + result); Jenkins.get().save(); - FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null); + FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null); } catch (JSONException x) { LOGGER.warning(() -> "Bad JSON:\n" + json.toString(2)); throw x; diff --git a/core/src/main/java/hudson/util/FormApply.java b/core/src/main/java/hudson/util/FormApply.java index 3d5f458ec126..ed6d63931288 100644 --- a/core/src/main/java/hudson/util/FormApply.java +++ b/core/src/main/java/hudson/util/FormApply.java @@ -24,8 +24,10 @@ package hudson.util; +import edu.umd.cs.findbugs.annotations.CheckForNull; import hudson.Functions; import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpSession; import java.io.IOException; import jenkins.model.Jenkins; import org.kohsuke.stapler.HttpResponses.HttpResponseException; @@ -40,6 +42,9 @@ * @since 1.453 */ public class FormApply { + private static final String NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE = FormApply.class.getName() + ".notificationMessage"; + private static final String NOTIFICATION_TYPE_SESSION_ATTRIBUTE = FormApply.class.getName() + ".notificationType"; + /** * Generates the response for the form submission in such a way that it handles the "apply" button * correctly. @@ -56,6 +61,7 @@ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object n showNotification(Messages.HttpResponses_Saved(), NotificationType.SUCCESS) .generateResponse(req, rsp, node); } else { + setNotificationInSession(req, Messages.HttpResponses_Saved(), NotificationType.SUCCESS); rsp.sendRedirect(destination); } } @@ -127,6 +133,52 @@ public void generateResponse(StaplerRequest2 req, StaplerResponse2 rsp, Object n }; } + private static void setNotificationInSession(StaplerRequest2 req, String message, NotificationType notificationType) { + HttpSession session = req.getSession(); + session.setAttribute(NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE, message); + session.setAttribute(NOTIFICATION_TYPE_SESSION_ATTRIBUTE, notificationType.name()); + } + + public static @CheckForNull Notification getAndClearNotification(StaplerRequest2 req) { + HttpSession session = req.getSession(false); + if (session == null) { + return null; + } + + String message = (String) session.getAttribute(NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE); + String notificationType = (String) session.getAttribute(NOTIFICATION_TYPE_SESSION_ATTRIBUTE); + session.removeAttribute(NOTIFICATION_MESSAGE_SESSION_ATTRIBUTE); + session.removeAttribute(NOTIFICATION_TYPE_SESSION_ATTRIBUTE); + + if (message == null || notificationType == null) { + return null; + } + + try { + return new Notification(message, NotificationType.valueOf(notificationType)); + } catch (IllegalArgumentException e) { + return null; + } + } + + public static final class Notification { + private final String message; + private final NotificationType notificationType; + + private Notification(String message, NotificationType notificationType) { + this.message = message; + this.notificationType = notificationType; + } + + public String getMessage() { + return message; + } + + public NotificationType getNotificationType() { + return notificationType; + } + } + /** * Corresponds to types declared in index.js diff --git a/core/src/main/java/jenkins/agents/CloudSet.java b/core/src/main/java/jenkins/agents/CloudSet.java index cbd5cc975d89..d458d7ac549c 100644 --- a/core/src/main/java/jenkins/agents/CloudSet.java +++ b/core/src/main/java/jenkins/agents/CloudSet.java @@ -285,7 +285,7 @@ public void doReorder(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExcept var clouds = new ArrayList<>(Jenkins.get().clouds); clouds.sort(Comparator.comparingInt(c -> getIndexOf(namesList, c))); Jenkins.get().clouds.replaceBy(clouds); - FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null); + FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null); } private static int getIndexOf(List namesList, Cloud cloud) { diff --git a/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java b/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java index 1208043b60cd..33d764d0cc04 100644 --- a/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java +++ b/core/src/main/java/jenkins/appearance/AppearanceGlobalConfiguration.java @@ -101,7 +101,7 @@ public Category getCategory() { public synchronized void doConfigure(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException, Descriptor.FormException { boolean result = configure(req, req.getSubmittedForm()); LOGGER.log(Level.FINE, "appearance saved: " + result); - FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null); + FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null); } private boolean configure(StaplerRequest2 req, JSONObject json) throws Descriptor.FormException, IOException { diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 7cfac6f3b3c6..f4c009f9e397 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -4045,7 +4045,7 @@ public synchronized void doConfigSubmit(StaplerRequest2 req, StaplerResponse2 rs save(); updateComputers(this); if (result) - FormApply.success(req.getContextPath() + '/').generateResponse(req, rsp, null); + FormApply.success(req.getContextPath() + "/manage/configure").generateResponse(req, rsp, null); else FormApply.success("configure").generateResponse(req, rsp, null); // back to config diff --git a/core/src/main/java/jenkins/tools/GlobalToolConfiguration.java b/core/src/main/java/jenkins/tools/GlobalToolConfiguration.java index 47208f942958..d00906363b09 100644 --- a/core/src/main/java/jenkins/tools/GlobalToolConfiguration.java +++ b/core/src/main/java/jenkins/tools/GlobalToolConfiguration.java @@ -84,7 +84,7 @@ public Category getCategory() { public synchronized void doConfigure(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException, Descriptor.FormException { boolean result = configure(req, req.getSubmittedForm()); LOGGER.log(Level.FINE, "tools saved: " + result); - FormApply.success(req.getContextPath() + "/manage").generateResponse(req, rsp, null); + FormApply.success(req.getContextPath() + "/manage/" + getUrlName()).generateResponse(req, rsp, null); } private boolean configure(StaplerRequest2 req, JSONObject json) throws Descriptor.FormException, IOException { diff --git a/core/src/main/resources/lib/layout/layout.jelly b/core/src/main/resources/lib/layout/layout.jelly index 0e2dac0f47e8..399aade7341e 100644 --- a/core/src/main/resources/lib/layout/layout.jelly +++ b/core/src/main/resources/lib/layout/layout.jelly @@ -91,6 +91,7 @@ THE SOFTWARE. + ${h.advertiseHeaders(response2)} @@ -155,7 +156,9 @@ THE SOFTWARE. + data-search-help-url="${%searchBox.url}" + data-notification-message="${formApplyNotification.message}" + data-notification-type="${formApplyNotification.notificationType}"> diff --git a/src/main/js/app.js b/src/main/js/app.js index c21991f6e826..819374500744 100644 --- a/src/main/js/app.js +++ b/src/main/js/app.js @@ -9,11 +9,25 @@ import ConfirmationLink from "@/components/confirmation-link"; import Dialogs from "@/components/dialogs"; import Defer from "@/components/defer"; +function showPageLoadNotification() { + const { notificationMessage, notificationType } = document.body.dataset; + if (!notificationMessage) { + return; + } + + const options = + (notificationType && window.notificationBar[notificationType]) || undefined; + window.notificationBar.show(notificationMessage, options); + delete document.body.dataset.notificationMessage; + delete document.body.dataset.notificationType; +} + AppBar.init(); Dropdowns.init(); CommandPalette.init(); Defer.init(); Notifications.init(); +showPageLoadNotification(); SearchBar.init(); Tooltips.init(); StopButtonLink.init(); diff --git a/test/src/test/java/jenkins/bugs/Jenkins64991Test.java b/test/src/test/java/jenkins/bugs/Jenkins64991Test.java index 7ff236736224..1a4bd4a457e7 100644 --- a/test/src/test/java/jenkins/bugs/Jenkins64991Test.java +++ b/test/src/test/java/jenkins/bugs/Jenkins64991Test.java @@ -85,7 +85,7 @@ void test403Redirect() throws Exception { final Page redirectedPage = HtmlFormUtil.submit(loginPage.getFormByName("login")); assertTrue(redirectedPage.isHtmlPage()); - assertEquals(j.getURL() + "manage/", redirectedPage.getUrl().toExternalForm()); + assertEquals(j.getURL() + "manage/configure", redirectedPage.getUrl().toExternalForm()); assertThat(redirectedPage.getWebResponse().getContentAsString(), containsStringIgnoringCase(Messages.GlobalSecurityConfiguration_DisplayName())); } From 96869ff90bae38790470325c021fa39d2b667fec Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:03:04 +0100 Subject: [PATCH 2/7] Update Jenkins64991Test.java --- test/src/test/java/jenkins/bugs/Jenkins64991Test.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/test/java/jenkins/bugs/Jenkins64991Test.java b/test/src/test/java/jenkins/bugs/Jenkins64991Test.java index 1a4bd4a457e7..7ff236736224 100644 --- a/test/src/test/java/jenkins/bugs/Jenkins64991Test.java +++ b/test/src/test/java/jenkins/bugs/Jenkins64991Test.java @@ -85,7 +85,7 @@ void test403Redirect() throws Exception { final Page redirectedPage = HtmlFormUtil.submit(loginPage.getFormByName("login")); assertTrue(redirectedPage.isHtmlPage()); - assertEquals(j.getURL() + "manage/configure", redirectedPage.getUrl().toExternalForm()); + assertEquals(j.getURL() + "manage/", redirectedPage.getUrl().toExternalForm()); assertThat(redirectedPage.getWebResponse().getContentAsString(), containsStringIgnoringCase(Messages.GlobalSecurityConfiguration_DisplayName())); } From 1d4e92619e44538601f5dde3858e33d9c2501960 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:06:05 +0100 Subject: [PATCH 3/7] Update CspHeaderDeciderTest.java --- .../java/jenkins/security/csp/impl/CspHeaderDeciderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java b/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java index 67e41556506d..65da3adfa05c 100644 --- a/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java +++ b/test/src/test/java/jenkins/security/csp/impl/CspHeaderDeciderTest.java @@ -234,7 +234,7 @@ public void testFallbackAdminMonitorAndSetup(JenkinsRule j) throws IOException, final Page afterSavingPage = HtmlFormUtil.submit(setupPage.getFormByName("config"), setupPage.getFormByName("config").getButtonByName("Submit")); assertThat(afterSavingPage, instanceOf(HtmlPage.class)); - assertThat(afterSavingPage.getUrl().getPath(), is(j.contextPath + "/manage/")); + assertThat(afterSavingPage.getUrl().getPath(), is(j.contextPath + "/manage/configureSecurity/")); assertThat(afterSavingPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy"), not(nullValue())); assertThat(afterSavingPage.getWebResponse().getResponseHeaderValue("Content-Security-Policy-Report-Only"), nullValue()); From c085134b8fa8c266bfadc862bfe32adf976b8b79 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:10:04 +0100 Subject: [PATCH 4/7] Tidy up --- src/main/js/app.js | 14 -------------- src/main/js/components/notifications/index.js | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/main/js/app.js b/src/main/js/app.js index 819374500744..c21991f6e826 100644 --- a/src/main/js/app.js +++ b/src/main/js/app.js @@ -9,25 +9,11 @@ import ConfirmationLink from "@/components/confirmation-link"; import Dialogs from "@/components/dialogs"; import Defer from "@/components/defer"; -function showPageLoadNotification() { - const { notificationMessage, notificationType } = document.body.dataset; - if (!notificationMessage) { - return; - } - - const options = - (notificationType && window.notificationBar[notificationType]) || undefined; - window.notificationBar.show(notificationMessage, options); - delete document.body.dataset.notificationMessage; - delete document.body.dataset.notificationType; -} - AppBar.init(); Dropdowns.init(); CommandPalette.init(); Defer.init(); Notifications.init(); -showPageLoadNotification(); SearchBar.init(); Tooltips.init(); StopButtonLink.init(); diff --git a/src/main/js/components/notifications/index.js b/src/main/js/components/notifications/index.js index 39bbfe5d95cc..2762920614f1 100644 --- a/src/main/js/components/notifications/index.js +++ b/src/main/js/components/notifications/index.js @@ -73,6 +73,21 @@ function init() { } }, }; + + tryShowPageLoadNotification(); +} + +function tryShowPageLoadNotification() { + const { notificationMessage, notificationType } = document.body.dataset; + if (!notificationMessage) { + return; + } + + const options = + (notificationType && window.notificationBar[notificationType]) || undefined; + window.notificationBar.show(notificationMessage, options); + delete document.body.dataset.notificationMessage; + delete document.body.dataset.notificationType; } export default { init }; From 4d9179e00f7bc1be1dddadcfaaa799d309a00a03 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:13:26 +0100 Subject: [PATCH 5/7] Tidy up --- core/src/main/java/hudson/util/FormApply.java | 6 +----- src/main/js/components/notifications/index.js | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/hudson/util/FormApply.java b/core/src/main/java/hudson/util/FormApply.java index ed6d63931288..7be1337e9004 100644 --- a/core/src/main/java/hudson/util/FormApply.java +++ b/core/src/main/java/hudson/util/FormApply.java @@ -154,11 +154,7 @@ private static void setNotificationInSession(StaplerRequest2 req, String message return null; } - try { - return new Notification(message, NotificationType.valueOf(notificationType)); - } catch (IllegalArgumentException e) { - return null; - } + return new Notification(message, NotificationType.valueOf(notificationType)); } public static final class Notification { diff --git a/src/main/js/components/notifications/index.js b/src/main/js/components/notifications/index.js index 2762920614f1..adfc3476f0dd 100644 --- a/src/main/js/components/notifications/index.js +++ b/src/main/js/components/notifications/index.js @@ -79,15 +79,15 @@ function init() { function tryShowPageLoadNotification() { const { notificationMessage, notificationType } = document.body.dataset; + if (!notificationMessage) { return; } - const options = - (notificationType && window.notificationBar[notificationType]) || undefined; - window.notificationBar.show(notificationMessage, options); - delete document.body.dataset.notificationMessage; - delete document.body.dataset.notificationType; + window.notificationBar.show( + notificationMessage, + window.notificationBar[notificationType], + ); } export default { init }; From e5d79952b2611bc098805e0c7a7b1e2b026c03b9 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:14:48 +0100 Subject: [PATCH 6/7] Update FormApply.java --- core/src/main/java/hudson/util/FormApply.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/hudson/util/FormApply.java b/core/src/main/java/hudson/util/FormApply.java index 7be1337e9004..ee7ebbd67424 100644 --- a/core/src/main/java/hudson/util/FormApply.java +++ b/core/src/main/java/hudson/util/FormApply.java @@ -175,7 +175,6 @@ public NotificationType getNotificationType() { } } - /** * Corresponds to types declared in index.js */ From 4869792c753266b708305fab8a2019c1e88222a3 Mon Sep 17 00:00:00 2001 From: Jan Faracik <43062514+janfaracik@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:53:48 +0100 Subject: [PATCH 7/7] Add save bar, only visible in experimental UI --- .../GlobalSecurityConfiguration/index.groovy | 14 +++--- .../AppearanceGlobalConfiguration/index.jelly | 14 +++--- .../jenkins/model/Jenkins/configure.jelly | 9 +++- .../GlobalToolConfiguration/index.groovy | 13 +++--- .../src/main/resources/lib/form/saveBar.jelly | 43 +++++++++++++++++++ 5 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 core/src/main/resources/lib/form/saveBar.jelly diff --git a/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy b/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy index c9555cf750b4..4dd1072fc3be 100644 --- a/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy +++ b/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy @@ -5,10 +5,12 @@ import hudson.markup.MarkupFormatterDescriptor import hudson.security.AuthorizationStrategy import hudson.Functions import hudson.model.Descriptor +import jenkins.model.experimentalflags.UserExperimentalFlag def f=namespace(lib.FormTagLib) def l=namespace(lib.LayoutTagLib) def st=namespace("jelly:stapler") +def newManageJenkins = UserExperimentalFlag.getFlagValueForCurrentUser("jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag") l.'settings-subpage'(permission: app.SYSTEM_READ) { set("readOnlyMode", !app.hasPermission(app.ADMINISTER)) @@ -55,15 +57,11 @@ l.'settings-subpage'(permission: app.SYSTEM_READ) { } l.isAdmin() { - f.bottomButtonBar { - f.submit(value: _("Save")) - f.apply() + if (newManageJenkins) { + f.saveBar() + } else { + f.saveApplyBar() } } } - - l.isAdmin() { - st.adjunct(includes: "lib.form.confirm") - } } - diff --git a/core/src/main/resources/jenkins/appearance/AppearanceGlobalConfiguration/index.jelly b/core/src/main/resources/jenkins/appearance/AppearanceGlobalConfiguration/index.jelly index 14316bec25a9..fff488e20e3d 100644 --- a/core/src/main/resources/jenkins/appearance/AppearanceGlobalConfiguration/index.jelly +++ b/core/src/main/resources/jenkins/appearance/AppearanceGlobalConfiguration/index.jelly @@ -46,6 +46,7 @@ THE SOFTWARE. + @@ -71,12 +72,15 @@ THE SOFTWARE. - + + + + + + + + - - - - diff --git a/core/src/main/resources/jenkins/model/Jenkins/configure.jelly b/core/src/main/resources/jenkins/model/Jenkins/configure.jelly index eeeb6906b433..980b48673c23 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/configure.jelly +++ b/core/src/main/resources/jenkins/model/Jenkins/configure.jelly @@ -71,7 +71,14 @@ THE SOFTWARE. - + + + + + + + + diff --git a/core/src/main/resources/jenkins/tools/GlobalToolConfiguration/index.groovy b/core/src/main/resources/jenkins/tools/GlobalToolConfiguration/index.groovy index 26960945eed6..f8a918842ff6 100644 --- a/core/src/main/resources/jenkins/tools/GlobalToolConfiguration/index.groovy +++ b/core/src/main/resources/jenkins/tools/GlobalToolConfiguration/index.groovy @@ -2,10 +2,12 @@ package jenkins.tools.GlobalToolConfiguration import hudson.Functions import hudson.model.Descriptor +import jenkins.model.experimentalflags.UserExperimentalFlag def f=namespace(lib.FormTagLib) def l=namespace(lib.LayoutTagLib) def st=namespace("jelly:stapler") +def newManageJenkins = UserExperimentalFlag.getFlagValueForCurrentUser("jenkins.model.experimentalflags.NewManageJenkinsUserExperimentalFlag") l.'settings-subpage'(permission: app.SYSTEM_READ) { set("readOnlyMode", !app.hasPermission(app.ADMINISTER)) @@ -22,14 +24,11 @@ l.'settings-subpage'(permission: app.SYSTEM_READ) { } l.isAdmin() { - f.bottomButtonBar { - f.submit(value: _("Save")) - f.apply(value: _("Apply")) + if (newManageJenkins) { + f.saveBar() + } else { + f.saveApplyBar() } } } - - l.isAdmin() { - st.adjunct(includes: "lib.form.confirm") - } } diff --git a/core/src/main/resources/lib/form/saveBar.jelly b/core/src/main/resources/lib/form/saveBar.jelly new file mode 100644 index 000000000000..0b1118bcc41f --- /dev/null +++ b/core/src/main/resources/lib/form/saveBar.jelly @@ -0,0 +1,43 @@ + + + + + + Creates the bottom bar for the "Save" button. + In read only mode (<j:set var="readOnlyMode" value="true"/>) the button is not displayed. + + + Optionally hides the save bar, can then be made visible with JavaScript. + Defaults to false. + + + + + + + +