Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a878a8b
[Bug] Render Video editable loading / error states natively in Studio
markus-moser May 5, 2026
6dfce83
Automatic frontend build
markus-moser May 5, 2026
41ac578
Refactor VideoEditable: dedupe edit overlay, simplify marker capture,…
markus-moser Jun 9, 2026
bc2d9dc
Automatic frontend build
markus-moser Jun 9, 2026
40763ab
Automatic frontend build
markus-moser Jun 9, 2026
1fcc63c
Surface error message, guard inline thumbnail configs and add missing…
markus-moser Jun 12, 2026
3c656d2
Automatic frontend build
markus-moser Jun 12, 2026
d71ded5
Generate the video thumbnail status endpoint from the schema
markus-moser Jun 22, 2026
6f6a866
Automatic frontend build
markus-moser Jun 22, 2026
b3d7d0e
Merge 2025.4 into 3407-fix-video-editable-rendering
markus-moser Jun 22, 2026
4be7412
Automatic frontend build
markus-moser Jun 22, 2026
7f12ec4
Refresh the video editable via reload-only instead of save-and-reload
markus-moser Jun 22, 2026
76cf272
Automatic frontend build
markus-moser Jun 22, 2026
4b6906c
Preserve unsaved changes when the video editable auto-refreshes
markus-moser Jun 22, 2026
3bea8e3
Automatic frontend build
markus-moser Jun 22, 2026
9b6d46d
Track edited documents synchronously to avoid dropping input on auto-…
markus-moser Jun 24, 2026
7e6dc3d
Automatic frontend build
markus-moser Jun 24, 2026
94bf9d9
Merge branch '2025.4' into 3407-fix-video-editable-rendering
markus-moser Jun 24, 2026
4be6fe5
Automatic frontend build
markus-moser Jun 24, 2026
db7c593
Correct edited-flag comment: iframe reload does not re-arm the edit-l…
markus-moser Jun 24, 2026
331f314
Automatic frontend build
markus-moser Jun 24, 2026
9939003
Use existing all-language 'edit' key for the video edit-button tooltip
markus-moser Jun 24, 2026
e672285
Automatic frontend build
markus-moser Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion assets/build/api/docs.jsonopenapi.json

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions assets/js/src/core/app/public-api/document/document-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface DocumentApi {
triggerValueChange: (documentId: number, key: string, value: any) => void
triggerValueChangeWithReload: (documentId: number, key: string, value: any) => void
triggerSaveAndReload: (documentId: number) => void
reloadIframe: (documentId: number) => void
notifyIframeReady: (documentId: number) => void
notifyAreablockTypes: (documentId: number, editableTypeId: string, areablockTypes: AreablockGroupedTypes) => void
mergeAreablockTypes: (documentId: number, editableTypeId: string, areablockTypes: AreablockGroupedTypes) => void
Expand All @@ -40,6 +41,12 @@ export interface DocumentApi {
class DocumentApiImpl implements DocumentApi {
private readonly autoSaveCallbacks = new Map<number, ReturnType<typeof debounce>>()

// Documents the user has edited since they were opened / last reloaded. Tracked here because it
// flips synchronously on the edit event, unlike the redux `modified` flag (set via a deferred
// startTransition) — so a poll-triggered reloadIframe can never race ahead of it and discard
// input that was just typed but not yet mirrored into `modified`.
private readonly editedDocuments = new Set<number>()

markDraftAsModified (documentId: number): void {
const currentState = store.getState()
const document = selectDocumentById(currentState, documentId)
Expand Down Expand Up @@ -82,15 +89,18 @@ class DocumentApiImpl implements DocumentApi {
unregisterIframe (documentId: number): void {
iframeDocumentEditorRegistry.unregister(documentId)
this.autoSaveCallbacks.delete(documentId)
this.editedDocuments.delete(documentId)
}

triggerValueChange (documentId: number, key: string, value: any): void {
this.editedDocuments.add(documentId)
this.markDraftAsModified(documentId)

void this.autoSaveCallbacks.get(documentId)?.()
}

triggerValueChangeWithReload (documentId: number, key: string, value: any): void {
this.editedDocuments.add(documentId)
this.markDraftAsModified(documentId)

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

// Refresh the editor iframe when the rendered editable is stale but the document state did
// not change on its own (e.g. a video thumbnail finished converting).
// If the user has edited this document, flush the current values first so the reload does not
// drop them — once edited, the edit-lock gate is (or is about to be) resolved, so that autosave
// resolves rather than hanging, and saveDocument reads the live editable values at save time.
// Otherwise reload directly, bypassing the autosave path (whose edit-lock gate holds autosaves
// until the first edit and would otherwise hang this poll-triggered refresh).
reloadIframe (documentId: number): void {
if (this.editedDocuments.has(documentId)) {
void this.performAutoSaveAndReload(documentId)

return
}

const iframeRef = iframeDocumentEditorRegistry.getIframeRef(documentId)
if (!isNil(iframeRef?.current)) {
iframeRef.current.reload()
}
}

notifyIframeReady (documentId: number): void {
iframeDocumentEditorRegistry.markAsReady(documentId)
}
Expand Down Expand Up @@ -163,6 +193,11 @@ class DocumentApiImpl implements DocumentApi {

await documentSaveService.saveDocument(documentId, SaveTaskType.AutoSave)

// Saved state is now the clean baseline, so drop the edited flag: a later poll-triggered
// refresh of this freshly reloaded (untouched) document then takes the cheap reload-only
// path instead of a redundant save-and-reload.
this.editedDocuments.delete(documentId)

if (!isNil(iframeRef?.current)) {
iframeRef.current.reload()
}
Expand Down
3 changes: 2 additions & 1 deletion assets/js/src/core/modules/asset/asset-api-slice-enhanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ export const {
useAssetGetGridConfigurationByFolderIdQuery,
useAssetGetAvailableGridColumnsQuery,
useAssetPatchFolderByIdMutation,
useAssetUploadInfoQuery
useAssetUploadInfoQuery,
useAssetVideoThumbnailStatusQuery
} = api

export { api }
32 changes: 29 additions & 3 deletions assets/js/src/core/modules/asset/asset-api-slice.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ const injectedRtkApi = api
})
.injectEndpoints({
endpoints: (build) => ({
assetVideoThumbnailStatus: build.query<
AssetVideoThumbnailStatusApiResponse,
AssetVideoThumbnailStatusApiArg
>({
query: (queryArg) => ({
url: `/pimcore-studio/api/assets/${queryArg.id}/video/thumbnail/${queryArg.thumbnailName}/status`,
}),
providesTags: ["Assets"],
}),
assetGetTypes: build.query<AssetGetTypesApiResponse, AssetGetTypesApiArg>({
query: () => ({ url: `/pimcore-studio/api/assets/types` }),
providesTags: ["Assets"],
Expand Down Expand Up @@ -457,6 +466,14 @@ const injectedRtkApi = api
overrideExisting: false,
});
export { injectedRtkApi as api };
export type AssetVideoThumbnailStatusApiResponse =
/** status 200 Conversion status of the video thumbnail */ VideoThumbnailStatus;
export type AssetVideoThumbnailStatusApiArg = {
/** Id of the video */
id: number;
/** Find asset by matching thumbnail name. */
thumbnailName: string;
};
export type AssetGetTypesApiResponse = /** status 200 Successfully retrieved all available asset types */ {
totalItems: number;
items: AssetType[];
Expand Down Expand Up @@ -1007,13 +1024,13 @@ export type AssetCustomMetadataGetByIdApiArg = {
/** Id of the asset */
id: number;
};
export type AssetType = {
export type VideoThumbnailStatus = {
/** AdditionalAttributes */
additionalAttributes?: {
[key: string]: string | number | boolean | object;
};
/** key */
key: string;
/** Conversion status of the requested video thumbnail. */
status: "finished" | "inprogress" | "error" | "not_started";
};
export type Error = {
/** Message */
Expand All @@ -1025,6 +1042,14 @@ export type DevError = {
/** Details */
details: string;
};
export type AssetType = {
/** AdditionalAttributes */
additionalAttributes?: {
[key: string]: string | number | boolean | object;
};
/** key */
key: string;
};
export type FixedCustomSettings = {
/** embedded meta data of the asset - array of any key-value pairs */
embeddedMetadata: object[];
Expand Down Expand Up @@ -1395,6 +1420,7 @@ export type CustomMetadata = {
data: any | null;
};
export const {
useAssetVideoThumbnailStatusQuery,
useAssetGetTypesQuery,
useAssetBatchDeleteMutation,
useAssetCloneMutation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface DocumentEditorContextProps {
updateValue: (key: string, value: ValueType) => void
updateValueWithReload: (key: string, value: ValueType) => void
triggerSaveAndReload: () => void
reloadIframe: () => void
getValues: () => Record<string, ValueType>
getValue: (key: string) => ValueType
initializeData: (data: Record<string, ValueType>) => void
Expand Down Expand Up @@ -109,6 +110,15 @@ export const useDocumentEditor = (): DocumentEditorContextProps => {
}
}, [id])

const reloadIframe = useCallback((): void => {
try {
const { document: documentApi } = getPimcoreStudioApi()
documentApi.reloadIframe(id)
} catch (error) {
console.warn('Could not reload document iframe:', error)
}
}, [id])

const notifyReady = useCallback((): void => {
if (!readyNotified.current) {
try {
Expand All @@ -125,6 +135,7 @@ export const useDocumentEditor = (): DocumentEditorContextProps => {
updateValue,
updateValueWithReload,
triggerSaveAndReload,
reloadIframe,
getValues,
getValue,
initializeData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,37 @@ export const useStyles = createStyles(({ token, css }) => {
border: 1px solid ${token.colorBorder};
border-radius: ${token.borderRadius}px;
box-shadow: ${token.boxShadow};
`,

progressOverlay: css`
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: ${token.marginSM}px;
background-color: ${token.colorBgContainer};
border: 2px dashed ${token.colorBorder};
border-radius: ${token.borderRadius}px;
pointer-events: none;
`,

progressLabel: css`
color: ${token.colorTextSecondary};
font-size: ${token.fontSizeSM}px;
`,

errorMessage: css`
position: absolute;
inset-inline: ${token.paddingXS}px;
bottom: ${token.paddingXS}px;
text-align: center;
color: ${token.colorTextSecondary};
font-size: ${token.fontSizeSM}px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
}
})
Loading
Loading