Skip to content

Commit 70809bb

Browse files
committed
Merge branch '2.4' into 2026.x
2 parents 841c24f + 220e209 commit 70809bb

5 files changed

Lines changed: 232 additions & 151 deletions

File tree

assets/studio/js/src/modules/data-importer/components/tabs/execution-tab.tsx

Lines changed: 9 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,17 @@
88
* @license Pimcore Open Core License (POCL)
99
*/
1010

11-
import React, { useEffect, useState } from 'react'
11+
import React from 'react'
1212
import {
13-
Button,
1413
DatePicker,
1514
Form,
16-
Progress,
17-
Select,
18-
Text,
19-
useMessage
15+
Select
2016
} from '@pimcore/studio-ui-bundle/components'
2117
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
22-
import { ApiError, trackError } from '@pimcore/studio-ui-bundle/modules/app'
23-
import {
24-
useBundleDataImporterConfigStartImportMutation,
25-
useBundleDataImporterConfigCancelExecutionMutation,
26-
useBundleDataImporterConfigCheckImportProgressQuery
27-
} from '../../data-importer-api-slice.gen'
2818
import type { DataImporterFormValues } from '../../types'
2919
import { DataImporterPanel } from './steps/data-importer-panel/data-importer-panel'
30-
import { useStyles } from './execution-tab.styles'
3120
import { CronDefinitionSection } from './execution-tab/cron-definition-section/cron-definition-section'
32-
import { ManualExecutionButton } from './execution-tab/manual-execution-button/manual-execution-button'
33-
34-
const POLL_INTERVAL_MS = 5000
21+
import { ExecutionStatus } from './execution-tab/execution-status/execution-status'
3522

