Skip to content

Commit 05ea49c

Browse files
authored
[Bug] Render Video editable loading / error states natively in Studio (#3486)
Render the video editable's in-progress/error states natively in Studio (editmode-gated, CoreBundle assets), poll the new asset video-thumbnail-status endpoint and refresh on completion, and preserve unsaved input across that auto-refresh.
1 parent 95b4e7e commit 05ea49c

762 files changed

Lines changed: 1341 additions & 1116 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

assets/build/api/docs.jsonopenapi.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

assets/js/src/core/app/public-api/document/document-api.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface DocumentApi {
2727
triggerValueChange: (documentId: number, key: string, value: any) => void
2828
triggerValueChangeWithReload: (documentId: number, key: string, value: any) => void
2929
triggerSaveAndReload: (documentId: number) => void
30+
reloadIframe: (documentId: number) => void
3031
notifyIframeReady: (documentId: number) => void
3132
notifyAreablockTypes: (documentId: number, editableTypeId: string, areablockTypes: AreablockGroupedTypes) => void
3233
mergeAreablockTypes: (documentId: number, editableTypeId: string, areablockTypes: AreablockGroupedTypes) => void
@@ -40,6 +41,12 @@ export interface DocumentApi {
4041
class DocumentApiImpl implements DocumentApi {
4142
private readonly autoSaveCallbacks = new Map<number, ReturnType<typeof debounce>>()
4243

44+
// Documents the user has edited since they were opened / last reloaded. Tracked here because it
45+
// flips synchronously on the edit event, unlike the redux `modified` flag (set via a deferred
46+
// startTransition) — so a poll-triggered reloadIframe can never race ahead of it and discard
47+
// input that was just typed but not yet mirrored into `modified`.
48+
private readonly editedDocuments = new Set<number>()
49+
4350
markDraftAsModified (documentId: number): void {
4451
const currentState = store.getState()
4552
const document = selectDocumentById(currentState, documentId)
@@ -82,15 +89,18 @@ class DocumentApiImpl implements DocumentApi {
8289
unregisterIframe (documentId: number): void {
8390
iframeDocumentEditorRegistry.unregister(documentId)
8491
this.autoSaveCallbacks.delete(documentId)
92+
this.editedDocuments.delete(documentId)
8593
}
8694

8795
triggerValueChange (documentId: number, key: string, value: any): void {
96+
this.editedDocuments.add(documentId)
8897
this.markDraftAsModified(documentId)
8998

9099
void this.autoSaveCallbacks.get(documentId)?.()
91100
}
92101

93102
triggerValueChangeWithReload (documentId: number, key: string, value: any): void {
103+
this.editedDocuments.add(documentId)
94104
this.markDraftAsModified(documentId)
95105

96106
// Perform immediate auto-save without debounce, then reload
@@ -101,6 +111,26 @@ class DocumentApiImpl implements DocumentApi {
101111
void this.performAutoSaveAndReload(documentId)
102112
}
103113

114+
// Refresh the editor iframe when the rendered editable is stale but the document state did
115+
// not change on its own (e.g. a video thumbnail finished converting).
116+
// If the user has edited this document, flush the current values first so the reload does not
117+
// drop them — once edited, the edit-lock gate is (or is about to be) resolved, so that autosave
118+
// resolves rather than hanging, and saveDocument reads the live editable values at save time.
119+
// Otherwise reload directly, bypassing the autosave path (whose edit-lock gate holds autosaves
120+
// until the first edit and would otherwise hang this poll-triggered refresh).
121+
reloadIframe (documentId: number): void {
122+
if (this.editedDocuments.has(documentId)) {
123+
void this.performAutoSaveAndReload(documentId)
124+
125+
return
126+
}
127+
128+
const iframeRef = iframeDocumentEditorRegistry.getIframeRef(documentId)
129+
if (!isNil(iframeRef?.current)) {
130+
iframeRef.current.reload()
131+
}
132+
}
133+
104134
notifyIframeReady (documentId: number): void {
105135
iframeDocumentEditorRegistry.markAsReady(documentId)
106136
}
@@ -163,6 +193,11 @@ class DocumentApiImpl implements DocumentApi {
163193

164194
await documentSaveService.saveDocument(documentId, SaveTaskType.AutoSave)
165195

196+
// Saved state is now the clean baseline, so drop the edited flag: a later poll-triggered
197+
// refresh of this freshly reloaded (untouched) document then takes the cheap reload-only
198+
// path instead of a redundant save-and-reload.
199+
this.editedDocuments.delete(documentId)
200+
166201
if (!isNil(iframeRef?.current)) {
167202
iframeRef.current.reload()
168203
}

assets/js/src/core/modules/asset/asset-api-slice-enhanced.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ export const {
130130
useAssetGetGridConfigurationByFolderIdQuery,
131131
useAssetGetAvailableGridColumnsQuery,
132132
useAssetPatchFolderByIdMutation,
133-
useAssetUploadInfoQuery
133+
useAssetUploadInfoQuery,
134+
useAssetVideoThumbnailStatusQuery
134135
} = api
135136

136137
export { api }

assets/js/src/core/modules/asset/asset-api-slice.gen.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ const injectedRtkApi = api
66
})
77
.injectEndpoints({
88
endpoints: (build) => ({
9+
assetVideoThumbnailStatus: build.query<
10+
AssetVideoThumbnailStatusApiResponse,
11+
AssetVideoThumbnailStatusApiArg
12+
>({
13+
query: (queryArg) => ({
14+
url: `/pimcore-studio/api/assets/${queryArg.id}/video/thumbnail/${queryArg.thumbnailName}/status`,
15+
}),
16+
providesTags: ["Assets"],
17+
}),
918
assetGetTypes: build.query<AssetGetTypesApiResponse, AssetGetTypesApiArg>({
1019
query: () => ({ url: `/pimcore-studio/api/assets/types` }),
1120
providesTags: ["Assets"],
@@ -457,6 +466,14 @@ const injectedRtkApi = api
457466
overrideExisting: false,
458467
});
459468
export { injectedRtkApi as api };
469+
export type AssetVideoThumbnailStatusApiResponse =
470+
/** status 200 Conversion status of the video thumbnail */ VideoThumbnailStatus;
471+
export type AssetVideoThumbnailStatusApiArg = {
472+
/** Id of the video */
473+
id: number;
474+
/** Find asset by matching thumbnail name. */
475+
thumbnailName: string;
476+
};
460477
export type AssetGetTypesApiResponse = /** status 200 Successfully retrieved all available asset types */ {
461478
totalItems: number;
462479
items: AssetType[];
@@ -1007,13 +1024,13 @@ export type AssetCustomMetadataGetByIdApiArg = {
10071024
/** Id of the asset */
10081025
id: number;
10091026
};
1010-
export type AssetType = {
1027+
export type VideoThumbnailStatus = {
10111028
/** AdditionalAttributes */
10121029
additionalAttributes?: {
10131030
[key: string]: string | number | boolean | object;
10141031
};
1015-
/** key */
1016-
key: string;
1032+
/** Conversion status of the requested video thumbnail. */
1033+
status: "finished" | "inprogress" | "error" | "not_started";
10171034
};
10181035
export type Error = {
10191036
/** Message */
@@ -1025,6 +1042,14 @@ export type DevError = {
10251042
/** Details */
10261043
details: string;
10271044
};
1045+
export type AssetType = {
1046+
/** AdditionalAttributes */
1047+
additionalAttributes?: {
1048+
[key: string]: string | number | boolean | object;
1049+
};
1050+
/** key */
1051+
key: string;
1052+
};
10281053
export type FixedCustomSettings = {
10291054
/** embedded meta data of the asset - array of any key-value pairs */
10301055
embeddedMetadata: object[];
@@ -1395,6 +1420,7 @@ export type CustomMetadata = {
13951420
data: any | null;
13961421
};
13971422
export const {
1423+
useAssetVideoThumbnailStatusQuery,
13981424
useAssetGetTypesQuery,
13991425
useAssetBatchDeleteMutation,
14001426
useAssetCloneMutation,

assets/js/src/core/modules/document/editor/shared-tab-manager/tabs/edit/hooks/use-document-editor.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface DocumentEditorContextProps {
1717
updateValue: (key: string, value: ValueType) => void
1818
updateValueWithReload: (key: string, value: ValueType) => void
1919
triggerSaveAndReload: () => void
20+
reloadIframe: () => void
2021
getValues: () => Record<string, ValueType>
2122
getValue: (key: string) => ValueType
2223
initializeData: (data: Record<string, ValueType>) => void
@@ -109,6 +110,15 @@ export const useDocumentEditor = (): DocumentEditorContextProps => {
109110
}
110111
}, [id])
111112

113+
const reloadIframe = useCallback((): void => {
114+
try {
115+
const { document: documentApi } = getPimcoreStudioApi()
116+
documentApi.reloadIframe(id)
117+
} catch (error) {
118+
console.warn('Could not reload document iframe:', error)
119+
}
120+
}, [id])
121+
112122
const notifyReady = useCallback((): void => {
113123
if (!readyNotified.current) {
114124
try {
@@ -125,6 +135,7 @@ export const useDocumentEditor = (): DocumentEditorContextProps => {
125135
updateValue,
126136
updateValueWithReload,
127137
triggerSaveAndReload,
138+
reloadIframe,
128139
getValues,
129140
getValue,
130141
initializeData,

assets/js/src/core/modules/element/dynamic-types/definitions/document/editable/components/video-editable/video-editable.styles.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,37 @@ export const useStyles = createStyles(({ token, css }) => {
2626
border: 1px solid ${token.colorBorder};
2727
border-radius: ${token.borderRadius}px;
2828
box-shadow: ${token.boxShadow};
29+
`,
30+
31+
progressOverlay: css`
32+
position: absolute;
33+
inset: 0;
34+
display: flex;
35+
flex-direction: column;
36+
align-items: center;
37+
justify-content: center;
38+
gap: ${token.marginSM}px;
39+
background-color: ${token.colorBgContainer};
40+
border: 2px dashed ${token.colorBorder};
41+
border-radius: ${token.borderRadius}px;
42+
pointer-events: none;
43+
`,
44+
45+
progressLabel: css`
46+
color: ${token.colorTextSecondary};
47+
font-size: ${token.fontSizeSM}px;
48+
`,
49+
50+
errorMessage: css`
51+
position: absolute;
52+
inset-inline: ${token.paddingXS}px;
53+
bottom: ${token.paddingXS}px;
54+
text-align: center;
55+
color: ${token.colorTextSecondary};
56+
font-size: ${token.fontSizeSM}px;
57+
overflow: hidden;
58+
text-overflow: ellipsis;
59+
white-space: nowrap;
2960
`
3061
}
3162
})

0 commit comments

Comments
 (0)