Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 7 additions & 5 deletions .github/workflows/beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
paths-ignore:
- '.github/workflows/**'
- '**/*.md'
- '*.md'
- '.gitignore'

permissions:
contents: write
Expand Down Expand Up @@ -72,11 +74,11 @@ jobs:
run: npm run build:${{ matrix.target }}
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
FEEDBACK_FORM_URL: ${{ secrets.FEEDBACK_FORM_URL }}
FEEDBACK_FORM_ENTRY_EMAIL: ${{ secrets.FEEDBACK_FORM_ENTRY_EMAIL }}
FEEDBACK_FORM_ENTRY_CATEGORY: ${{ secrets.FEEDBACK_FORM_ENTRY_CATEGORY }}
FEEDBACK_FORM_ENTRY_SUBJECT: ${{ secrets.FEEDBACK_FORM_ENTRY_SUBJECT }}
FEEDBACK_FORM_ENTRY_DETAILS: ${{ secrets.FEEDBACK_FORM_ENTRY_DETAILS }}
FEEDBACK_FORM_URL: ${{ vars.FEEDBACK_FORM_URL }}
FEEDBACK_FORM_ENTRY_EMAIL: ${{ vars.FEEDBACK_FORM_ENTRY_EMAIL }}
FEEDBACK_FORM_ENTRY_CATEGORY: ${{ vars.FEEDBACK_FORM_ENTRY_CATEGORY }}
FEEDBACK_FORM_ENTRY_SUBJECT: ${{ vars.FEEDBACK_FORM_ENTRY_SUBJECT }}
FEEDBACK_FORM_ENTRY_DETAILS: ${{ vars.FEEDBACK_FORM_ENTRY_DETAILS }}
Comment on lines +77 to +81
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This switches FEEDBACK_FORM_* from secrets to vars. If any of these values are sensitive (tokens, private endpoints, etc.), GitHub Variables are not the right storage mechanism compared to Secrets. If they’re truly non-sensitive configuration, add a short comment in the workflow clarifying that these are safe to be public-as-config; otherwise revert to secrets.*.

Copilot uses AI. Check for mistakes.

- name: Upload beta artifacts
uses: actions/upload-artifact@v4
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
paths-ignore:
- '.github/workflows/**'
- '**/*.md'
- '*.md'
- '.gitignore'

jobs:
build:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
paths-ignore:
- '.github/workflows/**'
- '**/*.md'
- '*.md'
- '.gitignore'

permissions:
contents: write
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ on:
paths-ignore:
- '.github/workflows/**'
- '**/*.md'
- '*.md'
- '.gitignore'

jobs:
test:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/validate-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
paths-ignore:
- '.github/workflows/**'
- '**/*.md'
- '*.md'
- '.gitignore'

jobs:
check-version:
Expand Down
84 changes: 56 additions & 28 deletions src/components/PlanDashboard/PlanDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import SettingsModal from '../modals/SettingsModal';
import AccountsModal from '../modals/AccountsModal';
import ExportModal from '../modals/ExportModal';
import FeedbackModal from '../modals/FeedbackModal';
import ViewModeSettingsModal from '../modals/ViewModeSettingsModal';
import { PlanTabs, TabManagementModal } from './PlanTabs';
import PlanSearchOverlay from './PlanSearchOverlay';
import PlanHistoryOverlay from './PlanHistoryOverlay';
import { Toast, Modal, Button, ConfirmDialog, ErrorDialog, FileRelinkModal, FormGroup, EncryptionConfigPanel, Dropdown, ViewModeSelector } from '../_shared';
import { initializeTabConfigs, getVisibleTabs, getHiddenTabs, toggleTabVisibility, reorderTabs, normalizeLegacyTabId } from '../../utils/tabManagement';
import { getPayFrequencyViewMode } from '../../utils/payPeriod';
import { sanitizeFavoriteViewModes } from '../../utils/viewModePreferences';
import type { SelectableViewMode } from '../../types/viewMode';
import { useGlobalKeyboardShortcuts } from '../../hooks';
import type { SearchResult } from '../../utils/planSearch';
import { getActionHandler, type SearchActionContext } from '../../utils/searchRegistry';
Expand Down Expand Up @@ -95,9 +97,8 @@ const isElementVisibleInContainer = (element: HTMLElement, container: HTMLElemen
);
};

const getFirstVisibleFavoriteMode = (): ViewMode => {
const favorites = sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites);
return favorites[0];
const getFirstVisibleFavoriteMode = (favorites: SelectableViewMode[]): ViewMode => {
return favorites[0] as ViewMode;
};

