When scrolling a page containing my app, it calls the component multiple times and eventually crashes

I am currently facing two issues while using Forge and I would like to get ideas on how to solve these problems.

  1. When I scroll a page (containing my app) the component is called multiple times and eventually the app crashes.

  2. Also, when I try to call the get user endpoint asynchronously in one of my resolvers, it returns:

INFO    18:48:17.817  70c53fbca7f2799f  { code: 401, message: 'Unauthorized; scope does not match' }

Sometimes it returns the correct json response of the user details, but most times it returns ‘unauthorized’. I have all the scopes in place (read:confluence-user). But still, I get this behavior. Please, is there something I am missing or doing wrong? Here is the code block:

const resolver = new Resolver();
resolver.define('getText', async ({ payload, context }) => {
  const response = await fetchSpacePages();
  const finalPromise = response.reduce((accumulator, page) => {
    if (page.metadata.properties[PAGE_META_DATA_KEY] === undefined) {
      return [];
    }
    const rows = page.metadata.properties[PAGE_META_DATA_KEY].value.map(
      async (meta) => {
        const user = await getUser(meta.uid);
        console.log(user);
        const row = {
          key: meta.id,
          title: page.title,
          creator: undefined,
          date: meta.created,
          user: user,
        };
        return row;
      }
    );
    return accumulator.concat(rows);
  }, []);
  const data = await Promise.all(finalPromise);
  return data;
});

Get User:

const getUser = async (accountId) => {
  const res = await api
    .asUser()
    .requestConfluence(/rest/api/user?accountId=${accountId});
  const data = await res.json();
  return data;
};

