summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2024-01-05 16:51:24 +0100
committerOskar Nyberg <oskar@mullvad.net>2024-01-05 16:51:24 +0100
commit70eee394031224b0d6f303b7d53ff23b7b079ebe (patch)
tree96ee7c34ee76592b704662a14e43dd061804ff0e /gui/src
parent1b97311392a0a5b7f52c813c17ef69740054ffe3 (diff)
parent2bebcce89bab9b4de95ed2cfa3c496059602ea3b (diff)
downloadmullvadvpn-70eee394031224b0d6f303b7d53ff23b7b079ebe.tar.xz
mullvadvpn-70eee394031224b0d6f303b7d53ff23b7b079ebe.zip
Merge branch 'prevent-empty-custom-lists-names-des-530'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/select-location/CustomListDialogs.tsx44
-rw-r--r--gui/src/renderer/components/select-location/CustomLists.tsx83
-rw-r--r--gui/src/renderer/components/select-location/LocationRow.tsx28
3 files changed, 112 insertions, 43 deletions
diff --git a/gui/src/renderer/components/select-location/CustomListDialogs.tsx b/gui/src/renderer/components/select-location/CustomListDialogs.tsx
index 7af8c1b622..56244e73eb 100644
--- a/gui/src/renderer/components/select-location/CustomListDialogs.tsx
+++ b/gui/src/renderer/components/select-location/CustomListDialogs.tsx
@@ -18,7 +18,7 @@ import { useSelector } from '../../redux/store';
import * as AppButton from '../AppButton';
import * as Cell from '../cell';
import { normalText, tinyText } from '../common-styles';
-import { ModalAlert, ModalMessage } from '../Modal';
+import { ModalAlert, ModalAlertType, ModalMessage } from '../Modal';
import SimpleInput from '../SimpleInput';
const StyledModalMessage = styled(ModalMessage)({
@@ -201,3 +201,45 @@ export function EditListDialog(props: EditListProps) {
</ModalAlert>
);
}
+
+interface DeleteConfirmDialogProps {
+ list: ICustomList;
+ isOpen: boolean;
+ hide: () => void;
+ confirm: () => void;
+}
+
+// Dialog for changing the name of a custom list.
+export function DeleteConfirmDialog(props: DeleteConfirmDialogProps) {
+ const confirm = useCallback(() => {
+ props.confirm();
+ props.hide();
+ }, []);
+
+ return (
+ <ModalAlert
+ type={ModalAlertType.warning}
+ isOpen={props.isOpen}
+ buttons={[
+ <AppButton.RedButton key="save" onClick={confirm}>
+ {messages.gettext('Delete list')}
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="cancel" onClick={props.hide}>
+ {messages.gettext('Cancel')}
+ </AppButton.BlueButton>,
+ ]}
+ close={props.hide}>
+ <ModalMessage>
+ {formatHtml(
+ sprintf(
+ messages.pgettext(
+ 'select-location-view',
+ 'Do you want to delete the list <b>%(list)s</b>?',
+ ),
+ { list: props.list.name },
+ ),
+ )}
+ </ModalMessage>
+ </ModalAlert>
+ );
+}
diff --git a/gui/src/renderer/components/select-location/CustomLists.tsx b/gui/src/renderer/components/select-location/CustomLists.tsx
index 7fb66f630e..737d66bf89 100644
--- a/gui/src/renderer/components/select-location/CustomLists.tsx
+++ b/gui/src/renderer/components/select-location/CustomLists.tsx
@@ -10,6 +10,7 @@ import { useBoolean, useStyledRef } from '../../lib/utilityHooks';
import Accordion from '../Accordion';
import * as Cell from '../cell';
import { measurements } from '../common-styles';
+import { BackAction } from '../KeyboardNavigation';
import SimpleInput from '../SimpleInput';
import { StyledLocationRowIcon } from './LocationRow';
import { useRelayListContext } from './RelayListContext';
@@ -52,7 +53,11 @@ const StyledAddListCellButton = styled(StyledCellButton)({
const StyledSideButtonIcon = styled(Cell.Icon)({
padding: '3px',
- [`${StyledCellButton}:hover &&, ${StyledAddListCellButton}:hover &&`]: {
+ [`${StyledCellButton}:disabled &&, ${StyledAddListCellButton}:disabled &&`]: {
+ backgroundColor: colors.white40,
+ },
+
+ [`${StyledCellButton}:not(:disabled):hover &&, ${StyledAddListCellButton}:not(:disabled):hover &&`]: {
backgroundColor: colors.white,
},
});
@@ -117,6 +122,7 @@ interface AddListFormProps {
function AddListForm(props: AddListFormProps) {
const [name, setName] = useState('');
+ const nameValid = name.trim() !== '';
const [error, setError, unsetError] = useBoolean();
const containerRef = useStyledRef<HTMLDivElement>();
const inputRef = useStyledRef<HTMLInputElement>();
@@ -128,16 +134,18 @@ function AddListForm(props: AddListFormProps) {
}, []);
const createList = useCallback(async () => {
- try {
- const result = await props.onCreateList(name);
- if (result) {
- setError();
+ if (nameValid) {
+ try {
+ const result = await props.onCreateList(name);
+ if (result) {
+ setError();
+ }
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to create list:', error.message);
}
- } catch (e) {
- const error = e as Error;
- log.error('Failed to create list:', error.message);
}
- }, [name, props.onCreateList]);
+ }, [name, props.onCreateList, nameValid]);
const onBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
@@ -162,34 +170,37 @@ function AddListForm(props: AddListFormProps) {
}, [props.visible]);
return (
- <Accordion expanded={props.visible} onTransitionEnd={onTransitionEnd}>
- <StyledCellContainer ref={containerRef}>
- <StyledInputContainer>
- <StyledInput
- ref={inputRef}
- value={name}
- onChangeValue={onChange}
- onSubmitValue={createList}
- onBlur={onBlur}
- maxLength={30}
- $error={error}
- autoFocus
- />
- </StyledInputContainer>
+ <BackAction disabled={!props.visible} action={props.cancel}>
+ <Accordion expanded={props.visible} onTransitionEnd={onTransitionEnd}>
+ <StyledCellContainer ref={containerRef}>
+ <StyledInputContainer>
+ <StyledInput
+ ref={inputRef}
+ value={name}
+ onChangeValue={onChange}
+ onSubmitValue={createList}
+ onBlur={onBlur}
+ maxLength={30}
+ $error={error}
+ autoFocus
+ />
+ </StyledInputContainer>
- <StyledAddListCellButton
- $backgroundColor={colors.blue}
- $backgroundColorHover={colors.blue80}
- onClick={createList}>
- <StyledSideButtonIcon source="icon-check" tintColor={colors.white60} width={18} />
- </StyledAddListCellButton>
- </StyledCellContainer>
- <Cell.CellFooter>
- <Cell.CellFooterText>
- {messages.pgettext('select-location-view', 'List names must be unique.')}
- </Cell.CellFooterText>
- </Cell.CellFooter>
- </Accordion>
+ <StyledAddListCellButton
+ $backgroundColor={colors.blue}
+ $backgroundColorHover={colors.blue80}
+ disabled={!nameValid}
+ onClick={createList}>
+ <StyledSideButtonIcon source="icon-check" tintColor={colors.white60} width={18} />
+ </StyledAddListCellButton>
+ </StyledCellContainer>
+ <Cell.CellFooter>
+ <Cell.CellFooterText>
+ {messages.pgettext('select-location-view', 'List names must be unique.')}
+ </Cell.CellFooterText>
+ </Cell.CellFooter>
+ </Accordion>
+ </BackAction>
);
}
diff --git a/gui/src/renderer/components/select-location/LocationRow.tsx b/gui/src/renderer/components/select-location/LocationRow.tsx
index 97c8528fa9..79b44d372a 100644
--- a/gui/src/renderer/components/select-location/LocationRow.tsx
+++ b/gui/src/renderer/components/select-location/LocationRow.tsx
@@ -19,7 +19,7 @@ import ChevronButton from '../ChevronButton';
import { measurements, normalText } from '../common-styles';
import ImageView from '../ImageView';
import RelayStatusIndicator from '../RelayStatusIndicator';
-import { AddToListDialog, EditListDialog } from './CustomListDialogs';
+import { AddToListDialog, DeleteConfirmDialog, EditListDialog } from './CustomListDialogs';
import {
CitySpecification,
CountrySpecification,
@@ -105,6 +105,10 @@ const StyledHoverIconButton = styled.button<IButtonColorProps & { $isLast?: bool
height: measurements.rowMinHeight,
appearance: 'none',
+ '&&:last-child': {
+ paddingRight: '25px',
+ },
+
'&&:not(:disabled):hover': {
backgroundColor: props.$backgroundColor,
},
@@ -158,6 +162,7 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
const { updateCustomList, deleteCustomList } = useAppContext();
const [addToListDialogVisible, showAddToListDialog, hideAddToListDialog] = useBoolean();
const [editDialogVisible, showEditDialog, hideEditDialog] = useBoolean();
+ const [deleteDialogVisible, showDeleteDialog, hideDeleteDialog] = useBoolean();
const background = getButtonColor(props.source.selected, props.level, props.source.disabled);
const customLists = useSelector((state) => state.settings.customLists);
@@ -165,12 +170,12 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
// Expand/collapse should only be available if the expanded property is provided in the source
const expanded = 'expanded' in props.source ? props.source.expanded : undefined;
const toggleCollapse = useCallback(() => {
- if (expanded !== undefined) {
+ if (expanded !== undefined && hasChildren) {
userInvokedExpand.current = true;
const callback = expanded ? props.onCollapse : props.onExpand;
callback(props.source.location);
}
- }, [props.onExpand, props.onCollapse, props.source.location, expanded]);
+ }, [props.onExpand, props.onCollapse, props.source.location, expanded, hasChildren]);
const handleClick = useCallback(() => {
if (!props.source.selected) {
@@ -214,7 +219,7 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
}, [customLists, props.source.location]);
// Remove an entire custom list.
- const onRemoveCustomList = useCallback(async () => {
+ const confirmRemoveCustomList = useCallback(async () => {
if (props.source.location.customList) {
try {
await deleteCustomList(props.source.location.customList);
@@ -269,16 +274,18 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
<StyledHoverIconButton onClick={showEditDialog} {...background}>
<StyledHoverIcon source="icon-edit" />
</StyledHoverIconButton>
- <StyledHoverIconButton onClick={onRemoveCustomList} $isLast {...background}>
+ <StyledHoverIconButton onClick={showDeleteDialog} $isLast {...background}>
<StyledHoverIcon source="icon-close" />
</StyledHoverIconButton>
</>
) : null}
- {hasChildren ? (
+ {hasChildren ||
+ ('customList' in props.source.location && !('country' in props.source.location)) ? (
<StyledLocationRowIcon
as={ChevronButton}
onClick={toggleCollapse}
+ disabled={!hasChildren}
up={expanded ?? false}
aria-label={sprintf(
expanded === true
@@ -312,6 +319,15 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
{'list' in props.source && (
<EditListDialog list={props.source.list} isOpen={editDialogVisible} hide={hideEditDialog} />
)}
+
+ {'list' in props.source && (
+ <DeleteConfirmDialog
+ list={props.source.list}
+ isOpen={deleteDialogVisible}
+ hide={hideDeleteDialog}
+ confirm={confirmRemoveCustomList}
+ />
+ )}
</>
);
}