const isEditableTarget = (target: EventTarget | null): boolean => {
Expand Down Expand Up @@ -148,24 +149,24 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
const [shouldScrollToRetirement, setShouldScrollToRetirement] = useState(false);
const [pendingTabScroll, setPendingTabScroll] = useState<{ tab: TabId; position: TabScrollPosition } | null>(null);
const [displayMode, setDisplayMode] = useState<ViewMode>(() => {
const favorites = sanitizeFavoriteViewModes(budgetData?.settings?.viewModeFavorites);
if (budgetData?.settings?.displayMode) {
return sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites).includes(
budgetData.settings.displayMode as never,
)
return favorites.includes(budgetData.settings.displayMode as never)
? budgetData.settings.displayMode
: getFirstVisibleFavoriteMode();
: getFirstVisibleFavoriteMode(favorites);
}

const cadenceMode = getPayFrequencyViewMode(budgetData?.paySettings?.payFrequency ?? 'bi-weekly');
return sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites).includes(cadenceMode as never)
return favorites.includes(cadenceMode as never)
? cadenceMode
: getFirstVisibleFavoriteMode();
: getFirstVisibleFavoriteMode(favorites);
});
const [showCopyModal, setShowCopyModal] = useState(false);
const [newYear, setNewYear] = useState('');
const [copyYearError, setCopyYearError] = useState<string | null>(null);
const [showSettings, setShowSettings] = useState(false);
const [settingsInitialSection, setSettingsInitialSection] = useState<string | undefined>(undefined);
const [showViewModeSettings, setShowViewModeSettings] = useState(false);
const [pendingPaySettingsFieldHighlight, setPendingPaySettingsFieldHighlight] = useState<string | undefined>(undefined);
const [paySettingsSearchRequestKey, setPaySettingsSearchRequestKey] = useState(0);
const [pendingBillsSearchAction, setPendingBillsSearchAction] = useState<
Expand Down Expand Up @@ -342,7 +343,6 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o

const normalizedViewMode = normalizeLegacyTabId(viewMode);
if (normalizedViewMode && VALID_TABS.includes(normalizedViewMode)) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setActiveTab(normalizedViewMode);
initializedTabContextRef.current = tabRestoreContext;
return;
Expand Down Expand Up @@ -433,6 +433,21 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
historyStateKeyRef.current = nextHistoryKey;
}, [activeTab, budgetData?.id, budgetData?.settings?.filePath, viewMode]);

useEffect(() => {
const handleViewModeAutoSwitched = (event: Event) => {
const customEvent = event as CustomEvent<{ message?: string }>;
setStatusToast({
message: customEvent.detail?.message ?? 'View mode switched to match your pay frequency.',
type: 'success',
});
};

window.addEventListener(APP_CUSTOM_EVENTS.viewModeAutoSwitched, handleViewModeAutoSwitched as EventListener);
return () => {
window.removeEventListener(APP_CUSTOM_EVENTS.viewModeAutoSwitched, handleViewModeAutoSwitched as EventListener);
};
}, []);

useEffect(() => {
if (viewMode || typeof window === 'undefined') return;

Expand All @@ -456,44 +471,39 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
// Initialize tab position and display mode from budget settings
useEffect(() => {
if (budgetData?.settings?.tabPosition) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setTabPosition(budgetData.settings.tabPosition);
}
if (budgetData?.settings?.tabDisplayMode) {
setTabDisplayMode(budgetData.settings.tabDisplayMode);
}
if (budgetData?.settings?.displayMode) {
const favorites = sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites);
const favorites = sanitizeFavoriteViewModes(budgetData.settings.viewModeFavorites);
setDisplayMode(
favorites.includes(budgetData.settings.displayMode as never)
? budgetData.settings.displayMode
: favorites[0],
);
} else if (budgetData?.paySettings?.payFrequency) {
const favorites = sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites);
const favorites = sanitizeFavoriteViewModes(budgetData.settings?.viewModeFavorites);
const cadenceMode = getPayFrequencyViewMode(budgetData.paySettings.payFrequency);
setDisplayMode(favorites.includes(cadenceMode as never) ? cadenceMode : favorites[0]);
}
}, [
budgetData?.settings?.tabDisplayMode,
budgetData?.settings?.tabPosition,
budgetData?.settings?.displayMode,
budgetData?.settings?.viewModeFavorites,
budgetData?.paySettings?.payFrequency,
]);

