diff --git a/apps/ui/package-lock.json b/apps/ui/package-lock.json index d53fc8ee..395b894d 100644 --- a/apps/ui/package-lock.json +++ b/apps/ui/package-lock.json @@ -36,7 +36,7 @@ "react-hot-toast": "^2.4.0", "react-i18next": "^14.1.0", "react-popper": "^2.3.0", - "react-router-dom": "^6.4.2", + "react-router-dom": "^6.28.0", "reactflow": "11.10.1", "rrule": "2.7.2", "uuid": "^9.0.0", @@ -4190,9 +4190,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", + "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", "engines": { "node": ">=14.0.0" } @@ -18057,11 +18057,11 @@ } }, "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", + "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", "dependencies": { - "@remix-run/router": "1.15.3" + "@remix-run/router": "1.21.0" }, "engines": { "node": ">=14.0.0" @@ -18071,12 +18071,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", + "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" + "@remix-run/router": "1.21.0", + "react-router": "6.28.0" }, "engines": { "node": ">=14.0.0" diff --git a/apps/ui/package.json b/apps/ui/package.json index 43098ac6..325a5f0e 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -31,7 +31,7 @@ "react-hot-toast": "^2.4.0", "react-i18next": "^14.1.0", "react-popper": "^2.3.0", - "react-router-dom": "^6.4.2", + "react-router-dom": "^6.28.0", "reactflow": "11.10.1", "rrule": "2.7.2", "uuid": "^9.0.0", diff --git a/apps/ui/public/locales/en.json b/apps/ui/public/locales/en.json index a110c787..04c3a073 100644 --- a/apps/ui/public/locales/en.json +++ b/apps/ui/public/locales/en.json @@ -45,6 +45,7 @@ "click_rate": "Click Rate", "clicks": "Clicks", "code": "Code", + "confirm_unsaved_changes": "Are you sure you want to leave? You have unsaved changes.", "create": "Create", "create_campaign": "Create Campaign", "create_journey": "Create Journey", diff --git a/apps/ui/public/locales/es.json b/apps/ui/public/locales/es.json index 65aab92c..e7d2cedb 100644 --- a/apps/ui/public/locales/es.json +++ b/apps/ui/public/locales/es.json @@ -45,6 +45,7 @@ "click_rate": "Ratio de clics", "clicks": "Clics", "code": "Código", + "confirm_unsaved_changes": "¿Estás seguro de que deseas salir? Tienes cambios sin guardar.", "create": "Crear", "create_campaign": "Crear Campaña", "create_journey": "Crear Camino", diff --git a/apps/ui/src/views/users/ListDetail.tsx b/apps/ui/src/views/users/ListDetail.tsx index cb52f663..2c71f337 100644 --- a/apps/ui/src/views/users/ListDetail.tsx +++ b/apps/ui/src/views/users/ListDetail.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useState } from 'react' +import { useCallback, useContext, useEffect, useState } from 'react' import api from '../../api' import { ListContext, ProjectContext } from '../../contexts' import { DynamicList, ListUpdateParams, Rule } from '../../types' @@ -20,15 +20,26 @@ import { EditIcon, SendIcon, UploadIcon } from '../../ui/icons' import { TagPicker } from '../settings/TagPicker' import { useTranslation } from 'react-i18next' import { Alert } from '../../ui' +import { useBlocker } from 'react-router-dom' -const RuleSection = ({ list, onRuleSave }: { list: DynamicList, onRuleSave: (rule: Rule) => void }) => { +interface RuleSectionProps { + list: DynamicList + onRuleSave: (rule: Rule) => void + onChange?: (rule: Rule) => void +} + +const RuleSection = ({ list, onRuleSave, onChange }: RuleSectionProps) => { const { t } = useTranslation() const [rule, setRule] = useState(list.rule) + const onSetRule = (rule: Rule) => { + setRule(rule) + onChange?.(rule) + } return <> onRuleSave(rule) }>{t('rules_save')} } /> - + } @@ -39,11 +50,44 @@ export default function ListDetail() { const [isDialogOpen, setIsDialogOpen] = useState(false) const [isEditListOpen, setIsEditListOpen] = useState(false) const [isUploadOpen, setIsUploadOpen] = useState(false) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) const [error, setError] = useState() const state = useSearchTableState(useCallback(async params => await api.lists.users(project.id, list.id, params), [list, project])) const route = useRoute() + useEffect(() => { + const refresh = () => { + api.lists.get(project.id, list.id) + .then(setList) + .then(() => state.reload) + .catch(() => {}) + } + + if (list.state !== 'loading') return + const complete = list.progress?.complete ?? 0 + const total = list.progress?.total ?? 0 + const percent = total > 0 ? complete / total * 100 : 0 + const refreshRate = percent < 5 ? 1000 : 5000 + const interval = setInterval(refresh, refreshRate) + refresh() + + return () => clearInterval(interval) + }, [list.state]) + + const blocker = useBlocker( + ({ currentLocation, nextLocation }) => hasUnsavedChanges && currentLocation.pathname !== nextLocation.pathname, + ) + + useEffect(() => { + if (blocker.state !== 'blocked') return + if (confirm(t('confirm_unsaved_changes'))) { + blocker.proceed() + } else { + blocker.reset() + } + }, [blocker.state]) + const saveList = async ({ name, rule, published, tags }: ListUpdateParams) => { try { const value = await api.lists.update(project.id, list.id, { name, rule, published, tags }) @@ -51,6 +95,7 @@ export default function ListDetail() { setList(value) setIsEditListOpen(false) setIsDialogOpen(true) + setHasUnsavedChanges(false) } catch (error: any) { const errorMessage = error.response?.data?.error ?? error.message setError(errorMessage) @@ -92,7 +137,12 @@ export default function ListDetail() { {error && {error}} - {list.type === 'dynamic' && await saveList({ name: list.name, rule })} />} + {list.type === 'dynamic' && ( + await saveList({ name: list.name, rule })} + onChange={() => setHasUnsavedChanges(true)} /> + )} , ) - nodes.push(' ' + operatorTypes[rule.type]?.find(ot => ot.key === rule.operator)?.label ?? rule.operator) + nodes.push(' ' + (operatorTypes[rule.type]?.find(ot => ot.key === rule.operator)?.label ?? rule.operator)) if (rule.operator !== 'empty' && rule.operator !== 'is set' && rule.operator !== 'is not set') { nodes.push(' ') diff --git a/package-lock.json b/package-lock.json index 1e8c2292..6681f392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -148,7 +148,7 @@ "react-hot-toast": "^2.4.0", "react-i18next": "^14.1.0", "react-popper": "^2.3.0", - "react-router-dom": "^6.4.2", + "react-router-dom": "^6.28.0", "reactflow": "11.10.1", "rrule": "2.7.2", "uuid": "^9.0.0", @@ -7194,9 +7194,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", + "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", "engines": { "node": ">=14.0.0" } @@ -28112,11 +28112,11 @@ } }, "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", + "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", "dependencies": { - "@remix-run/router": "1.15.3" + "@remix-run/router": "1.21.0" }, "engines": { "node": ">=14.0.0" @@ -28126,12 +28126,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.0.tgz", + "integrity": "sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==", "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" + "@remix-run/router": "1.21.0", + "react-router": "6.28.0" }, "engines": { "node": ">=14.0.0"