3623
export interface ExecutionTabProps {
3724
configName: string
@@ -46,88 +33,6 @@ const isOneTimeJob = (values: DataImporterFormValues): boolean =>
4633

4734
export const ExecutionTab = ({ configName, isDirty }: ExecutionTabProps): React.JSX.Element => {
4835
const { t } = useTranslation()
49-
const messageApi = useMessage()
50-
const { styles } = useStyles()
51-
52-
const [startImport, { isLoading: isStarting }] = useBundleDataImporterConfigStartImportMutation()
53-
const [cancelExecution, { isLoading: isCancelling }] = useBundleDataImporterConfigCancelExecutionMutation()
54-
55-
const { data: progressData, refetch: refetchProgress } = useBundleDataImporterConfigCheckImportProgressQuery(
56-
{ name: configName },
57-
{ pollingInterval: POLL_INTERVAL_MS }
58-
)
59-
60-
const [optimisticRunning, setOptimisticRunning] = useState(false)
61-
const [optimisticProgress, setOptimisticProgress] = useState<{ processedItems: number, totalItems: number, progress: number } | null>(null)
62-
const [optimisticCancelled, setOptimisticCancelled] = useState(false)
63-
const [hasCompleted, setHasCompleted] = useState(false)
64-
65-
// Once a real "running" response arrives, clear start-optimistic flags
66-
useEffect(() => {
67-
if (progressData?.isRunning === true) {
68-
setOptimisticRunning(false)
69-
setOptimisticProgress(null)
70-
setHasCompleted(false)
71-
}
72-
}, [progressData?.isRunning])
73-
74-
// Once polling confirms not running, clear cancel-optimistic flag; mark completed if items were processed
75-
useEffect(() => {
76-
if (progressData?.isRunning === false) {
77-
setOptimisticCancelled(false)
78-
if ((progressData.processedItems ?? 0) > 0) {
79-
setHasCompleted(true)
80-
}
81-
}
82-
}, [progressData?.isRunning])
83-
84-
const isRunning = !optimisticCancelled && (optimisticRunning || (progressData?.isRunning ?? false))
85-
const progress = optimisticProgress?.progress ?? progressData?.progress ?? 0
86-
const processedItems = optimisticProgress?.processedItems ?? progressData?.processedItems ?? 0
87-
const totalItems = optimisticProgress?.totalItems ?? progressData?.totalItems ?? 0
88-
89-
const handleStartImport = async (): Promise<void> => {
90-
const result = await startImport({ name: configName })
91-
92-
if ('error' in result) {
93-
if (result.error !== undefined) {
94-
trackError(new ApiError(result.error))
95-
}
96-
void messageApi.error(t('data-importer.execution.start-import.error'))
97-
return
98-
}
99-
100-
if (result.data.success) {
101-
void messageApi.success(t('data-importer.execution.start-import.success'))
102-
// Immediately show progress bar at 0, resetting any previous run's values
103-
setOptimisticProgress({ processedItems: 0, totalItems: progressData?.totalItems ?? 0, progress: 0 })
104-
setOptimisticRunning(true)
105-
setHasCompleted(false)
106-
} else {
107-
void messageApi.error(t('data-importer.execution.start-import.error'))
108-
}
109-
void refetchProgress()
110-
}
111-
112-
const handleCancelExecution = async (): Promise<void> => {
113-
const result = await cancelExecution({ name: configName })
114-
115-
if ('error' in result) {
116-
if (result.error !== undefined) {
117-
trackError(new ApiError(result.error))
118-
}
119-
void messageApi.error(t('data-importer.execution.cancel.error'))
120-
return
121-
}
122-
123-
void messageApi.success(t('data-importer.execution.cancel.success'))
124-
// Immediately hide the progress bar before polling confirms
125-
setOptimisticCancelled(true)
126-
setOptimisticRunning(false)
127-
setOptimisticProgress(null)
128-
setHasCompleted(false)
129-
void refetchProgress()
130-
}
13136

13237
const scheduleTypeOptions = [
13338
{ value: 'recurring', label: t('data-importer.execution.schedule-type.recurring') },
@@ -136,15 +41,11 @@ export const ExecutionTab = ({ configName, isDirty }: ExecutionTabProps): React.
13641

13742
return (
13843
<>
139-
{ /* ── Manual Execution ── */ }
140-
<DataImporterPanel title={ t('data-importer.execution.manual-execution') }>
141-
<ManualExecutionButton
142-
isDirty={ isDirty }
143-
isStarting={ isStarting }
144-
label={ t('data-importer.execution.start-import') }
145-
onStart={ () => { void handleStartImport() } }
146-
/>
147-
</DataImporterPanel>
44+
{ /* ── Manual Execution + Execution Status (isolated to avoid poll-driven re-renders) ── */ }
45+
<ExecutionStatus
46+
configName={ configName }
47+
isDirty={ isDirty }
48+
/>
14849

14950
{ /* ── Scheduled Execution ── */ }
15051
<DataImporterPanel title={ t('data-importer.execution.settings.title') }>
@@ -179,52 +80,13 @@ export const ExecutionTab = ({ configName, isDirty }: ExecutionTabProps): React.
17980
name={ ['executionConfig', 'scheduledAt'] }
18081
>
18182
<DatePicker
182-
outputFormat="DD-MM-YYYY HH:mm"
83+
outputFormat="YYYY-MM-DD HH:mm"
18384
outputType="dateString"
18485
showTime={ { format: 'HH:mm' } }
18586
/>
18687
</Form.Item>
18788
</Form.Conditional>
18889
</DataImporterPanel>
189-
190-
{ /* ── Execution Status ── */ }
191-
<DataImporterPanel
192-
noWidthLimit
193-
title={ t('data-importer.execution.status.title') }
194-
>
195-
{ isRunning
196-
? (
197-
<>
198-
<p className={ styles.progressLabel }>
199-
{ t('data-importer.execution.status.current-progress') }
200-
</p>
201-
<div className={ styles.progressWrapper }>
202-
<Progress
203-
format={ () => t('data-importer.execution.status.processing', { processedItems, totalItems }) }
204-
percent={ Math.round(progress * 100) }
205-
percentPosition={ { align: 'start', type: 'inner' } }
206-
size={ [-1, 32] }
207-
status="active"
208-
strokeColor={ styles.colorFill }
209-
trailColor={ 'rgba(0, 0, 0, 0.06)' }
210-
/>
211-
</div>
212-
<Button
213-
loading={ isCancelling }
214-
onClick={ () => { void handleCancelExecution() } }
215-
>
216-
{ t('data-importer.execution.status.cancel') }
217-
</Button>
218-
</>
219-
)
220-
: (
221-
<Text>
222-
{ t(hasCompleted
223-
? 'data-importer.execution.status.finished'
224-
: 'data-importer.execution.status.not-running') }
225-
</Text>
226-
) }
227-
</DataImporterPanel>
22890
</>
22991
)
23092
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* This source file is available under the terms of the
3+
* Pimcore Open Core License (POCL)
4+
* Full copyright and license information is available in
5+
* LICENSE.md which is distributed with this source code.
6+
*
7+
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
8+
* @license Pimcore Open Core License (POCL)
9+
*/
10+
11+
import React, { useEffect, useState } from 'react'
12+
import {
13+
Button,
14+
Progress,
15+
Text,
16+
useMessage
17+
} from '@pimcore/studio-ui-bundle/components'
18+
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
19+
import { ApiError, trackError } from '@pimcore/studio-ui-bundle/modules/app'
20+
import {
21+
useBundleDataImporterConfigStartImportMutation,
22+
useBundleDataImporterConfigCancelExecutionMutation,
23+
useBundleDataImporterConfigCheckImportProgressQuery
24+
} from '../../../../data-importer-api-slice-enhanced'
25+
import { DataImporterPanel } from '../../steps/data-importer-panel/data-importer-panel'
26+
import { ManualExecutionButton } from '../manual-execution-button/manual-execution-button'
27+
import { useStyles } from '../../execution-tab.styles'
28+
29+
const POLL_INTERVAL_MS = 5000
30+
31+
export interface ExecutionStatusProps {
32+
configName: string
33+
isDirty: boolean
34+
}
35+
36+
/**
37+
* Encapsulates import polling, optimistic state, the manual-execution button,
38+
* and the execution-status progress bar.
39+
*
40+
* Extracted from ExecutionTab so that poll-driven re-renders are isolated
41+
* and do not reset the DatePicker's unconfirmed selection.
42+
*/
43+
export const ExecutionStatus = ({ configName, isDirty }: ExecutionStatusProps): React.JSX.Element => {
44+
const { t } = useTranslation()
45+
const messageApi = useMessage()
46+
const { styles } = useStyles()
47+
48+
const [startImport, { isLoading: isStarting }] = useBundleDataImporterConfigStartImportMutation()
49+
const [cancelExecution, { isLoading: isCancelling }] = useBundleDataImporterConfigCancelExecutionMutation()
50+
51+
const { data: progressData, refetch: refetchProgress } = useBundleDataImporterConfigCheckImportProgressQuery(
52+
{ name: configName },
53+
{ pollingInterval: POLL_INTERVAL_MS }
54+
)
55+
56+
const [optimisticRunning, setOptimisticRunning] = useState(false)
57+
const [optimisticProgress, setOptimisticProgress] = useState<{ processedItems: number, totalItems: number, progress: number } | null>(null)
58+
const [optimisticCancelled, setOptimisticCancelled] = useState(false)
59+
const [hasCompleted, setHasCompleted] = useState(false)
60+
61+
// Once a real "running" response arrives, clear start-optimistic flags
62+
useEffect(() => {
63+
if (progressData?.isRunning === true) {
64+
setOptimisticRunning(false)
65+
setOptimisticProgress(null)
66+
setHasCompleted(false)
67+
}
68+
}, [progressData?.isRunning])
69+
70+
// Once polling confirms not running, clear cancel-optimistic flag; mark completed if items were processed
71+
useEffect(() => {
72+
if (progressData?.isRunning === false) {
73+
setOptimisticCancelled(false)
74+
if ((progressData.processedItems ?? 0) > 0) {
75+
setHasCompleted(true)
76+
}
77+
}
78+
}, [progressData?.isRunning])
79+
80+
const isRunning = !optimisticCancelled && (optimisticRunning || (progressData?.isRunning ?? false))
81+
const progress = optimisticProgress?.progress ?? progressData?.progress ?? 0
82+
const processedItems = optimisticProgress?.processedItems ?? progressData?.processedItems ?? 0
83+
const totalItems = optimisticProgress?.totalItems ?? progressData?.totalItems ?? 0
84+
85+
const handleStartImport = async (): Promise<void> => {
86+
const result = await startImport({ name: configName })
87+
88+
if ('error' in result) {
89+
if (result.error !== undefined) {
90+
trackError(new ApiError(result.error))
91+
}
92+
void messageApi.error(t('data-importer.execution.start-import.error'))
93+
return
94+
}
95+
96+
if (result.data.success) {
97+
void messageApi.success(t('data-importer.execution.start-import.success'))
98+
// Immediately show progress bar at 0, resetting any previous run's values
99+
setOptimisticProgress({ processedItems: 0, totalItems: progressData?.totalItems ?? 0, progress: 0 })
100+
setOptimisticRunning(true)
101+
setHasCompleted(false)
102+
} else {
103+
void messageApi.error(t('data-importer.execution.start-import.error'))
104+
}
105+
void refetchProgress()
106+
}
107+
108+
const handleCancelExecution = async (): Promise<void> => {
109+
const result = await cancelExecution({ name: configName })
110+
111+
if ('error' in result) {
112+
if (result.error !== undefined) {
113+
trackError(new ApiError(result.error))
114+
}
115+
void messageApi.error(t('data-importer.execution.cancel.error'))
116+
return
117+
}
118+
119+
void messageApi.success(t('data-importer.execution.cancel.success'))
120+
// Immediately hide the progress bar before polling confirms
121+
setOptimisticCancelled(true)
122+
setOptimisticRunning(false)
123+
setOptimisticProgress(null)
124+
setHasCompleted(false)
125+
void refetchProgress()
126+
}
127+
128+
return (
129+
<>
130+
{ /* ── Manual Execution ── */ }
131+
<DataImporterPanel title={ t('data-importer.execution.manual-execution') }>
132+
<ManualExecutionButton
133+
isDirty={ isDirty }
134+
isStarting={ isStarting }
135+
label={ t('data-importer.execution.start-import') }
136+
onStart={ () => { void handleStartImport() } }
137+
/>
138+
</DataImporterPanel>
139+
140+
{ /* ── Execution Status ── */ }
141+
<DataImporterPanel
142+
noWidthLimit
143+
title={ t('data-importer.execution.status.title') }
144+
>
145+
{ isRunning
146+
? (
147+
<>
148+
<p className={ styles.progressLabel }>
149+
{ t('data-importer.execution.status.current-progress') }
150+
</p>
151+
<div className={ styles.progressWrapper }>
152+
<Progress
153+
format={ () => t('data-importer.execution.status.processing', { processedItems, totalItems }) }
154+
percent={ Math.round(progress * 100) }
155+
percentPosition={ { align: 'start', type: 'inner' } }
156+
size={ [-1, 32] }
157+
status="active"
158+
strokeColor={ styles.colorFill }
159+
trailColor={ 'rgba(0, 0, 0, 0.06)' }
160+
/>
161+
</div>
162+
<Button
163+
loading={ isCancelling }
164+
onClick={ () => { void handleCancelExecution() } }
165+
>
166+
{ t('data-importer.execution.status.cancel') }
167+
</Button>
168+
</>
169+
)
170+
: (
171+
<Text>
172+
{ t(hasCompleted
173+
? 'data-importer.execution.status.finished'
174+
: 'data-importer.execution.status.not-running') }
175+
</Text>
176+
) }
177+
</DataImporterPanel>
178+
</>
179+
)
180+
}

assets/studio/js/src/modules/data-importer/data-importer-api-slice-enhanced.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ export const {
9696
useBundleDataImporterConfigGetQuery,
9797
useBundleDataImporterConfigSaveMutation,
9898
useBundleDataImporterConfigHasImportFileUploadedQuery,
99+
useBundleDataImporterConfigCheckImportProgressQuery,
100+
useBundleDataImporterConfigStartImportMutation,
101+
useBundleDataImporterConfigCancelExecutionMutation,
99102
useBundleDataImporterConnectionListQuery
100103
} = api
101104

0 commit comments

Comments
 (0)