Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions map/src/context/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ export const AppContextProvider = (props) => {
add: false,
location: null,
});

// Registry of exit guards: { key: guardFn }. Components register via useExitGuard({ register }).
// Callers: const guard = ctx.exitGuards.wptEdit; guard ? guard(action) : action();
const [exitGuards, setExitGuards] = useState({});

const [processingGroups, setProcessingGroups] = useState(false);
const [favLoading, setFavLoading] = useState(false);
const [removeFavGroup, setRemoveFavGroup] = useState(null);
Expand Down Expand Up @@ -555,6 +560,8 @@ export const AppContextProvider = (props) => {
setFavorites,
addFavorite,
setAddFavorite,
exitGuards,
setExitGuards,
localTracks,
setLocalTracks,
currentObjectType,
Expand Down
9 changes: 9 additions & 0 deletions map/src/dialogs/dialog.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
letter-spacing: 0.28px !important;
text-transform: uppercase !important;
}
.contentText {
font-size: 14px !important;
font-weight: 400 !important;
line-height: 20px !important;
letter-spacing: 0.25px !important;
color: var(--text-secondary) !important;
align-self: stretch !important;
padding: 12px 0 !important;
}
/* 280px (.title width) + 24px * 2 (left/right padding from .title) */
.dialog :global(.MuiPaper-root) {
max-width: 328px !important;
Expand Down
27 changes: 27 additions & 0 deletions map/src/dialogs/favorites/ExitWithoutSavingDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material';
import { useTranslation } from 'react-i18next';
import dialogStyles from '../dialog.module.css';

export default function ExitWithoutSavingDialog({ open, onKeepEditing, onExit }) {
const { t } = useTranslation();

return (
<Dialog className={dialogStyles.dialog} open={open} onClose={onKeepEditing}>
<DialogTitle className={dialogStyles.title}>{t('web:exit_without_saving')}</DialogTitle>
<DialogContent className={dialogStyles.content}>
<DialogContentText className={dialogStyles.contentText}>
{t('web:all_changes_will_be_lost')}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button id="se-exit-dialog-keep-editing" className={dialogStyles.button} onClick={onKeepEditing}>
{t('web:keep_editing')}
</Button>
<Button id="se-exit-dialog-exit" className={dialogStyles.button} onClick={onExit}>
{t('web:shared_string_exit')}
</Button>
</DialogActions>
</Dialog>
);
}
19 changes: 19 additions & 0 deletions map/src/frame/components/items/ChevronItem.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import { ListItemIcon, ListItemText, MenuItem, Typography } from '@mui/material';
import { ReactComponent as ChevronIcon } from '../../../assets/icons/ic_action_arrow_up.svg';
import styles from './items.module.css';

export default function ChevronItem({ id, icon = null, title, value, onClick, disabled = false }) {
return (
<MenuItem id={id} className={`${styles.item} ${styles.itemChevron}`} onClick={onClick} disabled={disabled}>
{icon && <ListItemIcon className={styles.icon}>{icon}</ListItemIcon>}
<ListItemText>
<Typography className={styles.mainText}>{title}</Typography>
</ListItemText>
<div className={styles.itemChevronRight}>
{value !== undefined && <Typography className={styles.addInfo}>{value}</Typography>}
<ChevronIcon className={styles.sectionRowChevron} />
</div>
</MenuItem>
);
}
11 changes: 10 additions & 1 deletion map/src/frame/components/items/items.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
gap: 24px !important;
min-height: 48px !important;
}
.itemChevron {
padding-right: 12px !important;
}
.itemChevronRight {
display: flex !important;
align-items: center !important;
gap: 6px !important;
flex-shrink: 0 !important;
}
.mainText {
color: var(--text-primary) !important;
font-size: 16px !important;
Expand Down Expand Up @@ -136,7 +145,7 @@
.sectionRow {
width: 360px !important;
min-height: 48px !important;
padding: 12px 20px !important;
padding: 12px 12px 12px 16px !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
Expand Down
18 changes: 14 additions & 4 deletions map/src/infoblock/components/InformationBlock.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,20 @@ export default function InformationBlock({

// Close WptEditPanel when the user navigates to another object or switches context
useEffect(() => {
ctx.setAddFavorite((prev) => {
if (!prev?.location && !prev?.editWpt) return prev;
return { ...prev, add: false, location: null, editTrack: false, editWpt: null, previewAppearance: null };
});
const close = () =>
ctx.setAddFavorite((prev) => {
if (!prev?.location && !prev?.editWpt) return prev;
return {
...prev,
add: false,
location: null,
editTrack: false,
editWpt: null,
previewAppearance: null,
};
});
const guard = ctx.exitGuards.wptEdit;
guard ? guard(close) : close();
}, [ctx.selectedWpt, ctx.currentObjectType]);

useEffect(() => {
Expand Down
41 changes: 40 additions & 1 deletion map/src/infoblock/components/favorite/WptEditPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import FavoritesManager, {
} from '../../../manager/FavoritesManager';
import FavoriteHelper from './FavoriteHelper';
import DeleteWptDialog from '../../../dialogs/favorites/DeleteWptDialog';
import ExitWithoutSavingDialog from '../../../dialogs/favorites/ExitWithoutSavingDialog';
import useExitGuard from '../../../util/hooks/useExitGuard';
import { ADDRESS_NOT_FOUND } from '../wpt/WptDetails';
import { FINAL_POI_ICON_NAME, WEB_POI_PREFIX, WEB_PREFIX } from '../wpt/WptTagsProvider';
import TracksManager, { GPX_FILE_EXT } from '../../../manager/track/TracksManager';
Expand Down Expand Up @@ -60,7 +62,9 @@ export default function WptEditPanel({ setShowInfoBlock }) {
const useSelected = !isEmpty(ctx.selectedGpxFile);

const [favoriteName, setFavoriteName] = useState(editWpt?.name ?? '');
const [initialName, setInitialName] = useState(editWpt?.name ?? '');
const [favoriteAddress, setFavoriteAddress] = useState(editWpt?.address ?? ctx.addFavorite?.address ?? '');
const [initialAddress, setInitialAddress] = useState(editWpt?.address ?? ctx.addFavorite?.address ?? '');
const [favoriteDescription, setFavoriteDescription] = useState(editWpt?.desc ?? '');
const [addAddress, setAddAddress] = useState(isEditMode || isPoi || (isAddMode && !isAddTrackWpt));
const [activePanel, setActivePanel] = useState(null); // null | 'description' | 'icon' | 'color'
Expand All @@ -78,6 +82,34 @@ export default function WptEditPanel({ setShowInfoBlock }) {
const [latLon, setLatLon] = useState(null);
const [deleteWptDialogOpen, setDeleteWptDialogOpen] = useState(false);

const hasEditChanges =
favoriteName !== (editWpt?.name ?? '') ||
favoriteDescription !== (editWpt?.desc ?? '') ||
favoriteAddress !== (editWpt?.address ?? '') ||
(favoriteGroup !== null && favoriteGroup.id !== editWpt?.groupId) ||
favoriteIcon !== (editWpt?.icon ?? MarkerOptions.DEFAULT_WPT_ICON) ||
favoriteColor !== (editWpt?.color ?? MarkerOptions.DEFAULT_WPT_COLOR) ||
favoriteShape !== (editWpt?.background ?? MarkerOptions.BACKGROUND_WPT_SHAPE_CIRCLE);

const hasAddChanges =
favoriteName !== initialName || favoriteDescription !== '' || favoriteAddress !== initialAddress;
Comment thread
alisa911 marked this conversation as resolved.
Outdated

const hasChanges = isEditMode ? hasEditChanges : hasAddChanges;

const { guardAction, dialog } = useExitGuard({
hasChanges,
renderDialog: ({ onKeepEditing, onExit }) => (
<ExitWithoutSavingDialog open={true} onKeepEditing={onKeepEditing} onExit={onExit} />
),
register: (fn) =>
ctx.setExitGuards((prev) => {
if (fn) return { ...prev, wptEdit: fn };
const next = { ...prev };
delete next.wptEdit;
return next;
}),
});

useEffect(() => {
getIconCategories().then();
if (!isTrackWpt) {
Expand Down Expand Up @@ -499,6 +531,10 @@ export default function WptEditPanel({ setShowInfoBlock }) {
setActivePanel((prev) => (prev === panel ? null : panel));
}

function handleClose() {
guardAction(closePanel);
}

const groups = isTrackWpt ? ctx.selectedGpxFile?.pointsGroups : ctx.favorites.groups;
const defaultGroup = isAddTrackWpt
? DEFAULT_GROUP_NAME_POINTS_GROUPS
Expand All @@ -515,6 +551,7 @@ export default function WptEditPanel({ setShowInfoBlock }) {

return (
<>
{dialog}
{activePanel === 'description' && (
<DescriptionPanel
description={favoriteDescription}
Expand Down Expand Up @@ -544,7 +581,7 @@ export default function WptEditPanel({ setShowInfoBlock }) {
{process && <LinearProgress />}
<HeaderWithUnderline
title={title}
onClose={closePanel}
onClose={handleClose}
showBackButton={isEditMode}
appBarProps={{ id: isEditMode ? 'se-back-edit-wpt-panel' : 'se-close-add-wpt-panel' }}
rightContent={
Expand All @@ -563,6 +600,7 @@ export default function WptEditPanel({ setShowInfoBlock }) {
<FavoriteName
favoriteName={favoriteName}
setFavoriteName={setFavoriteName}
onAutoFill={isAddMode ? setInitialName : undefined}
favoriteGroup={favoriteGroup}
favorite={isEditMode ? editWpt : undefined}
setErrorName={setErrorName}
Expand All @@ -584,6 +622,7 @@ export default function WptEditPanel({ setShowInfoBlock }) {
<FavoriteAddress
favoriteAddress={favoriteAddress}
setFavoriteAddress={setFavoriteAddress}
onAutoFill={setInitialAddress}
widthDialog={PANEL_CONTENT_WIDTH}
latLon={isAddMode ? latLon : null}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export default function ColorSelectionPanel({ selectedColor, setSelectedColor, f
const ok = await saveColorPalette(updated, ctx.setNotification);
if (ok) {
setColors(updated);
if (removed.value === selectedColor) {
const colorStillAvailable = updated.some((c) => c.value === removed.value);
if (removed.value === selectedColor && !colorStillAvailable) {
setSelectedColor(updated[0]?.value ?? MarkerOptions.DEFAULT_WPT_COLOR);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import { getAddressByLatLon } from '../../wpt/WptDetails';
import styles from '../wptEditPanel.module.css';

export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, widthDialog, latLon }) {
export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, onAutoFill, widthDialog, latLon }) {
const { t } = useTranslation();

const [searching, setSearching] = useState(false);
Expand All @@ -19,8 +19,9 @@ export default function FavoriteAddress({ favoriteAddress, setFavoriteAddress, w
function searchAddress() {
if (!latLon?.lat || !latLon?.lon) return;
setSearching(true);
getAddressByLatLon(latLon.lat, latLon.lon).then((address) => {
setFavoriteAddress(address ?? '');
getAddressByLatLon(latLon.lat, latLon.lon).then((addr = '') => {
setFavoriteAddress(addr);
onAutoFill?.(addr);
setSearching(false);
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useState } from 'react';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useTranslation } from 'react-i18next';
import FavoritesManager from '../../../../manager/FavoritesManager';
import SelectItemWithoutOptions from '../../../../frame/components/items/SelectItemWithoutOptions';
import ChevronItem from '../../../../frame/components/items/ChevronItem';
import FolderSelectionPanel from './FolderSelectionPanel';

export default function FavoriteGroup({ favoriteGroup, setFavoriteGroup, defaultGroup, isTrackWpt }) {
Expand All @@ -14,12 +13,10 @@ export default function FavoriteGroup({ favoriteGroup, setFavoriteGroup, default

return (
<>
<SelectItemWithoutOptions
<ChevronItem
id="se-fav-group-selector"
title={t('folder')}
value={displayName}
boldTitle={false}
endIcon={<ChevronRightIcon sx={{ color: 'var(--text-secondary)' }} />}
onClick={() => setPanelOpen((o) => !o)}
/>
{panelOpen && (
Expand Down
10 changes: 5 additions & 5 deletions map/src/infoblock/components/favorite/structure/FavoriteName.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styles from '../wptEditPanel.module.css';
export default function FavoriteName({
favoriteName,
setFavoriteName,
onAutoFill,
favoriteGroup,
favorite,
setErrorName,
Expand Down Expand Up @@ -75,13 +76,12 @@ export default function FavoriteName({
const objOptions = ctx.selectedWpt.poi?.options ?? ctx.selectedWpt.poi?.properties;
const { name } = getPropsFromSearchResultItem(objOptions, t);
setFavoriteName(name);
onAutoFill?.(name);
} else if (ctx.selectedWpt?.stop) {
const name = ctx.selectedWpt?.stop.options.name;
if (name && name.trim() !== '') {
setFavoriteName(name);
} else {
setFavoriteName(t('web:transport_stop'));
}
const resolved = name && name.trim() !== '' ? name : t('web:transport_stop');
setFavoriteName(resolved);
onAutoFill?.(resolved);
}
}, [ctx.selectedWpt]);

Expand Down
31 changes: 10 additions & 21 deletions map/src/login/garmin/GarminConnectedView.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import React, { useContext } from 'react';
import { Box, Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { ReactComponent as FolderIcon } from '../../assets/icons/ic_action_folder.svg';
import { ReactComponent as SyncIcon } from '../../assets/icons/ic_action_update.svg';
import { ReactComponent as LogoutIcon } from '../../assets/icons/ic_action_logout.svg';
import { ReactComponent as ExternalLinkIcon } from '../../assets/icons/ic_action_external_link.svg';
import { ReactComponent as ChevronIcon } from '../../assets/icons/ic_action_arrow_up.svg';
import { ReactComponent as FilterIcon } from '../../assets/icons/ic_action_filter.svg';
import ThickDivider from '../../frame/components/dividers/ThickDivider';
import SubTitleMenu from '../../frame/components/titles/SubTitleMenu';
import ChevronItem from '../../frame/components/items/ChevronItem';
import DefaultItem from '../../frame/components/items/DefaultItem';
import DividerWithMargin from '../../frame/components/dividers/DividerWithMargin';
import { MAIN_URL_WITH_SLASH, TRACKS_URL } from '../../manager/GlobalManager';
Expand All @@ -19,7 +18,6 @@ import AppContext from '../../context/AppContext';
import { GARMIN_FOLDER_NAME } from './garminApi';
import { GARMIN_ACTIVITY_GROUPS } from './GarminActivitiesToSync';
import { useRecentDataSaver } from '../../util/hooks/menu/useRecentDataSaver';
import styles from '../../frame/components/items/items.module.css';

export default function GarminConnectedView({
syncTimeMs,
Expand Down Expand Up @@ -76,18 +74,18 @@ export default function GarminConnectedView({
return (
<>
<SubTitleMenu text={t('web:my_data')} />
<DefaultItem
<ChevronItem
icon={<FolderIcon />}
name={t('web:garmin_activities')}
title={t('web:garmin_activities')}
value={String(activitiesCount)}
onClick={handleActivitiesClick}
rightSlot={<RightWithChevron text={String(activitiesCount)} />}
/>
<DividerWithMargin margin={'64px'} />
<DefaultItem
<ChevronItem
icon={<SyncIcon />}
name={t('web:garmin_last_sync')}
title={t('web:garmin_last_sync')}
value={syncTimeMs ? formatTimeAgo(syncTimeMs, t) : '—'}
onClick={handleLastActivityClick}
rightSlot={<RightWithChevron text={syncTimeMs ? formatTimeAgo(syncTimeMs, t) : '—'} />}
/>
<DividerWithMargin margin={'64px'} />
<DefaultItem
Expand All @@ -98,11 +96,11 @@ export default function GarminConnectedView({
/>
<ThickDivider mt={'0px'} mb={'0px'} />
<SubTitleMenu text={t('shared_string_settings')} />
<DefaultItem
<ChevronItem
icon={<FilterIcon />}
name={t('web:garmin_activities_to_sync')}
title={t('web:garmin_activities_to_sync')}
value={activityTypesLabel}
onClick={onActivitiesToSyncClick}
rightSlot={<RightWithChevron text={activityTypesLabel} />}
/>
<ThickDivider mt={'0px'} mb={'0px'} />
<DefaultItem
Expand All @@ -118,15 +116,6 @@ export default function GarminConnectedView({
);
}

function RightWithChevron({ text }) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<Typography className={styles.addInfo}>{text}</Typography>
<ChevronIcon className={styles.sectionRowChevron} />
</Box>
);
}

function handleViewOnGarminClick() {
globalThis.open('https://connect.garmin.com/app/activities', '_blank', 'noopener,noreferrer');
}
Expand Down
Loading