feat: App navigation - Logo upload (#22297)

## Description

Allowing users to upload a logo to show in the navigation along with
toggles to hide logo or application title.

Fixes #20134
Fixes #21946
Fixes #22260

## Media
<video
src="https://user-images.githubusercontent.com/22471214/235613131-129ac2ed-b994-4eab-8eba-7db297c2f7fd.mp4"><video>

## Type of change
- New feature (non-breaking change which adds functionality)

## How Has This Been Tested?
- Manual

### Test Plan
> https://github.com/appsmithorg/TestSmith/issues/2376

### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)


## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test
This commit is contained in:
Dhruvik Neharia 2023-05-05 12:19:20 +05:30 committed by GitHub
parent 94e4b8515e
commit 9d5e2e0246
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1150 additions and 312 deletions

View File

@ -48,7 +48,9 @@ describe("Embed settings options", function () {
.invoke("readText")
.as("embeddedAppUrl");
cy.enablePublicAccess();
cy.get(".t--back-to-home").click();
cy.get(
`${appNavigationLocators.header} ${appNavigationLocators.backToAppsButton}`,
).click();
homePage.CreateNewApplication();
ee.DragDropWidgetNVerify("iframewidget", 100, 100);
cy.get("@embeddedAppUrl").then((url) => {

View File

@ -1,4 +1,5 @@
const dsl = require("../../../../fixtures/previewMode.json");
const appNavigationLocators = require("../../../../locators/AppNavigation.json");
const BASE_URL = Cypress.config().baseUrl;
@ -9,7 +10,9 @@ describe("Preview mode functionality", function () {
it("1. on click of apps on header, it should take to application home page", function () {
cy.PublishtheApp();
cy.get(".t--back-to-home").click();
cy.get(
`${appNavigationLocators.header} ${appNavigationLocators.backToAppsButton}`,
).click();
cy.url().should("eq", BASE_URL + "applications");
});
});

View File

@ -8,6 +8,7 @@
"editButton": ".t--back-to-editor",
"forkButton": ".t--fork-app",
"signButton": ".t--sign-in",
"backToAppsButton": ".t--app-viewer-back-to-apps-button",
"userProfileDropdownButton": ".t--profile-menu-icon",
"userProfileDropdownMenu": ".t--profile-menu",
"modal": ".bp3-dialog",

View File

@ -96,5 +96,5 @@ export default {
optionsIcon: ".t--options-icon",
reconnectDatasourceModal: ".reconnect-datasource-modal",
importAppProgressWrapper: ".t-import-app-progress-wrapper",
backtoHome: ".t--back-to-home",
backtoHome: ".t--app-viewer-back-to-apps-button",
};

View File

@ -20,7 +20,8 @@ export class DeployMode {
_clearDropdown = "button.select-button span.cancel-icon";
private _jsonFormMultiSelectOptions = (option: string) =>
`//div[@title='${option}']//input[@type='checkbox']/ancestor::div[@title='${option}']`;
private _backtoHome = ".t--back-to-home";
private _backtoHome =
".t--app-viewer-navigation-header .t--app-viewer-back-to-apps-button";
private _homeAppsmithImage = "a.t--appsmith-logo";
//refering PublishtheApp from command.js

View File

@ -101,6 +101,34 @@ export const updateApplicationNavigationSettingAction = (
};
};
export const updateApplicationNavigationLogoAction = (logo: string) => {
return {
type: ReduxActionTypes.UPLOAD_NAVIGATION_LOGO_INIT,
payload: logo,
};
};
export const updateApplicationNavigationLogoSuccessAction = (
logoAssetId: string,
) => {
return {
type: ReduxActionTypes.UPLOAD_NAVIGATION_LOGO_SUCCESS,
payload: logoAssetId,
};
};
export const deleteApplicationNavigationLogoAction = () => {
return {
type: ReduxActionTypes.DELETE_NAVIGATION_LOGO_INIT,
};
};
export const deleteApplicationNavigationLogoSuccessAction = () => {
return {
type: ReduxActionTypes.DELETE_NAVIGATION_LOGO_SUCCESS,
};
};
export const publishApplication = (applicationId: string) => {
return {
type: ReduxActionTypes.PUBLISH_APPLICATION_INIT,

View File

@ -221,6 +221,16 @@ export interface PageDefaultMeta {
default: boolean;
}
export interface UploadNavigationLogoRequest {
applicationId: string;
logo: File;
onSuccessCallback?: () => void;
}
export interface DeleteNavigationLogoRequest {
applicationId: string;
}
export interface snapShotApplicationRequest {
applicationId: string;
}
@ -356,6 +366,35 @@ export class ApplicationApi extends Api {
);
}
static uploadNavigationLogo(
request: UploadNavigationLogoRequest,
): AxiosPromise<ApiResponse> {
const formData = new FormData();
if (request.logo) {
formData.append("file", request.logo);
}
return Api.post(
ApplicationApi.baseURL + "/" + request.applicationId + "/logo",
formData,
null,
{
headers: {
"Content-Type": "multipart/form-data",
},
},
);
}
static deleteNavigationLogo(
request: DeleteNavigationLogoRequest,
): AxiosPromise<ApiResponse> {
return Api.delete(
ApplicationApi.baseURL + "/" + request.applicationId + "/logo",
);
}
static createApplicationSnapShot(request: snapShotApplicationRequest) {
return Api.post(getSnapShotAPIRoute(request.applicationId));
}

View File

@ -796,6 +796,10 @@ export const ReduxActionTypes = {
"PROCESS_AUTO_LAYOUT_DIMENSION_UPDATES",
SET_GSHEET_TOKEN: "SET_GSHEET_TOKEN",
FILE_PICKER_CALLBACK_ACTION: "FILE_PICKER_CALLBACK_ACTION",
UPLOAD_NAVIGATION_LOGO_INIT: "UPLOAD_NAVIGATION_LOGO_INIT",
UPLOAD_NAVIGATION_LOGO_SUCCESS: "UPLOAD_NAVIGATION_LOGO_SUCCESS",
DELETE_NAVIGATION_LOGO_INIT: "DELETE_NAVIGATION_LOGO_INIT",
DELETE_NAVIGATION_LOGO_SUCCESS: "DELETE_NAVIGATION_LOGO_SUCCESS",
FETCH_GSHEET_SPREADSHEETS: "FETCH_GSHEET_SPREADSHEETS",
FETCH_GSHEET_SPREADSHEETS_SUCCESS: "FETCH_GSHEET_SPREADSHEETS_SUCCESS",
FETCH_GSHEET_SPREADSHEETS_FAILURE: "FETCH_GSHEET_SPREADSHEETS_FAILURE",
@ -984,6 +988,8 @@ export const ReduxActionErrorTypes = {
INSTALL_LIBRARY_FAILED: "INSTALL_LIBRARY_FAILED",
UNINSTALL_LIBRARY_FAILED: "UNINSTALL_LIBRARY_FAILED",
FETCH_JS_LIBRARIES_FAILED: "FETCH_JS_LIBRARIES_FAILED",
UPLOAD_NAVIGATION_LOGO_ERROR: "UPLOAD_NAVIGATION_LOGO_ERROR",
DELETE_NAVIGATION_LOGO_ERROR: "DELETE_NAVIGATION_LOGO_ERROR",
USER_PROFILE_PICTURE_UPLOAD_FAILED: "USER_PROFILE_PICTURE_UPLOAD_FAILED",
USER_IMAGE_INVALID_FILE_CONTENT: "USER_IMAGE_INVALID_FILE_CONTENT",
};

View File

@ -208,6 +208,7 @@ export const EDIT_APP = () => `Edit App`;
export const FORK_APP = () => `Fork App`;
export const SIGN_IN = () => `Sign in`;
export const SHARE_APP = () => `Share app`;
export const ALL_APPS = () => `All apps`;
export const EDITOR_HEADER = {
saving: () => "Saving",
@ -1574,17 +1575,21 @@ export const IN_APP_EMBED_SETTING = {
export const APP_NAVIGATION_SETTING = {
sectionHeader: () => "Navigation",
sectionHeaderDesc: () => "Customize the navigation bar",
showNavbarLabel: () => "Show Navbar",
showNavbarLabel: () => "Show navbar",
orientationLabel: () => "Orientation",
navStyleLabel: () => "Variant",
positionLabel: () => "Position",
itemStyleLabel: () => "Item Style",
colorStyleLabel: () => "Background color",
logoLabel: () => "Logo",
logoConfigurationLabel: () => "Logo Configuration",
showSignInLabel: () => "Show Sign In",
logoConfigurationLabel: () => "Logo configuration",
showSignInLabel: () => "Show sign in",
showSignInTooltip: () =>
"Toggle to show the sign-in button for users who are not logged in.",
logoUploadFormatError: () => `Uploaded file must be in .PNG or .JPG formats.`,
logoUploadSizeError: () => `Uploaded file must be less than 1MB.`,
showLogoLabel: () => "Show logo",
showApplicationTitleLabel: () => "Show application title",
};
export const LOCK_SIDEBAR_MESSAGE = () => `Lock sidebar open`;

View File

@ -1,101 +0,0 @@
import React, { useEffect } from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import AppsIcon from "remixicon-react/AppsLineIcon";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import type { NavigationSetting } from "constants/AppConstants";
import { NAVIGATION_SETTINGS } from "constants/AppConstants";
import {
getMenuItemBackgroundColorOnHover,
getMenuItemTextColor,
} from "pages/AppViewer/utils";
import styled from "styled-components";
import { TooltipComponent } from "design-system-old";
import classNames from "classnames";
type BackToHomeButtonProps = {
primaryColor: string;
navColorStyle: NavigationSetting["colorStyle"];
forSidebar?: boolean;
isLogoVisible?: boolean;
setIsLogoVisible?: (isLogoVisible: boolean) => void;
};
const StyledAppIcon = styled(AppsIcon)<
BackToHomeButtonProps & {
borderRadius: string;
}
>`
color: ${({ navColorStyle, primaryColor }) =>
getMenuItemTextColor(primaryColor, navColorStyle, true)};
border-radius: ${({ borderRadius }) => borderRadius};
transition: all 0.3s ease-in-out;
margin-top: ${({ forSidebar }) => (forSidebar ? " -3px" : "-2px")};
width: 100%;
`;
export const StyledLink = styled(Link)<BackToHomeButtonProps>`
min-width: max-content;
img {
width: 100%;
max-width: 4rem;
max-height: 1.5rem;
}
&:hover {
svg {
background-color: ${({ navColorStyle, primaryColor }) =>
getMenuItemBackgroundColorOnHover(primaryColor, navColorStyle)};
${({ navColorStyle, primaryColor }) => {
if (navColorStyle !== NAVIGATION_SETTINGS.COLOR_STYLE.LIGHT) {
return `color: ${getMenuItemTextColor(primaryColor, navColorStyle)};`;
}
}};
}
}
`;
function BackToHomeButton(props: BackToHomeButtonProps) {
const {
forSidebar,
isLogoVisible,
navColorStyle,
primaryColor,
setIsLogoVisible,
} = props;
const selectedTheme = useSelector(getSelectedAppTheme);
useEffect(() => {
if (setIsLogoVisible) {
setIsLogoVisible(false);
}
}, []);
return (
<TooltipComponent content="Back to apps" position="bottom-left">
<StyledLink
className={classNames({
"flex items-center gap-2 group t--back-to-home hover:no-underline":
true,
"mr-2": !isLogoVisible,
"mb-2 mr-3": isLogoVisible,
})}
navColorStyle={navColorStyle}
primaryColor={primaryColor}
to="/applications"
>
<StyledAppIcon
borderRadius={selectedTheme.properties.borderRadius.appBorderRadius}
className="p-1 w-7 h-7"
forSidebar={forSidebar}
navColorStyle={navColorStyle}
primaryColor={primaryColor}
/>
</StyledLink>
</TooltipComponent>
);
}
export default BackToHomeButton;

View File

@ -0,0 +1,75 @@
import React from "react";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";
import type { NavigationSetting } from "constants/AppConstants";
import { NAVIGATION_SETTINGS } from "constants/AppConstants";
import styled from "styled-components";
import classNames from "classnames";
import {
getAppMode,
getCurrentApplication,
} from "@appsmith/selectors/applicationSelectors";
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
import { getViewModePageList } from "selectors/editorSelectors";
import { useHref } from "pages/Editor/utils";
import { APP_MODE } from "entities/App";
import { builderURL, viewerURL } from "RouteBuilder";
import { get } from "lodash";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
type NavigationLogoProps = {
logoConfiguration: NavigationSetting["logoConfiguration"];
};
const StyledImage = styled.img`
max-width: 10rem;
max-height: 1.5rem;
`;
function NavigationLogo(props: NavigationLogoProps) {
const { logoConfiguration } = props;
const currentApplicationDetails: ApplicationPayload | undefined = useSelector(
getCurrentApplication,
);
const pages = useSelector(getViewModePageList);
const appMode = useSelector(getAppMode);
const defaultPage = pages.find((page) => page.isDefault) || pages[0];
const pageUrl = useHref(
appMode === APP_MODE.PUBLISHED ? viewerURL : builderURL,
{
pageId: defaultPage?.pageId,
},
);
const logoAssetId = get(
currentApplicationDetails,
"applicationDetail.navigationSetting.logoAssetId",
"",
);
if (
!logoAssetId?.length ||
logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.APPLICATION_TITLE_ONLY ||
logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.NO_LOGO_OR_APPLICATION_TITLE
) {
return null;
}
return (
<Link
className={classNames({
"mr-4": true,
"pointer-events-none select-none": pages.length <= 1,
})}
to={pageUrl}
>
<StyledImage
alt="Application's logo"
src={getAssetUrl(`/api/v1/assets/${logoAssetId}`)}
/>
</Link>
);
}
export default NavigationLogo;

View File

@ -0,0 +1,86 @@
import React from "react";
import { ImageInput } from "../../../../pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/ImageInput";
import { TextType, Text, Icon } from "design-system-old";
import {
createMessage,
APP_NAVIGATION_SETTING,
} from "@appsmith/constants/messages";
import type { UpdateSetting } from "../../../../pages/Editor/AppSettingsPane/AppSettings/NavigationSettings";
import { useDispatch, useSelector } from "react-redux";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { NavigationSetting } from "constants/AppConstants";
import { logoImageValidation } from "../../../../pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/utils";
import {
getIsDeletingNavigationLogo,
getIsUploadingNavigationLogo,
} from "@appsmith/selectors/applicationSelectors";
export type ButtonGroupSettingProps = {
updateSetting: UpdateSetting;
navigationSetting: NavigationSetting;
};
const LogoInput = ({ navigationSetting }: ButtonGroupSettingProps) => {
const dispatch = useDispatch();
const applicationId = useSelector(getCurrentApplicationId);
const isUploadingNavigationLogo = useSelector(getIsUploadingNavigationLogo);
const isDeletingNavigationLogo = useSelector(getIsDeletingNavigationLogo);
const handleChange = (file: File) => {
dispatch({
type: ReduxActionTypes.UPLOAD_NAVIGATION_LOGO_INIT,
payload: {
applicationId: applicationId,
logo: file,
},
});
};
const handleDelete = () => {
dispatch({
type: ReduxActionTypes.DELETE_NAVIGATION_LOGO_INIT,
payload: {
applicationId: applicationId,
},
});
};
return (
<div className={`pt-4 t--navigation-settings-logo`}>
<div className="flex items-center">
<Text type={TextType.P1}>
{createMessage(APP_NAVIGATION_SETTING.logoLabel)}
</Text>
{navigationSetting?.logoAssetId?.length ? (
<button
className="flex items-center justify-center text-center h-7 w-7 ml-auto"
disabled={isUploadingNavigationLogo || isDeletingNavigationLogo}
onClick={() => handleDelete()}
>
<Icon fillColor="#575757" name="trash" size="extraLarge" />
</button>
) : (
""
)}
</div>
<div className="pt-1">
<ImageInput
className="t--settings-brand-logo-input"
onChange={(file) => {
handleChange && handleChange(file);
}}
validate={logoImageValidation}
value={
navigationSetting?.logoAssetId?.length
? `/api/v1/assets/${navigationSetting?.logoAssetId}`
: ""
}
/>
</div>
</div>
);
};
export default LogoInput;

View File

@ -26,6 +26,7 @@ import type { ConnectToGitResponse } from "actions/gitSyncActions";
import type { AppIconName } from "design-system-old";
import type { NavigationSetting } from "constants/AppConstants";
import { defaultNavigationSetting } from "constants/AppConstants";
import produce from "immer";
export const initialState: ApplicationsReduxState = {
isFetchingApplications: false,
@ -49,6 +50,8 @@ export const initialState: ApplicationsReduxState = {
isAppSidebarPinned: true,
isSavingNavigationSetting: false,
isErrorSavingNavigationSetting: false,
isUploadingNavigationLogo: false,
isDeletingNavigationLogo: false,
};
export const handlers = {
@ -402,12 +405,6 @@ export const handlers = {
if (action.payload.name) {
isSavingAppName = true;
}
if (state.currentApplication && action.payload.applicationDetail) {
state.currentApplication.applicationDetail = {
...state.currentApplication.applicationDetail,
...action.payload.applicationDetail,
};
}
if (action.payload.applicationDetail?.navigationSetting) {
isSavingNavigationSetting = true;
@ -417,11 +414,16 @@ export const handlers = {
...state,
isSavingAppName,
isErrorSavingAppName: false,
...(action.payload.applicationDetail
? { applicationDetail: action.payload.applicationDetail }
: {}),
isSavingNavigationSetting,
isErrorSavingNavigationSetting: false,
...(action.payload.applicationDetail
? {
applicationDetail: {
...state.currentApplication?.applicationDetail,
...action.payload.applicationDetail,
},
}
: {}),
};
},
[ReduxActionTypes.UPDATE_APPLICATION_SUCCESS]: (
@ -459,6 +461,7 @@ export const handlers = {
...state,
isSavingAppName: false,
isErrorSavingAppName: true,
isSavingNavigationSetting: false,
isErrorSavingNavigationSetting: true,
};
},
@ -592,6 +595,81 @@ export const handlers = {
...state,
isAppSidebarPinned: action.payload,
}),
[ReduxActionTypes.UPLOAD_NAVIGATION_LOGO_INIT]: (
state: ApplicationsReduxState,
) => ({
...state,
isUploadingNavigationLogo: true,
}),
[ReduxActionTypes.UPLOAD_NAVIGATION_LOGO_SUCCESS]: (
state: ApplicationsReduxState,
action: ReduxAction<NavigationSetting["logoAssetId"]>,
) => {
return produce(state, (draftState: ApplicationsReduxState) => {
draftState.isUploadingNavigationLogo = false;
if (
draftState?.currentApplication?.applicationDetail?.navigationSetting
) {
draftState.currentApplication.applicationDetail.navigationSetting.logoAssetId =
action.payload;
}
});
},
[ReduxActionErrorTypes.UPLOAD_NAVIGATION_LOGO_ERROR]: (
state: ApplicationsReduxState,
) => {
return {
...state,
isUploadingNavigationLogo: false,
};
},
[ReduxActionTypes.DELETE_NAVIGATION_LOGO_INIT]: (
state: ApplicationsReduxState,
) => ({
...state,
isDeletingNavigationLogo: true,
}),
[ReduxActionTypes.DELETE_NAVIGATION_LOGO_SUCCESS]: (
state: ApplicationsReduxState,
) => {
const updatedNavigationSetting = Object.assign(
{},
state.currentApplication?.applicationDetail?.navigationSetting,
{
logoAssetId: "",
},
);
const updatedApplicationDetail = Object.assign(
{},
state.currentApplication?.applicationDetail,
{
navigationSetting: updatedNavigationSetting,
},
);
const updatedCurrentApplication = Object.assign(
{},
state.currentApplication,
{
applicationDetail: updatedApplicationDetail,
},
);
return Object.assign({}, state, {
isDeletingNavigationLogo: false,
currentApplication: updatedCurrentApplication,
});
},
[ReduxActionErrorTypes.DELETE_NAVIGATION_LOGO_ERROR]: (
state: ApplicationsReduxState,
) => {
return {
...state,
isDeletingNavigationLogo: false,
};
},
};
const applicationsReducer = createReducer(initialState, handlers);
@ -624,6 +702,8 @@ export interface ApplicationsReduxState {
isAppSidebarPinned: boolean;
isSavingNavigationSetting: boolean;
isErrorSavingNavigationSetting: boolean;
isUploadingNavigationLogo: boolean;
isDeletingNavigationLogo: boolean;
}
export interface Application {

View File

@ -15,6 +15,7 @@ import type {
CreateApplicationRequest,
CreateApplicationResponse,
DeleteApplicationRequest,
DeleteNavigationLogoRequest,
DuplicateApplicationRequest,
FetchApplicationPayload,
FetchApplicationResponse,
@ -27,6 +28,7 @@ import type {
SetDefaultPageRequest,
UpdateApplicationRequest,
UpdateApplicationResponse,
UploadNavigationLogoRequest,
WorkspaceApplicationObject,
} from "@appsmith/api/ApplicationApi";
import ApplicationApi from "@appsmith/api/ApplicationApi";
@ -39,6 +41,7 @@ import history from "utils/history";
import type { AppState } from "@appsmith/reducers";
import {
ApplicationVersion,
deleteApplicationNavigationLogoSuccessAction,
fetchApplication,
getAllApplications,
importApplicationSuccess,
@ -49,6 +52,7 @@ import {
setPageIdForImport,
setWorkspaceIdForImport,
showReconnectDatasourceModal,
updateApplicationNavigationLogoSuccessAction,
updateApplicationNavigationSettingAction,
updateCurrentApplicationEmbedSetting,
updateCurrentApplicationIcon,
@ -113,6 +117,10 @@ import { getCurrentUser } from "selectors/usersSelectors";
import { ERROR_CODES } from "@appsmith/constants/ApiConstants";
import { safeCrashAppRequest } from "actions/errorActions";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import {
defaultNavigationSetting,
keysOfNavigationSetting,
} from "constants/AppConstants";
import { setAllEntityCollapsibleStates } from "../../actions/editorContextActions";
export const getDefaultPageId = (
@ -913,3 +921,111 @@ export function* initDatasourceConnectionDuringImport(
yield put(initDatasourceConnectionDuringImportSuccess());
}
export function* uploadNavigationLogoSaga(
action: ReduxAction<UploadNavigationLogoRequest>,
) {
try {
const request: UploadNavigationLogoRequest = action.payload;
const response: ApiResponse<UpdateApplicationResponse> = yield call(
ApplicationApi.uploadNavigationLogo,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
if (request.logo) {
if (
request.logo &&
response.data.applicationDetail?.navigationSetting?.logoAssetId
) {
yield put(
updateApplicationNavigationLogoSuccessAction(
response.data.applicationDetail.navigationSetting.logoAssetId,
),
);
/**
* When the user creates a new application and they upload logo without
* interacting with any other navigation settings first, we get only
* navigationSetting = { logoAssetId: <id_string_here> } in the API response.
*
* Therefore, we need to handle this case by hitting the update application
* API and store the default navigation settings as well alongside
* the logoAssetId.
*/
const navigationSettingKeys = Object.keys(
response.data.applicationDetail?.navigationSetting,
);
if (
navigationSettingKeys?.length === 1 &&
navigationSettingKeys?.[0] === keysOfNavigationSetting.logoAssetId
) {
const newUpdateApplicationRequestWithDefaultNavigationSettings = {
...response.data,
applicationDetail: {
...response.data.applicationDetail,
navigationSetting: {
...defaultNavigationSetting,
...response.data.applicationDetail.navigationSetting,
},
},
};
const updateApplicationResponse: ApiResponse<UpdateApplicationResponse> =
yield call(
ApplicationApi.updateApplication,
newUpdateApplicationRequestWithDefaultNavigationSettings,
);
if (updateApplicationResponse?.data) {
yield put({
type: ReduxActionTypes.UPDATE_APPLICATION_SUCCESS,
payload: updateApplicationResponse.data,
});
if (
newUpdateApplicationRequestWithDefaultNavigationSettings
.applicationDetail?.navigationSetting &&
updateApplicationResponse.data.applicationDetail
?.navigationSetting
) {
yield put(
updateApplicationNavigationSettingAction(
updateApplicationResponse.data.applicationDetail
.navigationSetting,
),
);
}
}
}
}
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPLOAD_NAVIGATION_LOGO_ERROR,
payload: {
error,
},
});
}
}
export function* deleteNavigationLogoSaga(
action: ReduxAction<DeleteNavigationLogoRequest>,
) {
try {
const request: DeleteNavigationLogoRequest = action.payload;
yield call(ApplicationApi.deleteNavigationLogo, request);
yield put(deleteApplicationNavigationLogoSuccessAction());
} catch (error) {
yield put({
type: ReduxActionErrorTypes.DELETE_NAVIGATION_LOGO_ERROR,
payload: {
error,
},
});
}
}

View File

@ -216,6 +216,14 @@ export const getSidebarWidth = (state: AppState) => {
return 0;
};
export const getIsUploadingNavigationLogo = (state: AppState) => {
return state.ui.applications.isUploadingNavigationLogo;
};
export const getIsDeletingNavigationLogo = (state: AppState) => {
return state.ui.applications.isDeletingNavigationLogo;
};
const DEFAULT_EVALUATION_VERSION = 2;
export const selectEvaluationVersion = (state: AppState) =>

View File

@ -68,6 +68,7 @@ export const NAVIGATION_SETTINGS = {
LIGHT: "light",
THEME: "theme",
},
LOGO_ASSET_ID: "",
LOGO_CONFIGURATION: {
LOGO_AND_APPLICATION_TITLE: "logoAndApplicationTitle",
LOGO_ONLY: "logoOnly",
@ -84,6 +85,7 @@ export type NavigationSetting = {
position: (typeof NAVIGATION_SETTINGS.POSITION)[keyof typeof NAVIGATION_SETTINGS.POSITION];
itemStyle: (typeof NAVIGATION_SETTINGS.ITEM_STYLE)[keyof typeof NAVIGATION_SETTINGS.ITEM_STYLE];
colorStyle: (typeof NAVIGATION_SETTINGS.COLOR_STYLE)[keyof typeof NAVIGATION_SETTINGS.COLOR_STYLE];
logoAssetId: string;
logoConfiguration: (typeof NAVIGATION_SETTINGS.LOGO_CONFIGURATION)[keyof typeof NAVIGATION_SETTINGS.LOGO_CONFIGURATION];
};
@ -100,6 +102,7 @@ export const keysOfNavigationSetting = {
position: "position",
itemStyle: "itemStyle",
colorStyle: "colorStyle",
logoAssetId: "logoAssetId",
logoConfiguration: "logoConfiguration",
};
@ -111,6 +114,7 @@ export const defaultNavigationSetting = {
position: NAVIGATION_SETTINGS.POSITION.STATIC,
itemStyle: NAVIGATION_SETTINGS.ITEM_STYLE.TEXT,
colorStyle: NAVIGATION_SETTINGS.COLOR_STYLE.LIGHT,
logoAssetId: NAVIGATION_SETTINGS.LOGO_ASSET_ID,
logoConfiguration:
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE,
};
@ -120,7 +124,7 @@ export const SIDEBAR_WIDTH = {
MINIMAL: 66,
};
export const APPLICATION_TITLE_MAX_WIDTH = 224;
export const APPLICATION_TITLE_MAX_WIDTH = 192;
export const APPLICATION_TITLE_MAX_WIDTH_MOBILE = 150;
//all values are in milliseconds
export const REQUEST_IDLE_CALLBACK_TIMEOUT = {

View File

@ -1,3 +0,0 @@
export * from "ce/pages/AppViewer/BackToHomeButton";
import { default as CE_BackToHomeButton } from "ce/pages/AppViewer/BackToHomeButton";
export default CE_BackToHomeButton;

View File

@ -0,0 +1,3 @@
export * from "ce/pages/AppViewer/NavigationLogo";
import { default as CE_NavigationLogo } from "ce/pages/AppViewer/NavigationLogo";
export default CE_NavigationLogo;

View File

@ -0,0 +1,3 @@
export * from "ce/pages/Editor/NavigationSettings/LogoInput";
import { default as CE_LogoInput } from "ce/pages/Editor/NavigationSettings/LogoInput";
export default CE_LogoInput;

View File

@ -16,6 +16,8 @@ import {
initDatasourceConnectionDuringImport,
showReconnectDatasourcesModalSaga,
fetchUnconfiguredDatasourceList,
uploadNavigationLogoSaga,
deleteNavigationLogoSaga,
} from "ce/sagas/ApplicationSagas";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { all, takeLatest } from "redux-saga/effects";
@ -49,6 +51,14 @@ export default function* applicationSagas() {
duplicateApplicationSaga,
),
takeLatest(ReduxActionTypes.IMPORT_APPLICATION_INIT, importApplicationSaga),
takeLatest(
ReduxActionTypes.UPLOAD_NAVIGATION_LOGO_INIT,
uploadNavigationLogoSaga,
),
takeLatest(
ReduxActionTypes.DELETE_NAVIGATION_LOGO_INIT,
deleteNavigationLogoSaga,
),
takeLatest(ReduxActionTypes.FETCH_RELEASES, fetchReleases),
takeLatest(
ReduxActionTypes.INIT_DATASOURCE_CONNECTION_DURING_IMPORT_REQUEST,

View File

@ -97,6 +97,10 @@ const StyledButton = styled(Button)<{
color: ${styles.color} !important;
}
svg path {
fill: ${styles.color} !important;
}
&:hover,
&:active,
&:focus {
@ -105,6 +109,10 @@ const StyledButton = styled(Button)<{
span {
color: ${styles.color} !important;
}
svg path {
fill: ${styles.color} !important;
}
}
`;

View File

@ -43,12 +43,12 @@ export const StyledMenuContainer = styled.div<{
primaryColor: string;
navColorStyle: NavigationSetting["colorStyle"];
}>`
margin: 16px 0 0 0;
margin: 20px 0 0 0;
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
padding: 0 8px;
padding: 0 16px 12px;
flex-grow: 1;
padding-bottom: 12px;
@ -107,7 +107,7 @@ export const StyledCtaContainer = styled.div`
`;
export const StyledHeader = styled.div`
padding: 16px 8px 0px;
padding: 20px 20px 0px;
display: flex;
align-items: flex-start;
justify-content: space-between;

View File

@ -19,7 +19,6 @@ import {
previewModeSelector,
} from "selectors/editorSelectors";
import type { User } from "constants/userConstants";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
import SidebarProfileComponent from "./components/SidebarProfileComponent";
import CollapseButton from "./components/CollapseButton";
import classNames from "classnames";
@ -35,8 +34,9 @@ import {
} from "./Sidebar.styled";
import { getCurrentThemeDetails } from "selectors/themeSelectors";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
import BackToHomeButton from "@appsmith/pages/AppViewer/BackToHomeButton";
import NavigationLogo from "@appsmith/pages/AppViewer/NavigationLogo";
import MenuItemContainer from "./components/MenuItemContainer";
import BackToAppsButton from "./components/BackToAppsButton";
type SidebarProps = {
currentApplicationDetails?: ApplicationPayload;
@ -58,6 +58,10 @@ export function Sidebar(props: SidebarProps) {
const isMinimal =
currentApplicationDetails?.applicationDetail?.navigationSetting
?.navStyle === NAVIGATION_SETTINGS.NAV_STYLE.MINIMAL;
const logoConfiguration =
currentApplicationDetails?.applicationDetail?.navigationSetting
?.logoConfiguration ||
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE;
const primaryColor = get(
selectedTheme,
"properties.colors.primaryColor",
@ -82,7 +86,6 @@ export function Sidebar(props: SidebarProps) {
const isAppSettingsPaneWithNavigationTabOpen = useSelector(
getIsAppSettingsPaneWithNavigationTabOpen,
);
const [isLogoVisible, setIsLogoVisible] = useState(false);
useEffect(() => {
setQuery(window.location.search);
@ -127,7 +130,8 @@ export function Sidebar(props: SidebarProps) {
if (isPreviewMode) {
prefix += theme.smallHeaderHeight;
} else if (isAppSettingsPaneWithNavigationTabOpen) {
prefix += `${theme.smallHeaderHeight} - ${theme.bottomBarHeight}`;
// We deduct 64px as well since it is the margin coming from "m-8" class from tailwind
prefix += `${theme.smallHeaderHeight} - ${theme.bottomBarHeight} - 64px`;
} else {
prefix += "0px";
}
@ -148,31 +152,24 @@ export function Sidebar(props: SidebarProps) {
sidebarHeight={calculateSidebarHeight()}
>
<StyledHeader>
<div
className={classNames({
flex: true,
"flex-col": isLogoVisible,
})}
>
{currentUser?.username !== ANONYMOUS_USERNAME && (
<BackToHomeButton
forSidebar
isLogoVisible={isLogoVisible}
navColorStyle={navColorStyle}
primaryColor={primaryColor}
setIsLogoVisible={setIsLogoVisible}
/>
)}
<div className="flex flex-col gap-5">
<NavigationLogo logoConfiguration={logoConfiguration} />
{!isMinimal && (
<ApplicationName
appName={currentApplicationDetails?.name}
forSidebar
navColorStyle={navColorStyle}
navStyle={navStyle}
primaryColor={primaryColor}
/>
)}
{!isMinimal &&
(logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION
.LOGO_AND_APPLICATION_TITLE ||
logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION
.APPLICATION_TITLE_ONLY) && (
<ApplicationName
appName={currentApplicationDetails?.name}
forSidebar
navColorStyle={navColorStyle}
navStyle={navStyle}
primaryColor={primaryColor}
/>
)}
</div>
{!isMinimal && (
@ -231,6 +228,12 @@ export function Sidebar(props: SidebarProps) {
primaryColor={primaryColor}
url={editorURL}
/>
<BackToAppsButton
currentApplicationDetails={currentApplicationDetails}
insideSidebar
isMinimal={isMinimal}
/>
</StyledCtaContainer>
)}

View File

@ -12,14 +12,18 @@ export const StyledApplicationName = styled.div<{
primaryColor: string;
navColorStyle: NavigationSetting["colorStyle"];
navStyle: NavigationSetting["navStyle"];
forSidebar?: boolean;
isMobile: boolean;
forSidebar?: boolean;
fontWeight: "regular" | "bold";
}>`
color: ${({ navColorStyle, primaryColor }) =>
getApplicationNameTextColor(primaryColor, navColorStyle)};
font-size: ${THEMEING_TEXT_SIZES.base};
font-weight: ${({ fontWeight }) =>
fontWeight === "regular" ? "400" : "600"};
${({ forSidebar }) => (forSidebar ? "margin-left: 6px;" : "")};
${({ forSidebar, isMobile, navStyle }) => {
${({ isMobile, navStyle }) => {
if (isMobile) {
return `max-width: ${APPLICATION_TITLE_MAX_WIDTH_MOBILE}px;`;
} else if (
@ -27,8 +31,6 @@ export const StyledApplicationName = styled.div<{
!isMobile
) {
return `max-width: 500px;`;
} else if (forSidebar) {
return `max-width: ${APPLICATION_TITLE_MAX_WIDTH - 40}px;`;
} else {
return `max-width: ${APPLICATION_TITLE_MAX_WIDTH}px;`;
}

View File

@ -12,10 +12,18 @@ type ApplicationNameProps = {
navStyle: NavigationSetting["navStyle"];
primaryColor: string;
forSidebar?: boolean;
fontWeight?: "regular" | "bold";
};
const ApplicationName = (props: ApplicationNameProps) => {
const { appName, forSidebar, navColorStyle, navStyle, primaryColor } = props;
const {
appName,
fontWeight,
forSidebar,
navColorStyle,
navStyle,
primaryColor,
} = props;
const applicationNameRef = useRef<HTMLDivElement>(null);
const [ellipsisActive, setEllipsisActive] = useState(false);
const isMobile = useIsMobileDevice();
@ -42,6 +50,7 @@ const ApplicationName = (props: ApplicationNameProps) => {
>
<StyledApplicationName
className="overflow-hidden text-base overflow-ellipsis whitespace-nowrap t--app-viewer-application-name"
fontWeight={fontWeight || "bold"}
forSidebar={forSidebar}
isMobile={isMobile}
navColorStyle={navColorStyle}

View File

@ -0,0 +1,89 @@
import React from "react";
import Button from "../../AppViewerButton";
import { useSelector } from "react-redux";
import { ALL_APPS, createMessage } from "@appsmith/constants/messages";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { getMenuItemTextColor } from "pages/AppViewer/utils";
import type { NavigationSetting } from "constants/AppConstants";
import { NAVIGATION_SETTINGS } from "constants/AppConstants";
import { get } from "lodash";
import type { ApplicationPayload } from "@appsmith/constants/ReduxActionConstants";
import { useHistory } from "react-router";
import styled from "styled-components";
import AppsLineIcon from "remixicon-react/AppsLineIcon";
import { getCurrentUser } from "selectors/usersSelectors";
import type { User } from "constants/userConstants";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
import { TooltipComponent } from "design-system-old";
type BackToAppsButtonProps = {
currentApplicationDetails?: ApplicationPayload;
insideSidebar?: boolean;
isMinimal?: boolean;
};
const StyledAppIcon = styled(AppsLineIcon)<{
primaryColor: string;
navColorStyle: NavigationSetting["colorStyle"];
}>`
color: ${({ navColorStyle, primaryColor }) =>
getMenuItemTextColor(primaryColor, navColorStyle, true)};
width: 16px;
height: 16px;
`;
const BackToAppsButton = (props: BackToAppsButtonProps) => {
const { currentApplicationDetails, insideSidebar, isMinimal } = props;
const selectedTheme = useSelector(getSelectedAppTheme);
const navColorStyle =
currentApplicationDetails?.applicationDetail?.navigationSetting
?.colorStyle || NAVIGATION_SETTINGS.COLOR_STYLE.LIGHT;
const primaryColor = get(
selectedTheme,
"properties.colors.primaryColor",
"inherit",
);
const history = useHistory();
const currentUser: User | undefined = useSelector(getCurrentUser);
if (currentUser?.username === ANONYMOUS_USERNAME) {
return null;
}
return (
<TooltipComponent
boundary="viewport"
content={createMessage(ALL_APPS)}
disabled={insideSidebar}
hoverOpenDelay={500}
modifiers={{
preventOverflow: {
enabled: true,
boundariesElement: "viewport",
},
}}
position="bottom"
>
<Button
borderRadius={selectedTheme.properties.borderRadius.appBorderRadius}
className="h-8 t--app-viewer-back-to-apps-button"
icon={
<StyledAppIcon
navColorStyle={navColorStyle}
primaryColor={primaryColor}
/>
}
insideSidebar={insideSidebar}
isMinimal={isMinimal}
navColorStyle={navColorStyle}
onClick={() => {
history.push("/applications");
}}
primaryColor={primaryColor}
text={insideSidebar && !isMinimal && createMessage(ALL_APPS)}
/>
</TooltipComponent>
);
};
export default BackToAppsButton;

View File

@ -49,7 +49,7 @@ export const CollapseIconContainer = styled.div<{
${({ isOpen, isPinned }) => {
if (!isPinned && !isOpen) {
return `
transform: translateX(40px);
transform: translateX(54px);
background: ${Colors.WHITE};
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1), 0px 1px 2px rgba(0, 0, 0, 0.06);
transition: background 0.3s ease-in-out, transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);

View File

@ -1,7 +1,7 @@
import React from "react";
import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent";
import Button from "../../AppViewerButton";
import { Icon } from "design-system-old";
import { Icon, TooltipComponent } from "design-system-old";
import AppInviteUsersForm from "pages/workspace/AppInviteUsersForm";
import { useSelector } from "react-redux";
import { showAppInviteUsersDialogSelector } from "@appsmith/selectors/applicationSelectors";
@ -61,26 +61,40 @@ const ShareButton = (props: ShareButtonProps) => {
placeholder={createMessage(INVITE_USERS_PLACEHOLDER, cloudHosting)}
title={currentApplicationDetails?.name}
trigger={
<Button
borderRadius={selectedTheme.properties.borderRadius.appBorderRadius}
className="h-8 t--app-viewer-share-button"
data-cy="viewmode-share"
icon={
<Icon
fillColor={getApplicationNameTextColor(
primaryColor,
navColorStyle,
)}
name="share-line"
size="extraLarge"
/>
}
insideSidebar={insideSidebar}
isMinimal={isMinimal}
navColorStyle={navColorStyle}
primaryColor={primaryColor}
text={insideSidebar && !isMinimal && createMessage(SHARE_APP)}
/>
<TooltipComponent
boundary="viewport"
content={createMessage(SHARE_APP)}
disabled={insideSidebar}
hoverOpenDelay={500}
modifiers={{
preventOverflow: {
enabled: true,
boundariesElement: "viewport",
},
}}
position="bottom"
>
<Button
borderRadius={selectedTheme.properties.borderRadius.appBorderRadius}
className="h-8 t--app-viewer-share-button"
data-cy="viewmode-share"
icon={
<Icon
fillColor={getApplicationNameTextColor(
primaryColor,
navColorStyle,
)}
name="share-line"
size="extraLarge"
/>
}
insideSidebar={insideSidebar}
isMinimal={isMinimal}
navColorStyle={navColorStyle}
primaryColor={primaryColor}
text={insideSidebar && !isMinimal && createMessage(SHARE_APP)}
/>
</TooltipComponent>
}
workspaceId={currentWorkspaceId}
/>

View File

@ -25,7 +25,8 @@ import ProfileDropdown from "pages/common/ProfileDropdown";
import TopStacked from "../TopStacked";
import { HeaderRow, StyledNav } from "./TopHeader.styled";
import TopInline from "../TopInline";
import BackToHomeButton from "@appsmith/pages/AppViewer/BackToHomeButton";
import NavigationLogo from "@appsmith/pages/AppViewer/NavigationLogo";
import BackToAppsButton from "./BackToAppsButton";
type TopHeaderProps = {
currentApplicationDetails?: ApplicationPayload;
@ -52,6 +53,10 @@ const TopHeader = (props: TopHeaderProps) => {
const navStyle =
currentApplicationDetails?.applicationDetail?.navigationSetting?.navStyle ||
NAVIGATION_SETTINGS.NAV_STYLE.STACKED;
const logoConfiguration =
currentApplicationDetails?.applicationDetail?.navigationSetting
?.logoConfiguration ||
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE;
const primaryColor = get(
selectedTheme,
"properties.colors.primaryColor",
@ -86,19 +91,20 @@ const TopHeader = (props: TopHeaderProps) => {
setMenuOpen={setMenuOpen}
/>
{currentUser?.username !== ANONYMOUS_USERNAME && (
<BackToHomeButton
<NavigationLogo logoConfiguration={logoConfiguration} />
{(logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE ||
logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION
.APPLICATION_TITLE_ONLY) && (
<ApplicationName
appName={currentApplicationDetails?.name}
navColorStyle={navColorStyle}
navStyle={navStyle}
primaryColor={primaryColor}
/>
)}
<ApplicationName
appName={currentApplicationDetails?.name}
navColorStyle={navColorStyle}
navStyle={navStyle}
primaryColor={primaryColor}
/>
</section>
{currentApplicationDetails?.applicationDetail?.navigationSetting
@ -126,6 +132,10 @@ const TopHeader = (props: TopHeaderProps) => {
primaryColor={primaryColor}
url={editorURL}
/>
<BackToAppsButton
currentApplicationDetails={currentApplicationDetails}
/>
</HeaderRightItemContainer>
</div>
)}

View File

@ -23,6 +23,7 @@ import { get } from "lodash";
import { PageMenuContainer, StyledNavLink } from "./PageMenu.styled";
import { StyledCtaContainer } from "./Navigation/Sidebar.styled";
import ShareButton from "./Navigation/components/ShareButton";
import BackToAppsButton from "./Navigation/components/BackToAppsButton";
type NavigationProps = {
isOpen?: boolean;
@ -144,6 +145,11 @@ export function PageMenu(props: NavigationProps) {
/>
)}
<BackToAppsButton
currentApplicationDetails={application}
insideSidebar
/>
{!hideWatermark && (
<a
className="flex hover:no-underline mt-2"

View File

@ -42,6 +42,9 @@ export const initialState: any = {
},
},
ui: {
editor: {
isPreviewMode: false,
},
appSettingsPane: {
isOpen: false,
},

View File

@ -1,5 +1,5 @@
import React, { useMemo } from "react";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import Button from "./AppViewerButton";
import { AUTH_LOGIN_URL } from "constants/routes";
import {
@ -9,6 +9,7 @@ import {
import {
getCurrentApplication,
getCurrentPageId,
previewModeSelector,
} from "selectors/editorSelectors";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import {
@ -24,9 +25,10 @@ import { viewerURL } from "RouteBuilder";
import { useHistory } from "react-router";
import { useHref } from "pages/Editor/utils";
import type { NavigationSetting } from "constants/AppConstants";
import { Icon } from "design-system-old";
import { Icon, TooltipComponent } from "design-system-old";
import { getApplicationNameTextColor } from "./utils";
import { ButtonVariantTypes } from "components/constants";
import { setPreviewModeInitAction } from "actions/editorActions";
/**
* ---------------------------------------------------------------------------------------------------
@ -65,6 +67,8 @@ function PrimaryCTA(props: Props) {
const permissionRequired = PERMISSION_TYPE.MANAGE_APPLICATION;
const userPermissions = currentApplication?.userPermissions ?? [];
const canEdit = isPermitted(userPermissions, permissionRequired);
const isPreviewMode = useSelector(previewModeSelector);
const dispatch = useDispatch();
const appViewerURL = useHref(viewerURL, {
pageId: currentPageID,
@ -95,28 +99,46 @@ function PrimaryCTA(props: Props) {
const PrimaryCTA = useMemo(() => {
if (url && canEdit) {
return (
<Button
borderRadius={selectedTheme.properties.borderRadius.appBorderRadius}
className={className}
icon={
<Icon
fillColor={getApplicationNameTextColor(
primaryColor,
navColorStyle,
)}
name="edit-line"
size="extraLarge"
/>
}
insideSidebar={insideSidebar}
isMinimal={isMinimal}
navColorStyle={navColorStyle}
onClick={() => {
history.push(url);
<TooltipComponent
boundary="viewport"
content={createMessage(EDIT_APP)}
disabled={insideSidebar}
hoverOpenDelay={500}
modifiers={{
preventOverflow: {
enabled: true,
boundariesElement: "viewport",
},
}}
primaryColor={primaryColor}
text={insideSidebar && !isMinimal && createMessage(EDIT_APP)}
/>
position="bottom"
>
<Button
borderRadius={selectedTheme.properties.borderRadius.appBorderRadius}
className={className}
icon={
<Icon
fillColor={getApplicationNameTextColor(
primaryColor,
navColorStyle,
)}
name="edit-line"
size="extraLarge"
/>
}
insideSidebar={insideSidebar}
isMinimal={isMinimal}
navColorStyle={navColorStyle}
onClick={() => {
if (isPreviewMode) {
dispatch(setPreviewModeInitAction(!isPreviewMode));
} else {
history.push(url);
}
}}
primaryColor={primaryColor}
text={insideSidebar && !isMinimal && createMessage(EDIT_APP)}
/>
</TooltipComponent>
);
}
@ -140,6 +162,7 @@ function PrimaryCTA(props: Props) {
}}
primaryColor={primaryColor}
text={createMessage(FORK_APP)}
varient={ButtonVariantTypes.SECONDARY}
/>
);
}
@ -163,6 +186,7 @@ function PrimaryCTA(props: Props) {
navColorStyle={navColorStyle}
primaryColor={primaryColor}
text={createMessage(FORK_APP)}
varient={ButtonVariantTypes.SECONDARY}
/>
}
/>

View File

@ -0,0 +1,124 @@
import { Button, Size, Spinner } from "design-system-old";
import React, { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import {
getIsDeletingNavigationLogo,
getIsUploadingNavigationLogo,
} from "@appsmith/selectors/applicationSelectors";
import styled from "styled-components";
import classNames from "classnames";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
type ImageInputProps = {
value?: any;
onChange?(value?: any): void;
validate?(
e: React.ChangeEvent<HTMLInputElement>,
callback?: (e: React.ChangeEvent<HTMLInputElement>) => void,
): void;
className?: string;
defaultValue?: string;
};
const StyledImg = styled.img`
object-fit: contain;
max-width: 200px;
max-height: 100px;
`;
export const ImageInput = (props: ImageInputProps) => {
const { className, onChange, validate, value } = props;
const fileInputRef = useRef<HTMLInputElement>(null);
const isUploadingNavigationLogo = useSelector(getIsUploadingNavigationLogo);
const isDeletingNavigationLogo = useSelector(getIsDeletingNavigationLogo);
const [isLogoLoaded, setIsLogoLoaded] = useState(false);
// trigger file input on click of upload logo button
const onFileInputClick = () => {
fileInputRef.current?.click();
};
// on upload, pass the file to api
const onFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsLogoLoaded(false);
const file = e.target.files?.[0];
if (!file) return;
// validate the file, if invalid, show error
// else call the callback
validate &&
validate(e, () => {
onChange && onChange(file);
});
};
useEffect(() => {
if (isDeletingNavigationLogo) {
setIsLogoLoaded(false);
// reset the input to accept the same file again if delete happens
if (fileInputRef?.current) {
fileInputRef.current.value = "";
}
}
}, [isDeletingNavigationLogo]);
const renderLogo = () => {
if (isUploadingNavigationLogo || isDeletingNavigationLogo) {
return (
<div className="px-4 py-10 w-full flex justify-center">
<Spinner size="extraExtraExtraExtraLarge" />
</div>
);
}
if (value) {
return (
<StyledImg
alt="Your application's logo"
className={classNames({
hidden: !isLogoLoaded,
})}
onLoad={() => setIsLogoLoaded(true)}
src={getAssetUrl(value)}
/>
);
} else {
return "No logo set";
}
};
return (
<div
className={`relative flex items-center justify-center w-full border h-28 group ${
className ? className : ""
}`}
>
{renderLogo()}
<div className="absolute inset-0 items-center justify-center hidden gap-2 bg-black group-hover:flex bg-opacity-20">
<Button
icon="upload-line"
iconPosition="left"
onClick={onFileInputClick}
size={Size.medium}
text="Upload"
>
Upload
</Button>
</div>
<input
accept="image/jpeg, image/png"
className="hidden"
onChange={onFileInputChange}
ref={fileInputRef}
type="file"
/>
</div>
);
};
export default ImageInput;

View File

@ -1,43 +0,0 @@
import {
APP_NAVIGATION_SETTING,
createMessage,
} from "@appsmith/constants/messages";
import type { NavigationSetting } from "constants/AppConstants";
import type { DropdownOption } from "design-system-old";
import { Dropdown, Text, TextType } from "design-system-old";
import React from "react";
import type { UpdateSetting } from ".";
const LogoConfiguration = (props: {
options: DropdownOption[];
navigationSetting: NavigationSetting;
updateSetting: UpdateSetting;
}) => {
const { options } = props;
const unavailableLabel = " - [Unavailable atm]";
const handleOnSelect = (value?: { label: string; value: string }) => {
props.updateSetting("logoConfiguration", value?.value || "");
};
return (
<div className="pt-4">
<Text type={TextType.P1}>
{createMessage(APP_NAVIGATION_SETTING.logoConfigurationLabel) +
unavailableLabel}
</Text>
<Dropdown
onSelect={handleOnSelect}
options={options}
selected={options.find(
(item) => item.value === props.navigationSetting?.logoConfiguration,
)}
showLabelOnly
width="100%"
/>
</div>
);
};
export default LogoConfiguration;

View File

@ -0,0 +1,58 @@
import React from "react";
import type { Dispatch, SetStateAction } from "react";
import StyledPropertyHelpLabel from "./StyledPropertyHelpLabel";
import SwitchWrapper from "../../Components/SwitchWrapper";
import { Switch } from "design-system-old";
import type { LogoConfigurationSwitches } from ".";
import _ from "lodash";
const SwitchSettingForLogoConfiguration = (props: {
label: string;
keyName: keyof LogoConfigurationSwitches;
tooltip?: string;
logoConfigurationSwitches: LogoConfigurationSwitches;
setLogoConfigurationSwitches: Dispatch<
SetStateAction<LogoConfigurationSwitches>
>;
}) => {
const {
keyName,
label,
logoConfigurationSwitches,
setLogoConfigurationSwitches,
tooltip,
} = props;
// updateSetting("logoConfiguration", !isChecked);
// logEvent(keyName as keyof StringsFromNavigationSetting, !isChecked);
return (
<div className="pt-4">
<div className="flex justify-between content-center">
<StyledPropertyHelpLabel
label={label}
lineHeight="1.17"
maxWidth="270px"
tooltip={tooltip}
/>
<SwitchWrapper>
<Switch
checked={logoConfigurationSwitches[keyName]}
className="mb-0"
id={`t--navigation-settings-${_.kebabCase(keyName)}`}
large
onChange={() => {
setLogoConfigurationSwitches({
...logoConfigurationSwitches,
[keyName]: !logoConfigurationSwitches[keyName],
});
}}
/>
</SwitchWrapper>
</div>
</div>
);
};
export default SwitchSettingForLogoConfiguration;

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
import {
@ -21,13 +21,14 @@ import equal from "fast-deep-equal";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { updateApplication } from "@appsmith/actions/applicationActions";
import { Spinner } from "design-system-old";
import LogoInput from "@appsmith/pages/Editor/NavigationSettings/LogoInput";
import SwitchSettingForLogoConfiguration from "./SwitchSettingForLogoConfiguration";
/**
* TODO - @Dhruvik - ImprovedAppNav
* Revisit these imports in v1.1
* https://www.notion.so/appsmith/Ship-Faster-33b32ed5b6334810a0b4f42e03db4a5b?pvs=4
*/
// import LogoConfiguration from "./LogoConfiguration";
// import { ReactComponent as NavPositionStickyIcon } from "assets/icons/settings/nav-position-sticky.svg";
// import { ReactComponent as NavPositionStaticIcon } from "assets/icons/settings/nav-position-static.svg";
// import { ReactComponent as NavStyleMinimalIcon } from "assets/icons/settings/nav-style-minimal.svg";
@ -37,6 +38,11 @@ export type UpdateSetting = (
value: NavigationSetting[keyof NavigationSetting],
) => void;
export type LogoConfigurationSwitches = {
logo: boolean;
applicationTitle: boolean;
};
function NavigationSettings() {
const application = useSelector(getCurrentApplication);
const applicationId = useSelector(getCurrentApplicationId);
@ -44,6 +50,81 @@ function NavigationSettings() {
const [navigationSetting, setNavigationSetting] = useState(
application?.applicationDetail?.navigationSetting,
);
const [logoConfigurationSwitches, setLogoConfigurationSwitches] =
useState<LogoConfigurationSwitches>({
logo: false,
applicationTitle: false,
});
useEffect(() => {
setNavigationSetting(application?.applicationDetail?.navigationSetting);
// Logo configuration
switch (navigationSetting?.logoConfiguration) {
case NAVIGATION_SETTINGS.LOGO_CONFIGURATION.APPLICATION_TITLE_ONLY:
setLogoConfigurationSwitches({
logo: false,
applicationTitle: true,
});
break;
case NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE:
setLogoConfigurationSwitches({
logo: true,
applicationTitle: true,
});
break;
case NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_ONLY:
setLogoConfigurationSwitches({
logo: true,
applicationTitle: false,
});
break;
case NAVIGATION_SETTINGS.LOGO_CONFIGURATION.NO_LOGO_OR_APPLICATION_TITLE:
setLogoConfigurationSwitches({
logo: false,
applicationTitle: false,
});
break;
default:
break;
}
}, [application?.applicationDetail?.navigationSetting]);
useEffect(() => {
if (
logoConfigurationSwitches.logo &&
logoConfigurationSwitches.applicationTitle
) {
updateSetting(
"logoConfiguration",
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE,
);
} else if (
logoConfigurationSwitches.logo &&
!logoConfigurationSwitches.applicationTitle
) {
updateSetting(
"logoConfiguration",
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_ONLY,
);
} else if (
!logoConfigurationSwitches.logo &&
logoConfigurationSwitches.applicationTitle
) {
updateSetting(
"logoConfiguration",
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.APPLICATION_TITLE_ONLY,
);
} else if (
!logoConfigurationSwitches.logo &&
!logoConfigurationSwitches.applicationTitle
) {
updateSetting(
"logoConfiguration",
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.NO_LOGO_OR_APPLICATION_TITLE,
);
}
}, [logoConfigurationSwitches]);
const updateSetting = useCallback(
debounce(
@ -56,7 +137,7 @@ function NavigationSettings() {
isPlainObject(navigationSetting) &&
!isEmpty(navigationSetting)
) {
const newSettings = {
const newSettings: NavigationSetting = {
...navigationSetting,
[key]: value,
};
@ -104,14 +185,9 @@ function NavigationSettings() {
// }
// }
if (payload.applicationDetail) {
payload.applicationDetail.navigationSetting =
newSettings as NavigationSetting;
} else {
payload.applicationDetail = {
navigationSetting: newSettings as NavigationSetting,
};
}
payload.applicationDetail = {
navigationSetting: newSettings,
};
dispatch(updateApplication(applicationId, payload));
setNavigationSetting(newSettings);
@ -163,7 +239,8 @@ function NavigationSettings() {
{/**
* TODO - @Dhruvik - ImprovedAppNav
* Remove check for orientation = top in v1.1
* Remove check for orientation = top when adding sidebar minimal to show sidebar
* variants as well.
* https://www.notion.so/appsmith/Ship-Faster-33b32ed5b6334810a0b4f42e03db4a5b
*/}
{navigationSetting?.orientation ===
@ -303,46 +380,31 @@ function NavigationSettings() {
updateSetting={updateSetting}
/>
{/**
* TODO - @Dhruvik - ImprovedAppNav
* Hiding logo config for v1
* https://www.notion.so/appsmith/Logo-configuration-option-can-be-multiselect-2a436598539c4db99d1f030850fd8918?pvs=4
*/}
{/* <LogoConfiguration
navigationSetting={navigationSetting}
options={[
{
label: _.startCase(
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE,
),
value:
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE,
},
{
label: _.startCase(
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_ONLY,
),
value: NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_ONLY,
},
{
label: _.startCase(
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.APPLICATION_TITLE_ONLY,
),
value:
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.APPLICATION_TITLE_ONLY,
},
{
label: _.startCase(
NAVIGATION_SETTINGS.LOGO_CONFIGURATION
.NO_LOGO_OR_APPLICATION_TITLE,
),
value:
NAVIGATION_SETTINGS.LOGO_CONFIGURATION
.NO_LOGO_OR_APPLICATION_TITLE,
},
]}
updateSetting={updateSetting}
/> */}
<SwitchSettingForLogoConfiguration
keyName="logo"
label={createMessage(APP_NAVIGATION_SETTING.showLogoLabel)}
logoConfigurationSwitches={logoConfigurationSwitches}
setLogoConfigurationSwitches={setLogoConfigurationSwitches}
/>
{(navigationSetting?.logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_AND_APPLICATION_TITLE ||
navigationSetting?.logoConfiguration ===
NAVIGATION_SETTINGS.LOGO_CONFIGURATION.LOGO_ONLY) && (
<LogoInput
navigationSetting={navigationSetting}
updateSetting={updateSetting}
/>
)}
<SwitchSettingForLogoConfiguration
keyName="applicationTitle"
label={createMessage(
APP_NAVIGATION_SETTING.showApplicationTitleLabel,
)}
logoConfigurationSwitches={logoConfigurationSwitches}
setLogoConfigurationSwitches={setLogoConfigurationSwitches}
/>
<SwitchSetting
keyName="showSignIn"

View File

@ -4,6 +4,11 @@ import type {
} from "constants/AppConstants";
import { keysOfNavigationSetting } from "constants/AppConstants";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { Toaster, Variant } from "design-system-old";
import {
APP_NAVIGATION_SETTING,
createMessage,
} from "@appsmith/constants/messages";
export const logEvent = (
keyName: keyof StringsFromNavigationSetting,
@ -39,3 +44,48 @@ export const logEvent = (
break;
}
};
/**
* validates the uploaded logo file
*
* checks:
* 1. file size max 1MB
* 2. file type - jpg, or png
*
* @param e
* @param callback
* @returns
*/
export const logoImageValidation = (
e: React.ChangeEvent<HTMLInputElement>,
callback?: (e: React.ChangeEvent<HTMLInputElement>) => void,
) => {
const file = e.target.files?.[0];
// case 1: no file selected
if (!file) return false;
// case 2: file size > 2mb
if (file.size > 2 * 1024 * 1024) {
Toaster.show({
text: createMessage(APP_NAVIGATION_SETTING.logoUploadSizeError),
variant: Variant.danger,
});
return false;
}
// case 3: image selected
const validTypes = ["image/jpeg", "image/png"];
if (!validTypes.includes(file.type)) {
Toaster.show({
text: createMessage(APP_NAVIGATION_SETTING.logoUploadFormatError),
variant: Variant.danger,
});
return false;
}
callback && callback(e);
};

View File

@ -9,10 +9,23 @@ function AppSettingsPane() {
const dispatch = useDispatch();
const paneRef = useRef(null);
const portalRef = useRef(null);
// Close app settings pane when clicked outside
useOnClickOutside([paneRef, portalRef], () => {
if (document.getElementById("save-theme-modal")) return;
if (document.getElementById("delete-theme-modal")) return;
if (document.getElementById("manual-upgrades-modal")) return;
// If logo configuration navigation setting dropdown is open
if (
document.getElementsByClassName(
"t--navigation-settings-logo-configuration",
)?.[0] &&
document.getElementsByClassName("bp3-overlay-open")?.[0]
) {
return;
}
// No id property for `Dialog` component, so using class name
if (document.querySelector(".t--import-application-modal")) {
return;

View File

@ -45,6 +45,7 @@ const Container = styled.section<{
$isAutoLayout: boolean;
background: string;
isPreviewingNavigation?: boolean;
isAppSettingsPaneWithNavigationTabOpen?: boolean;
navigationHeight?: number;
}>`
width: ${({ $isAutoLayout }) =>
@ -56,12 +57,33 @@ const Container = styled.section<{
overflow-y: auto;
background: ${({ background }) => background};
${({ isPreviewingNavigation, navigationHeight }) => {
${({
isAppSettingsPaneWithNavigationTabOpen,
isPreviewingNavigation,
navigationHeight,
}) => {
let css = ``;
if (isPreviewingNavigation) {
return `
css += `
margin-top: ${navigationHeight}px !important;
`;
}
if (isAppSettingsPaneWithNavigationTabOpen) {
/**
* We need to remove the scrollbar width to avoid small white space on the
* right of the canvas since we disable all interactions, including scroll,
* while the app settings pane with navigation tab is open
*/
css += `
::-webkit-scrollbar {
width: 0px;
}
`;
}
return css;
}}
&:before {
@ -171,6 +193,9 @@ function CanvasContainer(props: CanvasContainerProps) {
"mt-24": shouldShowSnapShotBanner,
})}
id={"canvas-viewport"}
isAppSettingsPaneWithNavigationTabOpen={
isAppSettingsPaneWithNavigationTabOpen
}
isPreviewingNavigation={isPreviewingNavigation}
key={currentPageId}
navigationHeight={navigationHeight}

View File

@ -22,6 +22,8 @@ const NavigationPreview = forwardRef(
isPreviewMode || isAppSettingsPaneWithNavigationTabOpen,
"-translate-y-full duration-0":
!isPreviewMode || !isAppSettingsPaneWithNavigationTabOpen,
"select-none pointer-events-none":
isAppSettingsPaneWithNavigationTabOpen,
})}
ref={ref}
>

View File

@ -54,6 +54,7 @@ import SnapShotBannerCTA from "../CanvasLayoutConversion/SnapShotBannerCTA";
import { APP_MODE } from "entities/App";
import { getSelectedAppTheme } from "selectors/appThemingSelectors";
import { useIsMobileDevice } from "utils/hooks/useDeviceDetect";
import classNames from "classnames";
import { getSnapshotUpdatedTime } from "selectors/autoLayoutSelectors";
import { getReadableSnapShotDetails } from "utils/autoLayout/AutoLayoutUtils";
@ -182,8 +183,15 @@ function WidgetsEditor() {
{guidedTourEnabled && <Guide />}
<div className="relative flex flex-row w-full overflow-hidden">
<div className="relative flex flex-col w-full overflow-hidden">
<CanvasTopSection />
<div
className={classNames({
"relative flex flex-col w-full overflow-hidden": true,
"m-8 border border-gray-200":
isAppSettingsPaneWithNavigationTabOpen,
})}
>
{!isAppSettingsPaneWithNavigationTabOpen && <CanvasTopSection />}
<div
className="relative flex flex-row w-full overflow-hidden"
data-testid="widgets-editor"
@ -198,7 +206,12 @@ function WidgetsEditor() {
{showNavigation()}
<PageViewContainer
className="relative flex flex-row w-full justify-center overflow-hidden"
className={classNames({
"relative flex flex-row w-full justify-center overflow-hidden":
true,
"select-none pointer-events-none":
isAppSettingsPaneWithNavigationTabOpen,
})}
hasPinnedSidebar={
isPreviewingNavigation && !isMobile
? currentApplicationDetails?.applicationDetail