OfficeDev / office-js

A repo and NPM package for Office.js, corresponding to a copy of what gets published to the official "evergreen" Office.js CDN, at https://appsforoffice.microsoft.com/lib/1/hosted/office.js.
https://learn.microsoft.com/javascript/api/overview
Other
678 stars 95 forks source link

Powerpoint: get shapes, add an images and get shapes again can make context.sync() run indefinitely #5022

Open keienla opened 2 days ago

keienla commented 2 days ago

I'm trying to replace some images in a slide with their name & a new image in base64. To do it I get all the shapes, filter the ones with the name corresponding to an id given as parameter, create an image at the same position, remove old shape and set to the one created the id as name to find it again if wanted.

But after the new image is created, all context.sync() can run indefinitely, and all function as XXX.load('') may not work and throw an error as the desired element are not loaded. It's hard to explain more as this bug seems to be random and does not appear all the time.

Environment:

Expected behavior

All the context.sync() after the insertion of images should run as the context.sync() before the insertion of images. So just by loading correctly the desired elements and do not last an infinite amount of time.

Current behavior

Most of the time the function never end because some context.sync() never end. It always happen after the creation of the images, when I reload all shapes to set the desired name to the new added shapes. The problems come from this point.

My functions with in comment the moment of problems

/*
* Function to add the image in the slide    
* https://learn.microsoft.com/en-us/office/dev/add-ins/develop/read-and-write-data-to-the-active-selection-in-a-document-or-spreadsheet
* @param {string} image The string image code to create
* @param {{left:number,top:number}} position The position of the image
* @returns {Promise<boolean>}
*/
async function importImage(
    image: string,
    position: {
        left: number,
        top: number
    },
) {
    return new Promise((resolve, reject) => {
        Office.context.document.setSelectedDataAsync(
            image,
            {
                coercionType: Office.CoercionType.Image,
                imageLeft: position.left,
                imageTop: position.top,
            },
            (result) => {
                if (result.status === Office.AsyncResultStatus.Failed) {
                    return reject(result.error.message)
                }

                return resolve(true)
            })
    })
}

/**
* Function to replace the image with id given id
* @param {string} uuid The id of the image to replace. If no shape with this name, the image will not be created
* @param {string} image The code of the image
* @returns {Promise<boolean>}
*/
async function replaceImages(datas: {
    uuid: string,
    image: string
}[]): Promise<boolean> {
    if (!Office.context.document) throw new Error('Can\'t get context of Office Document')

    return PowerPoint.run(async (context) => {
        // Get the current slide
        let slides = context.presentation.getSelectedSlides()
        let currentSlide = slides.getItemAt(0)
        currentSlide.load('shapes, shapes/name')
        await context.sync()

        // Get the shapes to update
        const shapes = currentSlide.shapes.items.filter(shape => {
            return datas.findIndex(data => data.uuid === shape.name) > -1
        })

        if(!shapes?.length) return Promise.resolve(false)

        // Load position of the shapes to replace
        shapes.forEach(shape => {
            shape.load('left, top')
        })
        await context.sync()

        // Create the new images and remove the old one
        const uuidsReplaced: string[] = []
        for(const shape of shapes) {
            const data = datas.find(data => data.uuid === shape.name)!
            uuidsReplaced.push(data.uuid)
            await importImage(data.image, {left: shape.left, top: shape.top })
            shape.delete()
        }

        // The new shape is Added and old one deleted, but the name in the new image is not set yet to find it again if wanted
        // ! Problem from here. At this point, each context.sync() can last forever of badly load requirement and throw an error
        await context.sync()

        // get again all shapes
        slides = context.presentation.getSelectedSlides()
        currentSlide = slides.getItemAt(0)
        currentSlide.load('shapes, shapes/name')
        await context.sync()

        // The Added images are the {shapes.length} last shapes in collection
        const shapeCollection = [...currentSlide.shapes.items]
        const newShapes = shapeCollection.slice(shapeCollection.length - shapes.length)
        // Set the name to get it again if I want
        newShapes.forEach((newShape, index) => {
            newShape.name = uuidsReplaced[index]
        })

        await context.sync()

        return Promise.resolve(true)
    }).catch((error) => {
        console.error(error)
        return Promise.resolve(false)
    })
}

I had better result by adding a timer of 1-2 seconds between the shape.delete() and the next await context.sync(). But sometimes it still struggling and never finish.

jonahkarpman commented 2 days ago

Thank you for reporting and providing detailed information about this issue! I've assigned this issue to our SME for further investigation.

jonahkarpman commented 1 day ago

Created bug (9495764) for tracking.