I think your map(async.... thing is the problem. Your promise.all does not wait for those Promises. They are “loosely” flying around and.
You should optimize the code so that there are awaits for all async calls.

Okay thanks, @clouless will re-write that block

Hi @clouless, I have re-written the block but the issues are still there. I am using a for-loop now and as I understand, the execution of the code will be in series (please, correct if I am wrong). Here is the new code block:

const getRows = async (context) => {
  const page = await fetchPage(context.contentId);
  let rows = [];
  const metadatas = page.metadata.properties[PAGE_META_DATA_KEY].value;
  for (let i = 0; i < metadatas.length; i++) {
    const meta = metadatas[i];
    const user = await getUser(meta.uid);
    console.log(user);
    const row = {
      key: meta.id,
      title: page.title,
      creator: undefined,
      date: meta.created,
      user: user,
    };
    rows.push(row);
  }
  return rows;
};

const App = () => {
  const context = useProductContext();
  const [data] = useState(async () => await getRows(context));

Hi, looks much better. I am not quite sure if you can use async in useState.
I would do it like so:

  const [data] = useState(() => getRows(context)); // the doc says, "a function that returns a promise"

Or via useEffect

  const [data, setData] = useState([]); 
  useEffect(async () => {
    const rows = await getRows(context);
    setData(rows);
  });

But at best ask some Atlassian staff member why your stuff is executed multiple times. Maybe it is a glitch in the forge infrastructure. I wish you good luck in finding the solution :slight_smile: I am out of guesses myself :frowning:

2 Likes

@kkercz @rmassaioli @HeyJoe @nhur

Hi, Atlassian team,

Please, any update on this issue?

Hi @OnucheIdoko1,

Sorry for the delay in replying! Thanks for asking your question and letting us know about this issue.

Are you writing a Custom UI or UI kit extension?

I see that you are using a Resolver, which is a Custom UI only API. Your usage of the resolver seems like it should work. However, in a follow up message, it looks like you are using useProductContext(), which is a UI kit only API. Perhaps attempting to mix the two is the source of the issue.

Is it possible for you to share more information about the app to help me understand the issue you’re facing, so I can find a solution for you? The full code for the app (or at least a minimal reproducible case, if you’d prefer to keep your business logic private) would be helpful, as well as the manifest.yml file (you can redact any personal or private information), so I can determine the module type that you are using, as well as whether you are using UI kit or Custom UI.

Hello @kchan,

I am writing a UI Kit extension for this task. I am not using Resolver for this at all. I have other parts in my code that uses Resolver plus Custom UI, but this particular is a UI Kit extension.

It is using a “Confluence content byline item”. The component returns a “” and all components within the “” are UI Kit extensions.

So this is the app component that triggers the inline dialog:This text will be hidden

import ForgeUI, {
  render,
  Text,
  ContentBylineItem,
  InlineDialog,
  useState,
  useEffect,
  Table,
  Head,
  Row,
  Cell,
  useProductContext,
  SectionMessage,
  ButtonSet,
  Button,
} from '@forge/ui';

const fetchPage = async (contentId) => {
  const res = await api
    .asUser()
    .requestConfluence(
      `/rest/api/content/${contentId}?expand=metadata.properties.${PAGE_META_DATA_KEY}`
    );

  const data = await res.json();

  return data;
};

const getUser = async (accountId) => {
  const res = await api
    .asUser()
    .requestConfluence(`/rest/api/user?accountId=${accountId}`);

  const data = await res.json();

  return data;
};

const getRows = async (context) => {
  let rows = [];
  const page = await fetchPage(context.contentId);
  const metadatas = page.metadata.properties[PAGE_META_DATA_KEY].value;
  for (let i = 0; i < metadatas.length; i++) {
    const meta = metadatas[i];
    const user = await getUser(meta.uid);
    const row = {
      key: meta.id,
      title: page.title,
      creator: undefined,
      date: meta.created,
      user: user,
    };
    rows.push(row);
  }
  return rows;
};

const App = () => {
  const context = useProductContext();
  const [data, setData] = useState([]);

  useEffect(async () => {
    const rows = await getRows(context);
    setData(rows);
  }, []);

  return (
    <InlineDialog>
      <SectionMessage title='XXXXXXXXXXXX' appearance='info'>
        <Text>
          All XXXXXXXXXXXX taken from this page.{' '}
          {data.length == 0 || undefined === undefined
            ? 'No XXXXXXXXXXXX have been taken for this page. To take a XXXXXXXXXXXX go to ....'
            : ''}
        </Text>
      </SectionMessage>
      <Table>
        <Head>
          <Cell>
            <Text content='Taken By' />
          </Cell>
          <Cell>
            <Text content='Created Date' />
          </Cell>
          <Cell>
            <Text content='Actions' />
          </Cell>
        </Head>
        {data.map((page) => (
          <Row>
            <Cell>
              <Text content={page.user.displayName} />
            </Cell>
            <Cell>
              <Text content={page.date} />
            </Cell>
            <Cell>
              <ButtonSet>
                <Button text='View' />
              </ButtonSet>
            </Cell>
          </Row>
        ))}
      </Table>
    </InlineDialog>
  );
};

export const runViewXXXXXXXXXXXXInLineAction = render(
  <ContentBylineItem>
    <App />
  </ContentBylineItem>
);

And this is my manifest.xml:

modules:
  confluence:contentAction:
    - key: create-XXXXXXXXXXXX-action
      function: create-page-XXXXXXXXXXXX
      title: Take XXXXXXXXXXXX
      description: Create XXXXXXXXXXXX action at current timestamp
  confluence:spacePage:
    - key: XXXXXXXXXXXX-home-page
      resource: app
      resolver:
        function: XXXXXXXXXXXX-app-resolver
      title: Softcomply XXXXXXXXXXXX
      description: Manage all page or space XXXXXXXXXXXX
      route: softcomply-XXXXXXXXXXXX
      icon: https://XXXXXXXXXXXX/images/pluginLogoBlue.png
  confluence:contentBylineItem:
    - key: view-page-XXXXXXXXXXXX-inline-action
      function: view-page-XXXXXXXXXXXX
      title: View Page XXXXXXXXXXXX
      description: View list of XXXXXXXXXXXX created for the page
      viewportSize: large
  macro:
    - key: view-page-XXXXXXXXXXXX-macro
      resource: view-page-macro
      resolver:
        function: XXXXXXXXXXXX-app-resolver
      title: View Page XXXXXXXXXXXX Macro
      description:
        To view page XXXXXXXXXXXX, please, select a XXXXXXXXXXXX to be displayed on the page. The selected XXXXXXXXXXXX
        will be the default XXXXXXXXXXXX.
      config:
        function: view-XXXXXXXXXXXX-config
      viewportSize: xlarge
  function:
    - key: page-XXXXXXXXXXXX-view
      handler: index.run
    - key: create-page-XXXXXXXXXXXX
      handler: create.runXXXXXXXXXXXXAction
    - key: view-page-XXXXXXXXXXXX
      handler: viewInline.runViewXXXXXXXXXXXXInLineAction
    - key: XXXXXXXXXXXX-app-resolver
      handler: view.handler
    - key: view-XXXXXXXXXXXX-config
      handler: config.open
resources:
  - key: app
    path: static/custom-ui/packages/XXXXXXXXXXXX-page/build
  - key: view-page-macro
    path: static/custom-ui/packages/view-page-XXXXXXXXXXXX-macro/build
app:
  id: ari:cloud:ecosystem::app/15802ba8-c2cc-47a5-9d00-44044248648e
  name: XXXXXXXXXXXX
permissions:
  scopes:
    - read:confluence-content.summary
    - read:confluence-content.all
    - storage:app
    - write:confluence-content
    - read:confluence-props
    - write:confluence-props
    - read:confluence-user

Hi @OnucheIdoko1,

Sorry for the delay. I have been very busy over the last few days. I will get to this soon!

Hi @kchan,

Okay. Thank you

Hey @OnucheIdoko1,

I’ve been able to reproduce the issue and we are working towards a fix. Hopefully will be able to have a fix in a couple days.

Regarding your get user 401 issues, I am also able to reproduce this and will do some investigation as to why this is happening.

Hello @kchan,

Thank you.

Hi @OnucheIdoko1,

We have fixed the re-rendering UI Kit Byline issue.

We have also fixed the get user 401 issues, and are doing further investigation into a longer term fix.

Thanks for raising these issues, please feel free to let us know if you encounter any other problems!

Hi @kchan,

Thank you very much