u
This commit is contained in:
@@ -35,6 +35,14 @@ export {
|
|||||||
selectActiveHelpId,
|
selectActiveHelpId,
|
||||||
} from './helpAutomat';
|
} from './helpAutomat';
|
||||||
|
|
||||||
|
export {
|
||||||
|
default as pwaAutomat,
|
||||||
|
initPwa,
|
||||||
|
openPwaDialog,
|
||||||
|
closePwaDialog,
|
||||||
|
applyPwaUpdate,
|
||||||
|
} from './pwaAutomat';
|
||||||
|
|
||||||
export { default as supplierAutomat } from './supplierAutomat';
|
export { default as supplierAutomat } from './supplierAutomat';
|
||||||
export { default as warehouseAutomat } from './warehouseAutomat';
|
export { default as warehouseAutomat } from './warehouseAutomat';
|
||||||
|
|
||||||
|
|||||||
125
src/automats/pwaAutomat.js
Normal file
125
src/automats/pwaAutomat.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @file pwaAutomat.js
|
||||||
|
* @description Manages PWA update lifecycle, service worker registration,
|
||||||
|
* and user prompt dialog state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Automat } from 'react-automat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PwaState
|
||||||
|
* @property {boolean} needRefresh Whether a new SW update is available and waiting
|
||||||
|
* @property {boolean} dialogOpen Whether the update dialog is currently visible
|
||||||
|
* @property {boolean} offlineReady Whether content is cached for offline use
|
||||||
|
* @property {boolean} updating Whether an update is currently being applied
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {PwaState} */
|
||||||
|
const initialState = {
|
||||||
|
needRefresh: false,
|
||||||
|
dialogOpen: false,
|
||||||
|
offlineReady: false,
|
||||||
|
updating: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let swUpdater = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State store for PWA lifecycle & update notifications.
|
||||||
|
* Registered on window.__AUTOMATS__ as 'pwa'.
|
||||||
|
*
|
||||||
|
* @type {Automat<PwaState>}
|
||||||
|
*/
|
||||||
|
const pwaAutomat = new Automat(
|
||||||
|
initialState,
|
||||||
|
{
|
||||||
|
setRegistration(updater) {
|
||||||
|
swUpdater = updater;
|
||||||
|
},
|
||||||
|
setNeedRefresh() {
|
||||||
|
pwaAutomat.setState({
|
||||||
|
needRefresh: true,
|
||||||
|
dialogOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setOfflineReady() {
|
||||||
|
pwaAutomat.setState({
|
||||||
|
offlineReady: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openDialog() {
|
||||||
|
pwaAutomat.setState({ dialogOpen: true });
|
||||||
|
},
|
||||||
|
closeDialog() {
|
||||||
|
pwaAutomat.setState({ dialogOpen: false });
|
||||||
|
},
|
||||||
|
async applyUpdate() {
|
||||||
|
pwaAutomat.setState({ updating: true });
|
||||||
|
if (typeof swUpdater === 'function') {
|
||||||
|
try {
|
||||||
|
await swUpdater(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PWA] Error activating update:', err);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Allows testing the dialog in dev mode or in browser console:
|
||||||
|
// window.__AUTOMATS__.pwa.actions.simulateUpdateAvailable()
|
||||||
|
simulateUpdateAvailable() {
|
||||||
|
pwaAutomat.setState({
|
||||||
|
needRefresh: true,
|
||||||
|
dialogOpen: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 'pwa' },
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes PWA service worker registration in production.
|
||||||
|
*/
|
||||||
|
export function initPwa() {
|
||||||
|
if (!import.meta.env.PROD || !('serviceWorker' in navigator)) return;
|
||||||
|
|
||||||
|
import('virtual:pwa-register')
|
||||||
|
.then(({ registerSW }) => {
|
||||||
|
const updateSW = registerSW({
|
||||||
|
immediate: true,
|
||||||
|
onNeedRefresh() {
|
||||||
|
pwaAutomat.actions.setNeedRefresh();
|
||||||
|
},
|
||||||
|
onOfflineReady() {
|
||||||
|
pwaAutomat.actions.setOfflineReady();
|
||||||
|
},
|
||||||
|
onRegistered(registration) {
|
||||||
|
if (!registration) return;
|
||||||
|
|
||||||
|
// Check for new updates periodically (every 30 minutes)
|
||||||
|
setInterval(() => {
|
||||||
|
registration.update().catch(() => {});
|
||||||
|
}, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
// Check for updates when tab or PWA returns to foreground
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
registration.update().catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pwaAutomat.actions.setRegistration(updateSW);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn('[PWA] Service worker registration failed or omitted:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openPwaDialog = () => pwaAutomat.actions.openDialog();
|
||||||
|
export const closePwaDialog = () => pwaAutomat.actions.closeDialog();
|
||||||
|
export const applyPwaUpdate = () => pwaAutomat.actions.applyUpdate();
|
||||||
|
|
||||||
|
export default pwaAutomat;
|
||||||
114
src/components/PwaUpdateDialog.jsx
Normal file
114
src/components/PwaUpdateDialog.jsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||||
|
import AppDialog from './AppDialog';
|
||||||
|
import pwaAutomat from '../automats/pwaAutomat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PWA Update prompt dialog.
|
||||||
|
* Appears when a new service worker update has been downloaded in the background.
|
||||||
|
* Allows the user to reload the page to apply the update immediately,
|
||||||
|
* or postpone it ("Später") without losing ongoing form input.
|
||||||
|
*/
|
||||||
|
export default class PwaUpdateDialog extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
dialogOpen: pwaAutomat.state.dialogOpen,
|
||||||
|
updating: pwaAutomat.state.updating,
|
||||||
|
};
|
||||||
|
this.handleUpdate = this.handleUpdate.bind(this);
|
||||||
|
this.handleClose = this.handleClose.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.unsub = pwaAutomat.subscribe(this, ['dialogOpen', 'updating']);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unsub?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
if (this.state.updating) return;
|
||||||
|
pwaAutomat.actions.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdate() {
|
||||||
|
pwaAutomat.actions.applyUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { dialogOpen, updating } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onClose={this.handleClose}
|
||||||
|
maxWidth="xs"
|
||||||
|
fullWidth
|
||||||
|
aria-labelledby="pwa-update-dialog-title"
|
||||||
|
>
|
||||||
|
<DialogTitle
|
||||||
|
id="pwa-update-dialog-title"
|
||||||
|
sx={{ display: 'flex', alignItems: 'center', gap: 1.5, pb: 1 }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
color: 'primary.contrastText',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SystemUpdateAltIcon fontSize="small" />
|
||||||
|
</Box>
|
||||||
|
Aktualisierung verfügbar
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText sx={{ color: 'text.primary', mb: 1, fontWeight: 500 }}>
|
||||||
|
Eine neue Version von <strong>Dashfront</strong> ist bereit.
|
||||||
|
</DialogContentText>
|
||||||
|
<DialogContentText variant="body2" color="text.secondary">
|
||||||
|
Möchten Sie die Anwendung jetzt neu laden, um die Änderungen zu übernehmen?
|
||||||
|
Stellen Sie sicher, dass wichtige Eingaben gespeichert sind.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2.5, pt: 1, gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
onClick={this.handleClose}
|
||||||
|
color="inherit"
|
||||||
|
disabled={updating}
|
||||||
|
sx={{ textTransform: 'none' }}
|
||||||
|
>
|
||||||
|
Später
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={this.handleUpdate}
|
||||||
|
variant="contained"
|
||||||
|
disabled={updating}
|
||||||
|
startIcon={
|
||||||
|
updating ? <CircularProgress size={16} color="inherit" /> : null
|
||||||
|
}
|
||||||
|
autoFocus
|
||||||
|
sx={{ textTransform: 'none', px: 2 }}
|
||||||
|
>
|
||||||
|
{updating ? 'Aktualisiere…' : 'Jetzt neu laden'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</AppDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import Avatar from '@mui/material/Avatar';
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import Badge from '@mui/material/Badge';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Menu from '@mui/material/Menu';
|
import Menu from '@mui/material/Menu';
|
||||||
@@ -10,10 +11,11 @@ import LogoutIcon from '@mui/icons-material/Logout';
|
|||||||
import BusinessIcon from '@mui/icons-material/Business';
|
import BusinessIcon from '@mui/icons-material/Business';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
|
import HelpOutlineRoundedIcon from '@mui/icons-material/HelpOutlineRounded';
|
||||||
|
import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import authAutomat, { bootstrapAuthMe } from '../automats/authAutomat';
|
import authAutomat, { bootstrapAuthMe } from '../automats/authAutomat';
|
||||||
import { helpAutomat, toggleHelp } from '../automats';
|
import { helpAutomat, toggleHelp, pwaAutomat, openPwaDialog } from '../automats';
|
||||||
import CompanySettingsDialog from './CompanySettingsDialog';
|
import CompanySettingsDialog from './CompanySettingsDialog';
|
||||||
import SettingsDialog from './settings/SettingsDialog';
|
import SettingsDialog from './settings/SettingsDialog';
|
||||||
import { notifyError } from '../utils/notify';
|
import { notifyError } from '../utils/notify';
|
||||||
@@ -27,6 +29,7 @@ class UserMenu extends PureComponent {
|
|||||||
numbersOpen: false,
|
numbersOpen: false,
|
||||||
user: authAutomat.state.user,
|
user: authAutomat.state.user,
|
||||||
helpEnabled: helpAutomat.state?.enabled ?? false,
|
helpEnabled: helpAutomat.state?.enabled ?? false,
|
||||||
|
pwaUpdateAvailable: pwaAutomat.state.needRefresh,
|
||||||
};
|
};
|
||||||
this.handleOpenMenu = this.handleOpenMenu.bind(this);
|
this.handleOpenMenu = this.handleOpenMenu.bind(this);
|
||||||
this.handleCloseMenu = this.handleCloseMenu.bind(this);
|
this.handleCloseMenu = this.handleCloseMenu.bind(this);
|
||||||
@@ -35,6 +38,7 @@ class UserMenu extends PureComponent {
|
|||||||
this.handleOpenNumbers = this.handleOpenNumbers.bind(this);
|
this.handleOpenNumbers = this.handleOpenNumbers.bind(this);
|
||||||
this.handleCloseNumbers = this.handleCloseNumbers.bind(this);
|
this.handleCloseNumbers = this.handleCloseNumbers.bind(this);
|
||||||
this.handleToggleHelp = this.handleToggleHelp.bind(this);
|
this.handleToggleHelp = this.handleToggleHelp.bind(this);
|
||||||
|
this.handleOpenPwaUpdate = this.handleOpenPwaUpdate.bind(this);
|
||||||
this.handleLogout = this.handleLogout.bind(this);
|
this.handleLogout = this.handleLogout.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +47,20 @@ class UserMenu extends PureComponent {
|
|||||||
this.unsubscribeHelp = helpAutomat.subscribe(this, (state) => ({
|
this.unsubscribeHelp = helpAutomat.subscribe(this, (state) => ({
|
||||||
helpEnabled: Boolean(state?.enabled),
|
helpEnabled: Boolean(state?.enabled),
|
||||||
}));
|
}));
|
||||||
|
this.unsubscribePwa = pwaAutomat.subscribe(this, (state) => ({
|
||||||
|
pwaUpdateAvailable: Boolean(state?.needRefresh),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubscribeAuth?.();
|
this.unsubscribeAuth?.();
|
||||||
this.unsubscribeHelp?.();
|
this.unsubscribeHelp?.();
|
||||||
|
this.unsubscribePwa?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenPwaUpdate() {
|
||||||
|
this.setState({ anchorEl: null });
|
||||||
|
openPwaDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenMenu(event) {
|
handleOpenMenu(event) {
|
||||||
@@ -91,7 +104,7 @@ class UserMenu extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, anchorEl, settingsOpen, numbersOpen, helpEnabled } = this.state;
|
const { user, anchorEl, settingsOpen, numbersOpen, helpEnabled, pwaUpdateAvailable } = this.state;
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
const pictureSrc = (() => {
|
const pictureSrc = (() => {
|
||||||
@@ -112,9 +125,17 @@ class UserMenu extends PureComponent {
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
|
sx={{ p: 0, width: 32, height: 32, borderRadius: '50%' }}
|
||||||
>
|
>
|
||||||
<Avatar src={pictureSrc} alt={user?.email} sx={{ width: 32, height: 32 }}>
|
<Badge
|
||||||
{user?.email?.[0]?.toUpperCase()}
|
color="primary"
|
||||||
</Avatar>
|
variant="dot"
|
||||||
|
invisible={!pwaUpdateAvailable}
|
||||||
|
overlap="circular"
|
||||||
|
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
>
|
||||||
|
<Avatar src={pictureSrc} alt={user?.email} sx={{ width: 32, height: 32 }}>
|
||||||
|
{user?.email?.[0]?.toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
|
</Badge>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -124,6 +145,18 @@ class UserMenu extends PureComponent {
|
|||||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||||
>
|
>
|
||||||
|
{pwaUpdateAvailable && (
|
||||||
|
<MenuItem onClick={this.handleOpenPwaUpdate} sx={{ bgcolor: 'action.hover' }}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<SystemUpdateAltIcon fontSize="small" color="primary" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="Update verfügbar"
|
||||||
|
secondary="Jetzt aktualisieren"
|
||||||
|
primaryTypographyProps={{ fontWeight: 600, color: 'primary.main' }}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem disabled>{user?.email}</MenuItem>
|
<MenuItem disabled>{user?.email}</MenuItem>
|
||||||
<MenuItem disabled={!user?.selectedCompanyId} onClick={this.handleOpenSettings}>
|
<MenuItem disabled={!user?.selectedCompanyId} onClick={this.handleOpenSettings}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
|||||||
17
src/main.jsx
17
src/main.jsx
@@ -11,6 +11,8 @@ import { installCssVars } from './colors';
|
|||||||
// Side-effect imports: each module registers its automat and subscribes to
|
// Side-effect imports: each module registers its automat and subscribes to
|
||||||
// authAutomat, so the stores must be evaluated before the session bootstraps.
|
// authAutomat, so the stores must be evaluated before the session bootstraps.
|
||||||
import './automats';
|
import './automats';
|
||||||
|
import { initPwa } from './automats';
|
||||||
|
import PwaUpdateDialog from './components/PwaUpdateDialog';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
// Publish the color tokens as CSS custom properties so index.css (and any
|
// Publish the color tokens as CSS custom properties so index.css (and any
|
||||||
@@ -37,18 +39,8 @@ if (import.meta.env.DEV) {
|
|||||||
void clearDevServiceWorker();
|
void clearDevServiceWorker();
|
||||||
}
|
}
|
||||||
|
|
||||||
// PWA auto-update: reload the page as soon as a new build's service worker
|
// Initialize PWA service worker registration and update listener in production
|
||||||
// takes control. The virtual module only exists in production builds (the
|
initPwa();
|
||||||
// plugin is registered for `build` only), hence the dynamic import.
|
|
||||||
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
|
||||||
import('virtual:pwa-register')
|
|
||||||
.then(({ registerSW }) => {
|
|
||||||
registerSW({ immediate: true });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// No SW in this build — app still works, just without offline support.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<ThemeProvider theme={getActiveTheme()}>
|
<ThemeProvider theme={getActiveTheme()}>
|
||||||
@@ -71,6 +63,7 @@ createRoot(document.getElementById('root')).render(
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
<PwaUpdateDialog />
|
||||||
</AuthBootstrap>
|
</AuthBootstrap>
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</ThemeProvider>,
|
</ThemeProvider>,
|
||||||
|
|||||||
@@ -289,12 +289,9 @@ export default defineConfig(({ command }) => ({
|
|||||||
...(command === 'build'
|
...(command === 'build'
|
||||||
? [
|
? [
|
||||||
VitePWA({
|
VitePWA({
|
||||||
// autoUpdate: the SW calls clientsClaim()/skipWaiting() itself, so a
|
// prompt: new service worker waits in the background until the user
|
||||||
// new build takes over every open tab on its own.
|
// confirms update via the PWA update dialog.
|
||||||
registerType: 'autoUpdate',
|
registerType: 'prompt',
|
||||||
// Registration is done explicitly in src/main.jsx via
|
|
||||||
// `virtual:pwa-register` with `immediate: true`; that is what makes
|
|
||||||
// the *page* reload (a bare injected script only swaps the SW).
|
|
||||||
injectRegister: false,
|
injectRegister: false,
|
||||||
manifest: {
|
manifest: {
|
||||||
id: '/',
|
id: '/',
|
||||||
@@ -325,12 +322,6 @@ export default defineConfig(({ command }) => ({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
// NOTE: vite-plugin-pwa only auto-sets these two when
|
|
||||||
// injectRegister === 'auto'. Because we register manually in
|
|
||||||
// src/main.jsx (injectRegister: false), they must be explicit —
|
|
||||||
// otherwise a new SW waits forever behind the old one and no tab
|
|
||||||
// is ever claimed.
|
|
||||||
skipWaiting: true,
|
|
||||||
clientsClaim: true,
|
clientsClaim: true,
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,webmanifest,woff2}'],
|
||||||
globIgnores: ['**/*.wasm'],
|
globIgnores: ['**/*.wasm'],
|
||||||
|
|||||||
Reference in New Issue
Block a user