mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Data loading and initialization refactor: Major performance improvement reducing initial page load time by eliminating duplicate API requests caused by race conditions between authentication flow and route rendering:
- Fixed authentication race condition where `isAuthenticated` was set before data loading completed, causing routes to render and tables to mount prematurely - Added `isInitialized` flag to delay route rendering until after all initialization data is loaded via `initData()` - Consolidated version and environment settings fetching into centralized settings store with caching to prevent redundant calls - Implemented stale fetch prevention in ChannelsTable and StreamsTable using fetch version tracking to ignore outdated responses - Fixed filter handling in tables to use `debouncedFilters` consistently, preventing unnecessary refetches - Added initialization guards using refs to prevent double-execution of auth and superuser checks during React StrictMode's intentional double-rendering in development - Removed duplicate version/environment fetch calls from Sidebar, LoginForm, and SuperuserForm by using centralized store
This commit is contained in:
parent
655760f652
commit
e22e003be9
9 changed files with 167 additions and 87 deletions
|
|
@ -49,6 +49,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Changed
|
||||
|
||||
- Data loading and initialization refactor: Major performance improvement reducing initial page load time by eliminating duplicate API requests caused by race conditions between authentication flow and route rendering:
|
||||
- Fixed authentication race condition where `isAuthenticated` was set before data loading completed, causing routes to render and tables to mount prematurely
|
||||
- Added `isInitialized` flag to delay route rendering until after all initialization data is loaded via `initData()`
|
||||
- Consolidated version and environment settings fetching into centralized settings store with caching to prevent redundant calls
|
||||
- Implemented stale fetch prevention in ChannelsTable and StreamsTable using fetch version tracking to ignore outdated responses
|
||||
- Fixed filter handling in tables to use `debouncedFilters` consistently, preventing unnecessary refetches
|
||||
- Added initialization guards using refs to prevent double-execution of auth and superuser checks during React StrictMode's intentional double-rendering in development
|
||||
- Removed duplicate version/environment fetch calls from Sidebar, LoginForm, and SuperuserForm by using centralized store
|
||||
- Table preferences (header pin and table size) now managed together with centralized state management and localStorage persistence.
|
||||
- Streams table button labels: Renamed "Remove" to "Delete" and "Add Stream to Channel" to "Add to Channel" for clarity and consistency with other UI terminology.
|
||||
- Frontend tests GitHub workflow now uses Node.js 24 (matching Dockerfile) and runs on both `main` and `dev` branch pushes and pull requests for comprehensive CI coverage.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// frontend/src/App.js
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Route,
|
||||
|
|
@ -40,18 +39,25 @@ const defaultRoute = '/channels';
|
|||
const App = () => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const isInitialized = useAuthStore((s) => s.isInitialized);
|
||||
const setIsAuthenticated = useAuthStore((s) => s.setIsAuthenticated);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const initData = useAuthStore((s) => s.initData);
|
||||
const initializeAuth = useAuthStore((s) => s.initializeAuth);
|
||||
const setSuperuserExists = useAuthStore((s) => s.setSuperuserExists);
|
||||
|
||||
const authCheckStarted = useRef(false);
|
||||
const superuserCheckStarted = useRef(false);
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
// Check if a superuser exists on first load.
|
||||
useEffect(() => {
|
||||
if (superuserCheckStarted.current) return;
|
||||
superuserCheckStarted.current = true;
|
||||
|
||||
async function checkSuperuser() {
|
||||
try {
|
||||
const response = await API.fetchSuperUser();
|
||||
|
|
@ -69,10 +75,13 @@ const App = () => {
|
|||
}
|
||||
}
|
||||
checkSuperuser();
|
||||
}, []);
|
||||
}, [setSuperuserExists]);
|
||||
|
||||
// Authentication check
|
||||
useEffect(() => {
|
||||
if (authCheckStarted.current) return;
|
||||
authCheckStarted.current = true;
|
||||
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const loggedIn = await initializeAuth();
|
||||
|
|
@ -105,14 +114,15 @@ const App = () => {
|
|||
height: 0,
|
||||
}}
|
||||
navbar={{
|
||||
width: isAuthenticated
|
||||
? open
|
||||
? drawerWidth
|
||||
: miniDrawerWidth
|
||||
: 0,
|
||||
width:
|
||||
isAuthenticated && isInitialized
|
||||
? open
|
||||
? drawerWidth
|
||||
: miniDrawerWidth
|
||||
: 0,
|
||||
}}
|
||||
>
|
||||
{isAuthenticated && (
|
||||
{isAuthenticated && isInitialized && (
|
||||
<Sidebar
|
||||
drawerWidth={drawerWidth}
|
||||
miniDrawerWidth={miniDrawerWidth}
|
||||
|
|
@ -134,7 +144,7 @@ const App = () => {
|
|||
>
|
||||
<Box sx={{ p: 2, flex: 1, overflow: 'auto' }}>
|
||||
<Routes>
|
||||
{isAuthenticated ? (
|
||||
{isAuthenticated && isInitialized ? (
|
||||
<>
|
||||
<Route path="/channels" element={<Channels />} />
|
||||
<Route path="/sources" element={<ContentSources />} />
|
||||
|
|
@ -154,7 +164,11 @@ const App = () => {
|
|||
path="*"
|
||||
element={
|
||||
<Navigate
|
||||
to={isAuthenticated ? defaultRoute : '/login'}
|
||||
to={
|
||||
isAuthenticated && isInitialized
|
||||
? defaultRoute
|
||||
: '/login'
|
||||
}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { copyToClipboard } from '../utils';
|
||||
import {
|
||||
|
|
@ -33,8 +33,7 @@ import logo from '../images/logo.png';
|
|||
import useChannelsStore from '../store/channels';
|
||||
import './sidebar.css';
|
||||
import useSettingsStore from '../store/settings';
|
||||
import useAuthStore from '../store/auth'; // Add this import
|
||||
import API from '../api';
|
||||
import useAuthStore from '../store/auth';
|
||||
import { USER_LEVELS } from '../constants';
|
||||
import UserForm from './forms/User';
|
||||
|
||||
|
|
@ -75,16 +74,13 @@ const Sidebar = ({ collapsed, toggleDrawer, drawerWidth, miniDrawerWidth }) => {
|
|||
|
||||
const channels = useChannelsStore((s) => s.channels);
|
||||
const environment = useSettingsStore((s) => s.environment);
|
||||
const appVersion = useSettingsStore((s) => s.version);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const authUser = useAuthStore((s) => s.user);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
|
||||
const publicIPRef = useRef(null);
|
||||
|
||||
const [appVersion, setAppVersion] = useState({
|
||||
version: '',
|
||||
timestamp: null,
|
||||
});
|
||||
const [userFormOpen, setUserFormOpen] = useState(false);
|
||||
|
||||
const closeUserForm = () => setUserFormOpen(false);
|
||||
|
|
@ -144,36 +140,8 @@ const Sidebar = ({ collapsed, toggleDrawer, drawerWidth, miniDrawerWidth }) => {
|
|||
},
|
||||
];
|
||||
|
||||
// Fetch environment settings including version on component mount
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchEnvironment = async () => {
|
||||
API.getEnvironmentSettings();
|
||||
};
|
||||
|
||||
fetchEnvironment();
|
||||
}, [isAuthenticated]);
|
||||
|
||||
// Fetch version information on component mount (regardless of authentication)
|
||||
useEffect(() => {
|
||||
const fetchVersion = async () => {
|
||||
try {
|
||||
const versionData = await API.getVersion();
|
||||
setAppVersion({
|
||||
version: versionData.version || '',
|
||||
timestamp: versionData.timestamp || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch version information:', error);
|
||||
// Keep using default values from useState initialization
|
||||
}
|
||||
};
|
||||
|
||||
fetchVersion();
|
||||
}, []);
|
||||
// Environment settings and version are loaded by the settings store during initData()
|
||||
// No need to fetch them again here - just use the store values
|
||||
|
||||
const copyPublicIP = async () => {
|
||||
const success = await copyToClipboard(environment.public_ip);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import useAuthStore from '../../store/auth';
|
||||
import API from '../../api';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import {
|
||||
Paper,
|
||||
Title,
|
||||
|
|
@ -25,13 +25,14 @@ const LoginForm = () => {
|
|||
const logout = useAuthStore((s) => s.logout);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const initData = useAuthStore((s) => s.initData);
|
||||
const fetchVersion = useSettingsStore((s) => s.fetchVersion);
|
||||
const storedVersion = useSettingsStore((s) => s.version);
|
||||
|
||||
const navigate = useNavigate(); // Hook to navigate to other routes
|
||||
const [formData, setFormData] = useState({ username: '', password: '' });
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [savePassword, setSavePassword] = useState(false);
|
||||
const [forgotPasswordOpened, setForgotPasswordOpened] = useState(false);
|
||||
const [version, setVersion] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Simple base64 encoding/decoding for localStorage
|
||||
|
|
@ -55,11 +56,9 @@ const LoginForm = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch version info
|
||||
API.getVersion().then((data) => {
|
||||
setVersion(data?.version);
|
||||
});
|
||||
}, []);
|
||||
// Fetch version info using the settings store (will skip if already loaded)
|
||||
fetchVersion();
|
||||
}, [fetchVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load saved username if it exists
|
||||
|
|
@ -234,8 +233,8 @@ const LoginForm = () => {
|
|||
lineHeight: '1.2',
|
||||
}}
|
||||
>
|
||||
⚠ Password will be stored locally without encryption. Only
|
||||
use on trusted devices.
|
||||
⚠ Password will be stored locally without encryption. Only use
|
||||
on trusted devices.
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -252,7 +251,7 @@ const LoginForm = () => {
|
|||
</Stack>
|
||||
</form>
|
||||
|
||||
{version && (
|
||||
{storedVersion.version && (
|
||||
<Text
|
||||
size="xs"
|
||||
color="dimmed"
|
||||
|
|
@ -262,7 +261,7 @@ const LoginForm = () => {
|
|||
right: 30,
|
||||
}}
|
||||
>
|
||||
v{version}
|
||||
v{storedVersion.version}
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import API from '../../api';
|
||||
import useAuthStore from '../../store/auth';
|
||||
import useSettingsStore from '../../store/settings';
|
||||
import logo from '../../assets/logo.png';
|
||||
|
||||
function SuperuserForm() {
|
||||
|
|
@ -22,15 +23,14 @@ function SuperuserForm() {
|
|||
email: '',
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const [version, setVersion] = useState(null);
|
||||
const setSuperuserExists = useAuthStore((s) => s.setSuperuserExists);
|
||||
const fetchVersion = useSettingsStore((s) => s.fetchVersion);
|
||||
const storedVersion = useSettingsStore((s) => s.version);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch version info
|
||||
API.getVersion().then((data) => {
|
||||
setVersion(data?.version);
|
||||
});
|
||||
}, []);
|
||||
// Fetch version info using the settings store (will skip if already loaded)
|
||||
fetchVersion();
|
||||
}, [fetchVersion]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData((prev) => ({
|
||||
|
|
@ -120,7 +120,7 @@ function SuperuserForm() {
|
|||
</Stack>
|
||||
</form>
|
||||
|
||||
{version && (
|
||||
{storedVersion.version && (
|
||||
<Text
|
||||
size="xs"
|
||||
color="dimmed"
|
||||
|
|
@ -130,7 +130,7 @@ function SuperuserForm() {
|
|||
right: 30,
|
||||
}}
|
||||
>
|
||||
v{version}
|
||||
v{storedVersion.version}
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ const ChannelsTable = ({ onReady }) => {
|
|||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
const hasFetchedData = useRef(false);
|
||||
const fetchVersionRef = useRef(0); // Track fetch version to prevent stale updates
|
||||
|
||||
// Drag-and-drop sensors
|
||||
const sensors = useSensors(
|
||||
|
|
@ -423,6 +424,9 @@ const ChannelsTable = ({ onReady }) => {
|
|||
* Functions
|
||||
*/
|
||||
const fetchData = useCallback(async () => {
|
||||
// Increment fetch version to track this specific fetch request
|
||||
const currentFetchVersion = ++fetchVersionRef.current;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
|
@ -447,7 +451,7 @@ const ChannelsTable = ({ onReady }) => {
|
|||
}
|
||||
|
||||
// Apply debounced filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
Object.entries(debouncedFilters).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
// Convert null values to "null" string for URL parameter
|
||||
|
|
@ -467,6 +471,11 @@ const ChannelsTable = ({ onReady }) => {
|
|||
await API.getAllChannelIds(params),
|
||||
]);
|
||||
|
||||
// Skip state updates if a newer fetch has been initiated
|
||||
if (currentFetchVersion !== fetchVersionRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
hasFetchedData.current = true;
|
||||
|
||||
|
|
@ -483,6 +492,10 @@ const ChannelsTable = ({ onReady }) => {
|
|||
onReady();
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip state updates if a newer fetch has been initiated
|
||||
if (currentFetchVersion !== fetchVersionRef.current) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(false);
|
||||
// API layer handles "Invalid page" errors by resetting and retrying
|
||||
// Just re-throw to show notification for actual errors
|
||||
|
|
@ -492,11 +505,9 @@ const ChannelsTable = ({ onReady }) => {
|
|||
pagination,
|
||||
sorting,
|
||||
debouncedFilters,
|
||||
onReady,
|
||||
showDisabled,
|
||||
selectedProfileId,
|
||||
showOnlyStreamlessChannels,
|
||||
tvgsLoaded,
|
||||
]);
|
||||
|
||||
const stopPropagation = useCallback((e) => {
|
||||
|
|
@ -987,8 +998,9 @@ const ChannelsTable = ({ onReady }) => {
|
|||
// the actual sizes through its own state after initialization.
|
||||
// Note: logos is intentionally excluded - LazyLogo components handle their own logo data
|
||||
// from the store, so we don't need to recreate columns when logos load.
|
||||
// Note: tvgsLoaded is intentionally excluded - EditableEPGCell handles loading state internally
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[selectedProfileId, channelGroups, theme, tvgsById, epgs, tvgsLoaded]
|
||||
[selectedProfileId, channelGroups, theme, tvgsById, epgs]
|
||||
);
|
||||
|
||||
const renderHeaderCell = (header) => {
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ const StreamsTable = ({ onReady }) => {
|
|||
|
||||
const [paginationString, setPaginationString] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const fetchVersionRef = useRef(0); // Track fetch version to prevent stale updates
|
||||
|
||||
// Channel creation modal state (bulk)
|
||||
const [channelNumberingModalOpen, setChannelNumberingModalOpen] =
|
||||
|
|
@ -412,6 +413,9 @@ const StreamsTable = ({ onReady }) => {
|
|||
|
||||
const fetchData = useCallback(
|
||||
async ({ showLoader = true } = {}) => {
|
||||
// Increment fetch version to track this specific fetch request
|
||||
const currentFetchVersion = ++fetchVersionRef.current;
|
||||
|
||||
if (showLoader) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
|
@ -450,6 +454,11 @@ const StreamsTable = ({ onReady }) => {
|
|||
API.getStreamFilterOptions(params),
|
||||
]);
|
||||
|
||||
// Skip state updates if a newer fetch has been initiated
|
||||
if (currentFetchVersion !== fetchVersionRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAllRowIds(ids);
|
||||
|
||||
// Set filtered options based on current filters
|
||||
|
|
@ -481,9 +490,18 @@ const StreamsTable = ({ onReady }) => {
|
|||
onReady();
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip logging if a newer fetch has been initiated
|
||||
if (currentFetchVersion !== fetchVersionRef.current) {
|
||||
return;
|
||||
}
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
|
||||
// Skip state updates if a newer fetch has been initiated
|
||||
if (currentFetchVersion !== fetchVersionRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasFetchedOnce.current = true;
|
||||
if (showLoader) {
|
||||
setIsLoading(false);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const isTokenExpired = (expirationTime) => {
|
|||
const useAuthStore = create((set, get) => ({
|
||||
isAuthenticated: false,
|
||||
isInitialized: false,
|
||||
isInitializing: false,
|
||||
needsSuperuser: false,
|
||||
user: {
|
||||
username: '',
|
||||
|
|
@ -37,17 +38,24 @@ const useAuthStore = create((set, get) => ({
|
|||
setUser: (user) => set({ user }),
|
||||
|
||||
initData: async () => {
|
||||
const user = await API.me();
|
||||
if (user.user_level <= USER_LEVELS.STREAMER) {
|
||||
throw new Error('Unauthorized');
|
||||
// Prevent multiple simultaneous initData calls
|
||||
if (get().isInitializing || get().isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({ user, isAuthenticated: true });
|
||||
|
||||
// Ensure settings are loaded first
|
||||
await useSettingsStore.getState().fetchSettings();
|
||||
set({ isInitializing: true });
|
||||
|
||||
try {
|
||||
const user = await API.me();
|
||||
if (user.user_level <= USER_LEVELS.STREAMER) {
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
set({ user });
|
||||
|
||||
// Ensure settings are loaded first
|
||||
await useSettingsStore.getState().fetchSettings();
|
||||
|
||||
// Only after settings are loaded, fetch the essential data
|
||||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
|
|
@ -64,10 +72,20 @@ const useAuthStore = create((set, get) => ({
|
|||
await Promise.all([useUsersStore.getState().fetchUsers()]);
|
||||
}
|
||||
|
||||
// Only set isAuthenticated and isInitialized AFTER all data is loaded
|
||||
// This prevents routes from rendering before data is ready
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
isInitialized: true,
|
||||
isInitializing: false,
|
||||
});
|
||||
|
||||
// Note: Logos are loaded after the Channels page tables finish loading
|
||||
// This is handled by the tables themselves signaling completion
|
||||
} catch (error) {
|
||||
console.error('Error initializing data:', error);
|
||||
set({ isInitializing: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -118,7 +136,7 @@ const useAuthStore = create((set, get) => ({
|
|||
// Action to refresh the token
|
||||
getRefreshToken: async () => {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (!refreshToken) return false; // Add explicit return here
|
||||
if (!refreshToken) return false;
|
||||
|
||||
try {
|
||||
const data = await api.refreshToken(refreshToken);
|
||||
|
|
@ -126,18 +144,17 @@ const useAuthStore = create((set, get) => ({
|
|||
set({
|
||||
accessToken: data.access,
|
||||
tokenExpiration: decodeToken(data.access),
|
||||
isAuthenticated: true,
|
||||
});
|
||||
localStorage.setItem('accessToken', data.access);
|
||||
localStorage.setItem('tokenExpiration', decodeToken(data.access));
|
||||
|
||||
return data.access;
|
||||
}
|
||||
return false; // Add explicit return for when data.access is not available
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Token refresh failed:', error);
|
||||
await get().logout();
|
||||
return false; // Add explicit return after error
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -156,6 +173,8 @@ const useAuthStore = create((set, get) => ({
|
|||
refreshToken: null,
|
||||
tokenExpiration: null,
|
||||
isAuthenticated: false,
|
||||
isInitialized: false,
|
||||
isInitializing: false,
|
||||
user: null,
|
||||
});
|
||||
localStorage.removeItem('accessToken');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { create } from 'zustand';
|
||||
import api from '../api';
|
||||
|
||||
const useSettingsStore = create((set) => ({
|
||||
const useSettingsStore = create((set, get) => ({
|
||||
settings: {},
|
||||
environment: {
|
||||
// Add default values for environment settings
|
||||
|
|
@ -10,15 +10,27 @@ const useSettingsStore = create((set) => ({
|
|||
country_name: '',
|
||||
env_mode: 'prod',
|
||||
},
|
||||
version: {
|
||||
version: '',
|
||||
timestamp: null,
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
fetchSettings: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const settings = await api.getSettings();
|
||||
const env = await api.getEnvironmentSettings();
|
||||
set({
|
||||
// Only fetch version if not already loaded (may have been fetched by Login/Superuser form)
|
||||
const currentVersion = get().version;
|
||||
const needsVersion = !currentVersion.version;
|
||||
|
||||
const [settings, env, versionData] = await Promise.all([
|
||||
api.getSettings(),
|
||||
api.getEnvironmentSettings(),
|
||||
needsVersion ? api.getVersion() : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const newState = {
|
||||
settings: settings.reduce((acc, setting) => {
|
||||
acc[setting.key] = setting;
|
||||
return acc;
|
||||
|
|
@ -30,12 +42,42 @@ const useSettingsStore = create((set) => ({
|
|||
country_name: '',
|
||||
env_mode: 'prod',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Only update version if we fetched it
|
||||
if (versionData) {
|
||||
newState.version = {
|
||||
version: versionData?.version || '',
|
||||
timestamp: versionData?.timestamp || null,
|
||||
};
|
||||
}
|
||||
|
||||
set(newState);
|
||||
} catch (error) {
|
||||
set({ error: 'Failed to load settings.', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch version independently (for unauthenticated pages like Login)
|
||||
fetchVersion: async () => {
|
||||
// Skip if already loaded
|
||||
if (get().version.version) {
|
||||
return get().version;
|
||||
}
|
||||
try {
|
||||
const versionData = await api.getVersion();
|
||||
const version = {
|
||||
version: versionData?.version || '',
|
||||
timestamp: versionData?.timestamp || null,
|
||||
};
|
||||
set({ version });
|
||||
return version;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch version:', error);
|
||||
return get().version;
|
||||
}
|
||||
},
|
||||
|
||||
updateSetting: (setting) =>
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, [setting.key]: setting },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue