diff --git a/pr-all-unpushed-branches.sh b/pr-all-unpushed-branches.sh new file mode 100755 index 00000000..02468680 --- /dev/null +++ b/pr-all-unpushed-branches.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# first, create a list of all local branches +LOCAL_BRANCHES=$(git for-each-ref --format='%(refname:short)' refs/heads/) + +# remove all branches that have a remote tracking branch +LOCAL_BRANCHES=$(echo "$LOCAL_BRANCHES" | while read BRANCH; do + if [ -z "$(git for-each-ref --format='%(upstream:short)' refs/heads/$BRANCH)" ]; then + echo "$BRANCH" + fi +done) + +# then, remove branches that have been pushed to the remote +# shellcheck disable=SC2001 +LOCAL_BRANCHES_FORMATTED=$(echo "$LOCAL_BRANCHES" | sed 's|^|refs/heads/|') + +REMOTE_BRANCHES=$(git ls-remote origin --heads "$LOCAL_BRANCHES_FORMATTED" | awk '{print $2}' | sed 's|refs/heads/||') +for BRANCH in $REMOTE_BRANCHES; do + LOCAL_BRANCHES=$(echo "$LOCAL_BRANCHES" | grep -v "^$BRANCH$") +done + +# now, LOCAL_BRANCHES contains only branches that have not been pushed to the remote +# for each of these branches, create a pull request using the GitHub CLI +for BRANCH in $LOCAL_BRANCHES; do + # echo "Creating pull request for branch: $BRANCH" + # push the branch to the remote + if ! git push -u origin "$BRANCH"; then + echo "Failed to push branch: $BRANCH" + continue + fi + + PR_EXISTS=$(gh pr list --head "$BRANCH" --json number --jq '.[].number') + if [ -z "$PR_EXISTS" ]; then + echo "Creating pull request for branch: $BRANCH" + if ! gh pr create -B develop -d -H "$BRANCH" -f -a "@me"; then + echo "Failed to create pull request for branch: $BRANCH" + continue + else + echo "Pull request created for branch: $BRANCH" + fi + else + echo "Pull request already exists for branch: $BRANCH (PR #$PR_EXISTS)" + fi +done diff --git a/src/components/modals/ChangeBooleanValueModal.tsx b/src/components/modals/ChangeBooleanValueModal.tsx index b4077606..4e83a5bf 100644 --- a/src/components/modals/ChangeBooleanValueModal.tsx +++ b/src/components/modals/ChangeBooleanValueModal.tsx @@ -35,6 +35,7 @@ const ChangeBooleanValueModal: FC = ({ const [wasModified, setWasModified] = useState(false); const handleSave = () => { + setWasModified(false); onSave?.(value); onClose?.(); }; diff --git a/src/components/modals/ChangeEnumValueModal.tsx b/src/components/modals/ChangeEnumValueModal.tsx index 7649f358..23118cb7 100644 --- a/src/components/modals/ChangeEnumValueModal.tsx +++ b/src/components/modals/ChangeEnumValueModal.tsx @@ -44,6 +44,7 @@ const ChangeEnumValueModal: FC = ({ const [wasModified, setWasModified] = useState(false); const handleSave = () => { + setWasModified(false); onSave?.(value); onClose?.(); }; @@ -98,7 +99,6 @@ const ChangeEnumValueModal: FC = ({ {possibleValues.map(({ label, value }) => ( = ({ return; } + setWasModified(false); onSave?.(value); onClose?.(); }; diff --git a/src/components/modals/ConfirmUnsavedDataModal.tsx b/src/components/modals/ConfirmUnsavedDataModal.tsx new file mode 100644 index 00000000..ad9af460 --- /dev/null +++ b/src/components/modals/ConfirmUnsavedDataModal.tsx @@ -0,0 +1,63 @@ +import type { FC } from 'react'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box } from 'react-native-flex-layout'; +import type { ModalProps } from 'react-native-paper'; +import { Button, Portal, Text, useTheme } from 'react-native-paper'; + +import BaseModal from '@/components/BaseModal'; + +export type ConfirmUnsavedDataModalInput = false | (() => void); + +export interface ConfirmUnsavedDataModalProps + extends Omit { + visible: ConfirmUnsavedDataModalInput; +} + +const ConfirmUnsavedDataModal: FC = props => { + const { onDismiss, visible } = props; + const { t } = useTranslation(); + const theme = useTheme(); + + const handleAbort = useCallback(() => { + onDismiss?.(); + }, [onDismiss]); + + return ( + + + + + {t('unsavedDataTips')} + + + + + + + + + ); +}; + +export default ConfirmUnsavedDataModal; diff --git a/src/components/styled/SettingsSurface.tsx b/src/components/styled/SettingsSurface.tsx index 3792add9..f9215097 100644 --- a/src/components/styled/SettingsSurface.tsx +++ b/src/components/styled/SettingsSurface.tsx @@ -4,7 +4,7 @@ import { Surface, useTheme } from 'react-native-paper'; import styled from 'styled-components'; -const settingsSurfaceBorderRadiusFactor = 3; +const settingsSurfaceBorderRadiusFactor = 8; export const settingsSurfaceRoundness = (theme: ThemeBase) => { return theme.roundness! * settingsSurfaceBorderRadiusFactor; @@ -12,6 +12,7 @@ export const settingsSurfaceRoundness = (theme: ThemeBase) => { const InternalSettingsSurface = styled(Surface)` margin: 4px 16px 12px; + padding: 0 4px; border-radius: ${props => (props.theme.roundness ?? 0) * settingsSurfaceBorderRadiusFactor}px; `; diff --git a/src/translations b/src/translations index d563c50e..00e26054 160000 --- a/src/translations +++ b/src/translations @@ -1 +1 @@ -Subproject commit d563c50e877819c1b33c134315a0cecfc0d2e4c6 +Subproject commit 00e2605463560bc2ef011de8520bf104a3814083 diff --git a/src/views/navigation/screens/SettingsGroup/DtuSettingsScreen.tsx b/src/views/navigation/screens/SettingsGroup/DtuSettingsScreen.tsx index ba2fc60c..d0489982 100644 --- a/src/views/navigation/screens/SettingsGroup/DtuSettingsScreen.tsx +++ b/src/views/navigation/screens/SettingsGroup/DtuSettingsScreen.tsx @@ -13,6 +13,8 @@ import { NRFPaLevel } from '@/types/opendtu/settings'; import ChangeEnumValueModal from '@/components/modals/ChangeEnumValueModal'; import ChangeTextValueModal from '@/components/modals/ChangeTextValueModal'; +import type { ConfirmUnsavedDataModalInput } from '@/components/modals/ConfirmUnsavedDataModal'; +import ConfirmUnsavedDataModal from '@/components/modals/ConfirmUnsavedDataModal'; import SettingsSurface from '@/components/styled/SettingsSurface'; import useDtuSettings from '@/hooks/useDtuSettings'; @@ -66,11 +68,28 @@ const DtuSettingsScreen: FC = ({ navigation }) => { const [isRefreshing, setIsRefreshing] = useState(false); const [isSaving, setIsSaving] = useState(false); - const handleGetDtuSettings = useCallback(async () => { - setIsRefreshing(true); - await openDtuApi.getDtuConfig(); - setIsRefreshing(false); - }, [openDtuApi]); + const hasChanges = useMemo(() => { + return !deepEqual(initialDtuSettings, dtuSettings); + }, [initialDtuSettings, dtuSettings]); + + const [confirmRefreshDataModalOpen, setConfirmRefreshDataModalOpen] = + useState(false); + + const performRefresh = useCallback( + async (forceRefresh: boolean = false) => { + if (hasChanges && !forceRefresh) { + setConfirmRefreshDataModalOpen(() => () => { + performRefresh(true); + }); + return; + } + + setIsRefreshing(true); + await openDtuApi.getDtuConfig(); + setIsRefreshing(false); + }, + [hasChanges, openDtuApi], + ); const handleSave = useCallback(async () => { if (!dtuSettings) { @@ -95,13 +114,11 @@ const DtuSettingsScreen: FC = ({ navigation }) => { useEffect(() => { if (navigation.isFocused()) { - handleGetDtuSettings(); + performRefresh(); } - }, [handleGetDtuSettings, navigation]); - - const hasChanges = useMemo(() => { - return !deepEqual(initialDtuSettings, dtuSettings); - }, [initialDtuSettings, dtuSettings]); + // we do not want to include performRefresh here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigation]); const [changeSerialModalOpen, setChangeSerialModalOpen] = useState(false); const [changePollIntervalModalOpen, setChangePollIntervalModalOpen] = @@ -148,7 +165,17 @@ const DtuSettingsScreen: FC = ({ navigation }) => { return ( <> - navigation.goBack()} /> + { + if (hasChanges) { + setConfirmRefreshDataModalOpen(() => () => { + navigation.goBack(); + }); + return; + } + navigation.goBack(); + }} + /> {isSaving || hasChanges ? ( = ({ navigation }) => { refreshControl={ = ({ navigation }) => { return true; }} /> + { + setConfirmRefreshDataModalOpen(false); + }} + /> ); }; diff --git a/src/views/navigation/screens/SettingsGroup/NTPSettingsScreen.tsx b/src/views/navigation/screens/SettingsGroup/NTPSettingsScreen.tsx index 5a0566b2..f6e0a32d 100644 --- a/src/views/navigation/screens/SettingsGroup/NTPSettingsScreen.tsx +++ b/src/views/navigation/screens/SettingsGroup/NTPSettingsScreen.tsx @@ -13,6 +13,8 @@ import { SunsetType } from '@/types/opendtu/settings'; import ChangeEnumValueModal from '@/components/modals/ChangeEnumValueModal'; import ChangeTextValueModal from '@/components/modals/ChangeTextValueModal'; +import type { ConfirmUnsavedDataModalInput } from '@/components/modals/ConfirmUnsavedDataModal'; +import ConfirmUnsavedDataModal from '@/components/modals/ConfirmUnsavedDataModal'; import NTPChangeTimezoneModal from '@/components/modals/NTPChangeTimezoneModal'; import NTPCurrentTimeComponents from '@/components/settings/NTPCurrentTimeComponents'; import SettingsSurface from '@/components/styled/SettingsSurface'; @@ -43,36 +45,53 @@ const NTPSettingsScreen: FC = ({ navigation }) => { const [isRefreshing, setIsRefreshing] = useState(false); const [isSaving, setIsSaving] = useState(false); + const hasChanges = useMemo(() => { + return !deepEqual(initialTimeSettings, timeSettings); + }, [initialTimeSettings, timeSettings]); + const [currentOpendtuTime, setCurrentOpendtuTime] = useState< Date | undefined >(undefined); - const handleGetNTPSettings = useCallback(async () => { - setIsRefreshing(true); + const [confirmRefreshDataModalOpen, setConfirmRefreshDataModalOpen] = + useState(false); - try { - await openDtuApi.getNTPConfig(); + const performRefresh = useCallback( + async (forceRefresh: boolean = false) => { + if (hasChanges && !forceRefresh) { + setConfirmRefreshDataModalOpen(() => () => { + performRefresh(true); + }); + return; + } + + setIsRefreshing(true); + + try { + await openDtuApi.getNTPConfig(); - const timeData = await openDtuApi.getNTPTime(); + const timeData = await openDtuApi.getNTPTime(); - if (timeData) { - const date = new Date( - timeData.year, - timeData.month - 1, - timeData.day, - timeData.hour, - timeData.minute, - timeData.second, - ); + if (timeData) { + const date = new Date( + timeData.year, + timeData.month - 1, + timeData.day, + timeData.hour, + timeData.minute, + timeData.second, + ); - setCurrentOpendtuTime(date); + setCurrentOpendtuTime(date); + } + } catch (error) { + log.error('Error fetching NTP settings', error); + } finally { + setIsRefreshing(false); } - } catch (error) { - log.error('Error fetching NTP settings', error); - } finally { - setIsRefreshing(false); - } - }, [openDtuApi]); + }, + [hasChanges, openDtuApi], + ); const handleSave = useCallback(async () => { if (!timeSettings) { @@ -97,13 +116,11 @@ const NTPSettingsScreen: FC = ({ navigation }) => { useEffect(() => { if (navigation.isFocused()) { - handleGetNTPSettings(); + performRefresh(); } - }, [handleGetNTPSettings, navigation]); - - const hasChanges = useMemo(() => { - return !deepEqual(initialTimeSettings, timeSettings); - }, [initialTimeSettings, timeSettings]); + // we do not want to include performRefresh here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigation]); const [synchronizeTimeLoading, setSynchronizeTimeLoading] = useState(false); @@ -158,7 +175,17 @@ const NTPSettingsScreen: FC = ({ navigation }) => { return ( <> - navigation.goBack()} /> + { + if (hasChanges) { + setConfirmRefreshDataModalOpen(() => () => { + navigation.goBack(); + }); + return; + } + navigation.goBack(); + }} + /> {isSaving || hasChanges ? ( = ({ navigation }) => { refreshControl={ = ({ navigation }) => { timeSettings={timeSettings} setTimeSettings={setTimeSettings} /> + { + setConfirmRefreshDataModalOpen(false); + }} + /> ); }; diff --git a/src/views/navigation/screens/SettingsGroup/NetworkSettingsScreen.tsx b/src/views/navigation/screens/SettingsGroup/NetworkSettingsScreen.tsx index e5ea2043..31b7e2a4 100644 --- a/src/views/navigation/screens/SettingsGroup/NetworkSettingsScreen.tsx +++ b/src/views/navigation/screens/SettingsGroup/NetworkSettingsScreen.tsx @@ -12,6 +12,8 @@ import type { NetworkSettings } from '@/types/opendtu/settings'; import ChangeBooleanValueModal from '@/components/modals/ChangeBooleanValueModal'; import ChangeTextValueModal from '@/components/modals/ChangeTextValueModal'; +import type { ConfirmUnsavedDataModalInput } from '@/components/modals/ConfirmUnsavedDataModal'; +import ConfirmUnsavedDataModal from '@/components/modals/ConfirmUnsavedDataModal'; import SettingsSurface from '@/components/styled/SettingsSurface'; import useDtuSettings from '@/hooks/useDtuSettings'; @@ -38,11 +40,28 @@ const NetworkSettingsScreen: FC = ({ navigation }) => { const [isRefreshing, setIsRefreshing] = useState(false); const [isSaving, setIsSaving] = useState(false); - const handleGetNetworkSettings = useCallback(async () => { - setIsRefreshing(true); - await openDtuApi.getNetworkConfig(); - setIsRefreshing(false); - }, [openDtuApi]); + const hasChanges = useMemo(() => { + return !deepEqual(initialNetworkSettings, networkSettings); + }, [initialNetworkSettings, networkSettings]); + + const [confirmRefreshDataModalOpen, setConfirmRefreshDataModalOpen] = + useState(false); + + const performRefresh = useCallback( + async (forceRefresh: boolean = false) => { + if (hasChanges && !forceRefresh) { + setConfirmRefreshDataModalOpen(() => () => { + performRefresh(true); + }); + return; + } + + setIsRefreshing(true); + await openDtuApi.getNetworkConfig(); + setIsRefreshing(false); + }, + [hasChanges, openDtuApi], + ); const handleSave = useCallback(async () => { if (!networkSettings) { @@ -67,13 +86,11 @@ const NetworkSettingsScreen: FC = ({ navigation }) => { useEffect(() => { if (navigation.isFocused()) { - handleGetNetworkSettings(); + performRefresh(); } - }, [handleGetNetworkSettings, navigation]); - - const hasChanges = useMemo(() => { - return !deepEqual(initialNetworkSettings, networkSettings); - }, [initialNetworkSettings, networkSettings]); + // we do not want to include performRefresh here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [navigation]); const [changeSsidModalOpen, setChangeSsidModalOpen] = useState(false); @@ -111,7 +128,17 @@ const NetworkSettingsScreen: FC = ({ navigation }) => { return ( <> - navigation.goBack()} /> + { + if (hasChanges) { + setConfirmRefreshDataModalOpen(() => () => { + navigation.goBack(); + }); + return; + } + navigation.goBack(); + }} + /> {isSaving || hasChanges ? ( = ({ navigation }) => { refreshControl={ = ({ navigation }) => { title={t('settings.networkSettings.changeApTimeout.title')} description={t('settings.networkSettings.changeApTimeout.description')} /> + { + setConfirmRefreshDataModalOpen(false); + }} + /> ); };