Skip to content

Commit 98e62d0

Browse files
pdo-axelorpbe-axelor
authored andcommitted
Add backend dark logo/icon support
logo/icon removed from ws/public/app/info
1 parent c54a6b6 commit 98e62d0

File tree

7 files changed

+132
-62
lines changed

7 files changed

+132
-62
lines changed

axelor-core/src/main/java/com/axelor/app/AvailableAppSettings.java

+2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ public interface AvailableAppSettings {
3333
String APPLICATION_HELP = "application.help";
3434
String APPLICATION_COPYRIGHT = "application.copyright";
3535
String APPLICATION_LOGO = "application.logo";
36+
String APPLICATION_LOGO_DARK = "application.logo-dark";
3637
String APPLICATION_ICON = "application.icon";
38+
String APPLICATION_ICON_DARK = "application.icon-dark";
3739
String APPLICATION_MODE = "application.mode";
3840
String APPLICATION_BASE_URL = "application.base-url";
3941
String APPLICATION_CONFIG_PROVIDER = "application.config-provider";

axelor-tools/src/main/java/com/axelor/tools/code/entity/model/Entity.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,9 @@ public void setSuperClass(String superClass) {
478478
}
479479

480480
public String computeSuperClassName() {
481-
if (isModelClass()) { return null; }
481+
if (isModelClass()) {
482+
return null;
483+
}
482484
if (isBlank(superClass)) {
483485
return notFalse(isAuditable) ? "com.axelor.auth.db.AuditableModel" : "com.axelor.db.Model";
484486
}

axelor-web/src/main/java/com/axelor/web/service/InfoResource.java

+24-13
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
*/
1919
package com.axelor.web.service;
2020

21+
import com.axelor.app.AppSettings;
2122
import com.axelor.auth.AuthUtils;
2223
import com.axelor.auth.db.User;
2324
import com.axelor.common.MimeTypesUtils;
2425
import com.axelor.common.ObjectUtils;
2526
import com.axelor.common.StringUtils;
27+
import com.axelor.common.UriBuilder;
2628
import com.axelor.inject.Beans;
2729
import com.axelor.meta.MetaFiles;
2830
import com.axelor.meta.db.MetaFile;
@@ -32,10 +34,9 @@
3234
import io.swagger.v3.oas.annotations.Hidden;
3335
import io.swagger.v3.oas.annotations.Operation;
3436
import io.swagger.v3.oas.annotations.tags.Tag;
35-
import java.io.File;
36-
import java.io.FileInputStream;
3737
import java.io.InputStream;
3838
import java.net.URI;
39+
import java.nio.file.Files;
3940
import java.util.Map;
4041
import java.util.Optional;
4142
import javax.inject.Inject;
@@ -106,36 +107,46 @@ public Response getTheme(@QueryParam("name") String name) {
106107
@GET
107108
@Path("logo")
108109
@Hidden
109-
public Response getLogoContent() {
110-
return getImageContent(infoService.getLogo());
110+
public Response getLogoContent(@QueryParam("mode") String mode) {
111+
return getImageContent(infoService.getLogo(mode));
111112
}
112113

113114
@GET
114115
@Path("icon")
115116
@Hidden
116-
public Response getIconContent() {
117-
return getImageContent(infoService.getIcon());
117+
public Response getIconContent(@QueryParam("mode") String mode) {
118+
return getImageContent(infoService.getIcon(mode));
118119
}
119120

120121
private Response getImageContent(Object image) {
121122
try {
122123
if (image instanceof MetaFile) {
123124
final MetaFile metaFile = (MetaFile) image;
124125
final String filePath = metaFile.getFilePath();
125-
final File inputFile = MetaFiles.getPath(filePath).toFile();
126+
final java.nio.file.Path inputPath = MetaFiles.getPath(filePath);
126127

127-
return Response.ok(new FileInputStream(inputFile))
128-
.type(MimeTypesUtils.getContentType(inputFile))
129-
.build();
128+
if (Files.exists(inputPath)) {
129+
return Response.ok(Files.newInputStream(inputPath))
130+
.type(MimeTypesUtils.getContentType(inputPath))
131+
.build();
132+
}
130133
} else if (ObjectUtils.notEmpty(image)) {
131134
final String path = image.toString();
132135
final InputStream inputStream = request.getServletContext().getResourceAsStream(path);
133136

134-
if (inputStream == null) {
135-
return Response.seeOther(new URI(path)).build();
137+
if (inputStream != null) {
138+
return Response.ok(inputStream).type(MimeTypesUtils.getContentType(path)).build();
139+
}
140+
141+
URI uri = new URI(path);
142+
if (!uri.isAbsolute()) {
143+
uri =
144+
UriBuilder.from(AppSettings.get().getBaseURL())
145+
.addPath(path.startsWith("/") ? path : "/" + path)
146+
.toUri();
136147
}
137148

138-
return Response.ok(inputStream).type(MimeTypesUtils.getContentType(path)).build();
149+
return Response.seeOther(uri).build();
139150
}
140151
} catch (Exception e) {
141152
log.error("Unable to get image content for {}: {}", image, e.getMessage());

axelor-web/src/main/java/com/axelor/web/service/InfoService.java

+77-40
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@
4242
import com.axelor.script.CompositeScriptHelper;
4343
import com.axelor.script.ScriptBindings;
4444
import com.axelor.script.ScriptHelper;
45+
import java.lang.reflect.Method;
4546
import java.util.Collection;
4647
import java.util.HashMap;
4748
import java.util.List;
4849
import java.util.Map;
4950
import java.util.Optional;
5051
import java.util.Set;
52+
import java.util.function.Supplier;
5153
import java.util.stream.Collectors;
5254
import javax.inject.Inject;
5355
import javax.servlet.http.HttpServletRequest;
@@ -95,8 +97,6 @@ private Map<String, Object> appInfo() {
9597
SETTINGS.format(
9698
I18n.get(SETTINGS.getProperties().get(AvailableAppSettings.APPLICATION_COPYRIGHT))));
9799
map.put("theme", SETTINGS.get(AvailableAppSettings.APPLICATION_THEME, null));
98-
map.put("logo", getLogoLink());
99-
map.put("icon", getIconLink());
100100
map.put("lang", AppFilter.getLocale().toLanguageTag());
101101

102102
final Map<String, Object> signIn = signInInfo();
@@ -318,59 +318,96 @@ private Object featuresInfo() {
318318
*
319319
* <p>The returned image can be a resource path string, a URL string or a MetaFile
320320
*
321+
* @param mode light or dark
321322
* @return user specific logo
322323
*/
323-
public Object getLogo() {
324-
final String logo = SETTINGS.get(AvailableAppSettings.APPLICATION_LOGO, "img/axelor.png");
325-
if (SETTINGS.get(AvailableAppSettings.CONTEXT_APP_LOGO) != null) {
326-
final ScriptBindings bindings = new ScriptBindings(new HashMap<>());
327-
final ScriptHelper helper = new CompositeScriptHelper(bindings);
328-
try {
329-
return Optional.ofNullable(helper.eval("__config__.appLogo")).orElse(logo);
330-
} catch (Exception e) {
331-
// Ignore
332-
}
333-
}
334-
return logo;
335-
}
336-
337-
/**
338-
* Gets user specific application logo link, or falls back to default application logo.
339-
*
340-
* @return user specific logo link
341-
*/
342-
public String getLogoLink() {
343-
return getLink(getLogo());
324+
public Object getLogo(String mode) {
325+
return getImage(
326+
SETTINGS.get(AvailableAppSettings.CONTEXT_APP_LOGO),
327+
mode,
328+
"appLogo",
329+
() ->
330+
isDark(mode)
331+
? SETTINGS.get(
332+
AvailableAppSettings.APPLICATION_LOGO_DARK,
333+
SETTINGS.get(AvailableAppSettings.APPLICATION_LOGO, "img/axelor-dark.png"))
334+
: SETTINGS.get(AvailableAppSettings.APPLICATION_LOGO, "img/axelor.png"));
344335
}
345336

346337
/**
347338
* Gets user specific application icon, or falls back to default application icon.
348339
*
349340
* <p>The returned image can be a resource path string, a URL string or a MetaFile
350341
*
342+
* @param mode light or dark
351343
* @return user specific application icon
352344
*/
353-
public Object getIcon() {
354-
final String icon = SETTINGS.get(AvailableAppSettings.APPLICATION_ICON, "ico/favicon.ico");
355-
if (SETTINGS.get(AvailableAppSettings.CONTEXT_APP_ICON) != null) {
356-
final ScriptBindings bindings = new ScriptBindings(new HashMap<>());
357-
final ScriptHelper helper = new CompositeScriptHelper(bindings);
358-
try {
359-
return Optional.ofNullable(helper.eval("__config__.appIcon")).orElse(icon);
360-
} catch (Exception e) {
361-
// Ignore
345+
public Object getIcon(String mode) {
346+
return getImage(
347+
SETTINGS.get(AvailableAppSettings.CONTEXT_APP_ICON),
348+
mode,
349+
"appIcon",
350+
() ->
351+
isDark(mode)
352+
? SETTINGS.get(
353+
AvailableAppSettings.APPLICATION_ICON_DARK,
354+
SETTINGS.get(AvailableAppSettings.APPLICATION_ICON, "ico/favicon.ico"))
355+
: SETTINGS.get(AvailableAppSettings.APPLICATION_ICON, "ico/favicon.ico"));
356+
}
357+
358+
private Object getImage(
359+
String contextImage, String mode, String config, Supplier<String> defaultValue) {
360+
Object result;
361+
362+
try {
363+
result = getImage(contextImage, mode);
364+
if (ObjectUtils.notEmpty(result)) {
365+
return result;
362366
}
367+
return defaultValue.get();
368+
} catch (Exception e) {
369+
// Ignore
363370
}
364-
return icon;
371+
372+
try {
373+
result = getConfigImage(config);
374+
if (ObjectUtils.notEmpty(result)) {
375+
return result;
376+
}
377+
} catch (Exception e) {
378+
// Ignore
379+
}
380+
381+
return defaultValue.get();
365382
}
366383

367-
/**
368-
* Gets user specific application icon link, or falls back to default application icon.
369-
*
370-
* @return user specific application icon link
371-
*/
372-
public String getIconLink() {
373-
return getLink(getIcon());
384+
private Object getImage(String imageCall, String mode) throws Exception {
385+
final String[] parts = imageCall.split("\\:", 2);
386+
387+
if (parts.length != 2) {
388+
return null;
389+
}
390+
391+
final String className = parts[0];
392+
final String methodName = parts[1];
393+
394+
final Class<?> klass = Class.forName(className);
395+
final Method method = klass.getMethod(methodName, String.class);
396+
final Object bean = Beans.get(klass);
397+
398+
return method.invoke(bean, mode);
399+
}
400+
401+
// Legacy way to retrieve context image without passing mode
402+
@Deprecated(forRemoval = true)
403+
private Object getConfigImage(String name) {
404+
final ScriptBindings bindings = new ScriptBindings(new HashMap<>());
405+
final ScriptHelper scriptHelper = new CompositeScriptHelper(bindings);
406+
return scriptHelper.eval("__config__." + name);
407+
}
408+
409+
private boolean isDark(String mode) {
410+
return "dark".equalsIgnoreCase(mode);
374411
}
375412

376413
public String getLink(Object value) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: Support application settings application.logo-dark and application.icon-dark
3+
type: feature
4+
description: |
5+
`ws/public/app/logo` and `ws/public/app/logo` accept `mode` query parameter (`light` or `dark`).
6+
Application configuration `context.appLogo` and `context.appIcon` accept `String mode` parameter.
7+
Removed logo and icon from `ws/public/app/info`.

documentation/modules/dev-guide/examples/axelor-config.properties

+9
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ application.copyright = Copyright (c) {year} Axelor. All Rights Reserved.
1010
# Header logo. Should be 40px in height with transparent background.
1111
application.logo = img/axelor-logo.png
1212

13+
# Header logo for dark mode theme. Should be 40px in height with transparent background.
14+
#application.logo-dark =
15+
1316
# Website icon. Must be a multiple of 48px square for favicon compatibility.
1417
application.icon = "ico/favicon.ico"
1518

19+
# Website icon for dark mode theme. Must be a multiple of 48px square for favicon compatibility.
20+
#application.icon-dark =
21+
1622
# Home website. Home page link in user dropdown menu
1723
application.home = https://www.axelor.com
1824

@@ -91,6 +97,9 @@ application.mode = dev
9197
# Sign-in logo, default to `application.logo`
9298
#application.sign-in.logo =
9399

100+
# Sign-in logo for dark theme mode, default to `application.logo-dark`
101+
#application.sign-in.logo-dark =
102+
94103
# Sign-in title
95104
#application.sign-in.title =
96105

documentation/modules/dev-guide/pages/application/config.adoc

+10-8
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ db.default.password = secret
203203
| `application.author` | application author |
204204
| `application.copyright` | application copyright |
205205
| `application.logo` | header logo. Should be 40px in height with transparent background | img/axelor.png
206+
| `application.logo-dark` | (optional) dark header logo. Should be 40px in height with transparent background | `application.logo` or img/axelor-dark.png
206207
| `application.icon` | website icon. Must be a multiple of 48px square for favicon compatibility | ico/favicon.ico
208+
| `application.icon-dark` | (optional) dark website icon. Must be a multiple of 48px square for favicon compatibility | `application.icon` or ico/favicon.ico
207209
| `application.home` | home website. Link to be used as home page link in user dropdown menu |
208210
| `application.help` | online help. Link to be used in About page |
209211
| `application.mode` | application deployment mode. Can be `prod` or `dev` | dev
@@ -471,7 +473,8 @@ context.appLogo = com.axelor.custom.AppService:getLogo
471473
context.appIcon = com.axelor.custom.AppService:getIcon
472474
----
473475

474-
The `appLogo`/`appIcon` methods should return either:
476+
The `appLogo`/`appIcon` methods can optionally accept a `String mode` parameter : either `light` or `dark` or if null,
477+
assuming default `light` is used. Methods should return either:
475478

476479
* a string, link to logo/icon file (eg. `img/my-logo.png`, `ico/my-icon.ico`)
477480
* an instance of `MetaFile` pointing to the logo/icon file
@@ -482,18 +485,17 @@ Here is an example in case it returns a `MetaFile`:
482485
----
483486
public class AppService {
484487
485-
public MetaFile getLogo() {
486-
final User user = AuthUtils.getUser();
487-
if(user == null || user.getActiveCompany() == null) {
488-
return null;
489-
}
490-
return user.getActiveCompany().getLogo();
488+
public MetaFile getLogo(String mode) {
489+
return Optional.ofNullable(AuthUtils.getUser())
490+
.map(User::getActiveCompany)
491+
.map(company -> "dark".equals(mode) ? company.getDarkLogo() : company.getLogo())
492+
.orElse(null); // Returning null will show default application logo.
491493
}
492494
}
493495
----
494496

495497
If return value is null, it will show default logo/icon specified
496-
with `application.logo` and `application.icon` in configuration file.
498+
with `application.logo`/`application.logo-dark` and `application.icon`/`application.icon-dark` in configuration file.
497499

498500
[#custom-login-page]
499501
== Custom Login Page

0 commit comments

Comments
 (0)