// When plan-specific favorites change, ensure displayMode is still in the list
useEffect(() => {
const handleViewModeFavoritesChanged = () => {
const favorites = sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites);
if (!favorites.includes(displayMode as never)) {
setDisplayMode(favorites[0]);
}
};

window.addEventListener(APP_CUSTOM_EVENTS.viewModeFavoritesChanged, handleViewModeFavoritesChanged);
return () => {
window.removeEventListener(APP_CUSTOM_EVENTS.viewModeFavoritesChanged, handleViewModeFavoritesChanged);
};
}, [displayMode]);
const favorites = sanitizeFavoriteViewModes(budgetData?.settings?.viewModeFavorites);
if (!favorites.includes(displayMode as never)) {
setDisplayMode(favorites[0]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [budgetData?.settings?.viewModeFavorites]);

// Handle tab position changes
const handleTabPositionChange = useCallback((newPosition: TabPosition) => {
Expand Down Expand Up @@ -870,7 +880,6 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
planLoadingStartRef.current = Date.now();
}

// eslint-disable-next-line react-hooks/set-state-in-effect
setShowPlanLoadingScreen(true);
return;
}
Expand Down Expand Up @@ -1165,16 +1174,27 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
} else if (action.type === 'open-settings') {
setSettingsInitialSection(action.sectionId);
setShowSettings(true);
} else if (action.type === 'open-view-mode-settings') {
setShowViewModeSettings(true);
}
},
[openTabFromLink, selectTab, setPendingBillsSearchAction, setPendingBillsSearchTargetId, setBillsSearchRequestKey, setPendingLoansSearchAction, setPendingLoansSearchTargetId, setLoansSearchRequestKey, setPendingSavingsSearchAction, setPendingSavingsSearchTargetId, setSavingsSearchRequestKey, setTaxSearchOpenSettingsRequestKey, setShowAccountsModal, setShowSettings, setSettingsInitialSection, setScrollToAccountId, setPendingPaySettingsFieldHighlight, setPaySettingsSearchRequestKey],
);

const handleOpenViewModeSettings = useCallback(() => {
setSettingsInitialSection('app-data-reset');
setShowSettings(true);
setShowViewModeSettings(true);
}, []);

const handleViewModeFavoritesChange = useCallback((newFavorites: SelectableViewMode[]) => {
if (!budgetData) return;
updateBudgetData({
settings: {
...budgetData.settings,
viewModeFavorites: newFavorites,
},
});
}, [budgetData, updateBudgetData]);

const handleOpenObjectHistory = useCallback((target: AuditHistoryTarget) => {
setHistoryTarget(target);
}, []);
Expand Down Expand Up @@ -1632,6 +1652,7 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
<ViewModeSelector
mode={displayMode}
onChange={handleDisplayModeChange}
favorites={sanitizeFavoriteViewModes(budgetData.settings.viewModeFavorites)}
payCadenceMode={getPayFrequencyViewMode(budgetData.paySettings.payFrequency)}
onOpenViewModeSettings={handleOpenViewModeSettings}
disabled={activeTab === 'metrics'}
Expand Down Expand Up @@ -1949,6 +1970,13 @@ const PlanDashboard: React.FC<PlanDashboardProps> = ({ onResetSetup, viewMode, o
}}
/>

<ViewModeSettingsModal
isOpen={showViewModeSettings}
onClose={() => setShowViewModeSettings(false)}
favorites={sanitizeFavoriteViewModes(budgetData.settings.viewModeFavorites)}
onChange={handleViewModeFavoritesChange}
/>

{/* Edit Plan Metadata Modal */}
<Modal
isOpen={showPlanEditModal}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
padding: 0.25rem;
border-radius: 8px;
overflow: visible;
border: 2px solid var(--border-header-soft);
}

.view-mode-selector button {
Expand Down
34 changes: 11 additions & 23 deletions src/components/_shared/layout/ViewModeSelector/ViewModeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { Settings } from 'lucide-react';
import { APP_CUSTOM_EVENTS } from '../../../../constants/events';
import { FileStorageService } from '../../../../services/fileStorage';
import type { SelectableViewMode } from '../../../../types/viewMode';
import type { ViewMode } from '../../../../types/viewMode';
import { buildViewModeSelectorOptions, sanitizeFavoriteViewModes } from '../../../../utils/viewModePreferences';
import { getDisplayModeLabel } from '../../../../utils/payPeriod';
Expand All @@ -16,6 +15,8 @@ interface ViewModeSelectorProps<T extends string = ViewMode> {
mode: T;
onChange: (mode: T) => void;
options?: ViewModeOption<T>[];
/** Plan-specific favorites; when provided, skips internal app-settings reads. */
favorites?: SelectableViewMode[];
payCadenceMode?: T;
payCadenceLabel?: string;
onOpenViewModeSettings?: () => void;
Expand All @@ -26,29 +27,13 @@ const ViewModeSelector = <T extends string = ViewMode,>({
mode,
onChange,
options,
favorites: favoritesProp,
payCadenceMode,
payCadenceLabel = 'Your Pay Frequency',
onOpenViewModeSettings,
disabled = false,
}: ViewModeSelectorProps<T>) => {
const [favoriteModes, setFavoriteModes] = useState(() =>
sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites),
);

useEffect(() => {
if (options) return;

const handleFavoritesChanged = () => {
setFavoriteModes(
sanitizeFavoriteViewModes(FileStorageService.getAppSettings().viewModeFavorites),
);
};

window.addEventListener(APP_CUSTOM_EVENTS.viewModeFavoritesChanged, handleFavoritesChanged);
return () => {
window.removeEventListener(APP_CUSTOM_EVENTS.viewModeFavoritesChanged, handleFavoritesChanged);
};
}, [options]);
const favoriteModes = favoritesProp ?? sanitizeFavoriteViewModes(undefined);

const resolvedOptions = useMemo(() => {
if (options) {
Expand All @@ -59,7 +44,10 @@ const ViewModeSelector = <T extends string = ViewMode,>({
}, [options, favoriteModes, payCadenceMode]);

const optionsWithCadence = useMemo(() => {
if (!payCadenceMode || options) {
// Only auto-add cadence tab when the user has not explicitly configured
// favorites (favouritesProp absent). When plan-specific favorites are
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the spelling of 'favouritesProp' to 'favoritesProp' to match the actual prop name used in this component.

Suggested change
// favorites (favouritesProp absent). When plan-specific favorites are
// favorites (favoritesProp absent). When plan-specific favorites are

Copilot uses AI. Check for mistakes.
// provided, the user's checkbox selection is the authoritative list.
if (!payCadenceMode || options || favoritesProp !== undefined) {
return resolvedOptions;
}

Expand All @@ -74,7 +62,7 @@ const ViewModeSelector = <T extends string = ViewMode,>({
} as ViewModeOption<T>,
...resolvedOptions,
];
}, [payCadenceMode, options, resolvedOptions]);
}, [payCadenceMode, options, favoritesProp, resolvedOptions]);

return (
<div className="view-mode-selector-wrap">
Expand Down
29 changes: 28 additions & 1 deletion src/components/modals/PaySettingsModal/PaySettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { useBudget } from '../../../contexts/BudgetContext';
import { useAppDialogs } from '../../../hooks';
import type { BudgetData } from '../../../types/budget';
import type { PayFrequency } from '../../../types/frequencies';
import type { SelectableViewMode } from '../../../types/viewMode';
import type { PaySettings } from '../../../types/payroll';
import type { AuditHistoryTarget } from '../../../types/audit';
import { convertBudgetAmounts } from '../../../services/budgetCurrencyConversion';
import { CURRENCIES, getCurrencySymbol } from '../../../utils/currency';
import { getPaychecksPerYear } from '../../../utils/payPeriod';
import { getDisplayModeLabel, getPaychecksPerYear, getPayFrequencyViewMode } from '../../../utils/payPeriod';
import { normalizeStoredAllocationAmount } from '../../../utils/allocationEditor';
import { sanitizeFavoriteViewModes, syncFavoritesForCadence } from '../../../utils/viewModePreferences';
import { APP_CUSTOM_EVENTS } from '../../../constants/events';
import { formatSuggestedLeftover, getSuggestedLeftoverPerPaycheck } from '../../../utils/paySuggestions';
import { Modal, Button, ErrorDialog, Dropdown, FormGroup, InputWithPrefix, FormattedNumberInput, RadioGroup } from '../../_shared';
import '../../_shared/payEditorShared.css';
Expand Down Expand Up @@ -248,6 +251,30 @@ const PaySettingsModal: React.FC<PaySettingsModalProps> = ({ isOpen, onClose, se
};
}

// Sync view mode selector favorites when pay frequency changes so the new
// cadence tab appears at its canonical position in this plan's selector.
if (editPayFrequency !== budgetData.paySettings.payFrequency) {
const cadenceMode = getPayFrequencyViewMode(editPayFrequency) as SelectableViewMode;
const existingFavorites = sanitizeFavoriteViewModes(updatedBudget.settings.viewModeFavorites);
const newFavorites = syncFavoritesForCadence(existingFavorites, cadenceMode);
updatedBudget = {
...updatedBudget,
settings: {
...updatedBudget.settings,
displayMode: cadenceMode,
viewModeFavorites: newFavorites ?? existingFavorites,
},
};

window.dispatchEvent(
new CustomEvent(APP_CUSTOM_EVENTS.viewModeAutoSwitched, {
detail: {
message: `View mode switched to ${getDisplayModeLabel(cadenceMode)}`,
},
}),
);
}

updateBudgetData(updatedBudget);

setFieldErrors({});
Expand Down
Loading
Loading