Frame
component is now available as part of Forge’s Early Access Program (EAP). To start using this EAP, sign up using the Forge EAP form.
Forge’s EAP offers experimental features to selected users for testing and feedback purposes. These features are unsupported and not recommended for use in production environments. They are also subject to change without notice.
For more details , see Forge EAP, Preview, and GA.
The app in this example showcases a logo designer and renders a preview of the designed icon within the Frame component.
The result will look like this:
This example illustrates the following:
Frame
component into a standard UI Kit app facilitates the embedding of standard static web app resources within the UI Kit Apps. This integration also unlocks features and flexibilities that are currently exclusive to Custom UI, but not available in UI Kit.If you're interested in examining the complete implementation of this app, you can clone or fork it from this repository: forge-ui-kit-frame-project-logo-designer
This tutorial assumes you're already familiar with the basics of Forge development. If this is your first time using Forge, see Getting started first.
Using your terminal complete the following:
Create your app by running:
1 2forge create
Enter a name for your app (up to 50 characters). For example, logo-designer.
Select the UI Kit category.
Select the Confluence product.
Select the confluence-global-page template.
Change to the app subdirectory to see the app files:
1 2cd logo-designer
Install @forge/react
version 10.2.0
or higher. To install:
npm install --save @forge/react@latest
or
npm install --save @forge/react@^10.2.0
In this example, a static frontend resource for the Frame
component is created using Create React App (CRA). However, other web frontend libraries are also viable options.
Make sure you've installed the most recent versions of the following:
node --version
npm --version
yarn --version
To create the web app resources:
Create the resources
folder to add static frontend app:
1 2mkdir resources cd resources
Create a react app:
1 2yarn create react-app project-logo-display
Setup CRA based react app to be used as Forge App based resources as described in here. Specifically, set "homepage": "./"
in the resources/project-logo-display/package.json
file:
1 2{ "name": "project-logo-display", "version": "0.1.0", "private": true, "homepage": "./", .... }
Build assets:
1 2cd project-logo-display yarn build
In the app’s top-level directory, open the manifest.yml
file.
Find the title
entry under confluence:globalPage
, and update the value to Logo Designer
.
Create a new resource with key equals to logo-display
under the resources
section, and set the path pointing to the build folder of the target Frame component app as described in the previous step (i.e., resources/project-logo-display/build
).
1 2resources: - key: main path: src/frontend/index.jsx - key: logo-display path: resources/project-logo-display/build
Configure the required app permissions:
unsafe-inline
for CSS to allow manipulating logo stylesYou manifest.yml
file should look like the following:
1 2modules: confluence:globalPage: - key: logo-designer-hello-world-global-page resource: main render: native resolver: function: resolver title: Logo Designer route: logo-designer function: - key: resolver handler: index.handler resources: - key: main path: src/frontend/index.jsx - key: logo-display path: resources/project-logo-display/build permissions: scopes: - storage:app content: styles: - "unsafe-inline" external: images: - "https://imgur.com" - "https://i.imgur.com" app: runtime: name: nodejs18.x id: '<your app id>'
.js
files and add the following code to store the list of logo images:
src/frontend/constants.js
resources/project-logo-display/src/constants.js
Since this list will be utilized in both the UI Kit app and the Frame
component, we will be duplicating files containing the constant that need to be generated in both the UI Kit app (src/frontend/constants.js
) and the Frame component (resources/project-logo-display/src/constants.js
).
1 2export const fruitSelectionMap = [ { name: "Avocado", url: "https://imgur.com/CIsX0ZO.png" }, { name: "Coconut", url: "https://imgur.com/lRHQMS9.png" }, { name: "Pear", url: "https://imgur.com/YwiIGQA.png" }, { name: "Red Apple", url: "https://imgur.com/wduDSGD.png" }, { name: "Orange", url: "https://imgur.com/lSyCbHI.png" }, { name: "Pomegranate", url: "https://imgur.com/aOHoUbj.png" }, { name: "Peach", url: "https://imgur.com/1rBCiC3.png" }, { name: "Lemon", url: "https://imgur.com/T1s8Pts.png" }, ];
Create the components in the UI Kit components folder (src/frontend/components
), which includes:
src/frontend/components/BorderRadiusSlider.jsx
: To adjust the border radius of the logo container.
1 2import React from "react"; import { Stack, Text, Range } from "@forge/react"; export const BorderRadiusSlider = ({ value, onChange }) => { return ( <Stack space="space.200"> <Text>Radius</Text> <Range value={value} min={0} max={100} onChange={onChange} /> </Stack> ); };
src/frontend/components/FruitIcon.jsx
: To represent individual icons.
1 2import React, { useCallback } from "react"; import { Stack, Radio, Image } from "@forge/react"; export const FruitIcon = ({ name, url, isChecked, onChange }) => { const handleChange = useCallback((event) => { if (onChange) { onChange(event.target.value); } }, []); return ( <Stack alignInline="center"> <Image src={url} alt={name} size="xlarge" /> <Radio name="icon" value={name} onChange={handleChange} isChecked={isChecked} /> </Stack> ); };
src/frontend/components/IconSelection.jsx
: To capture logo icon selection.
1 2import React from "react"; import { Stack, Text, Inline } from "@forge/react"; import { FruitIcon } from "./FruitIcon"; import { fruitSelectionMap } from "../constants"; export const IconSelection = ({ value, onChange }) => { return ( <Stack space="space.200"> <Text>Select an icon</Text> <Inline shouldWrap> {fruitSelectionMap.map(({ name, url }) => ( <FruitIcon key={name} name={name} url={url} isChecked={name === value} onChange={onChange} /> ))} </Inline> </Stack> ); };
Update the src/frontend/index.jsx
file to connect the basic frontend components described above for the initial draft on the UI Kit side.
1 2import React from "react"; import ForgeReconciler, { Inline, Stack, Heading, Frame } from "@forge/react"; import { IconSelection } from "./components/IconSelection"; import { BorderRadiusSlider } from "./components/BorderRadiusSlider"; const App = () => { const [config, setConfig] = React.useState({ icon: "Avocado", radius: 20 }); // TODO: setup logo update and preview logic const handleConfigChange = ({ key, value }) => { console.log(key, value); setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; return ( <Stack space="space.400"> <Heading as="h1">Project logo designer</Heading> <Inline space="space.400"> <Stack space="space.1000"> <IconSelection value={config.icon} onChange={(value) => handleConfigChange({ key: "icon", value }) } /> <BorderRadiusSlider value={config.radius} onChange={(value) => handleConfigChange({ key: "radius", value }) } /> </Stack> {/* TODO: Setup logo display component */} </Inline> </Stack> ); }; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode> );
Start by navigating to the target Frame
component project folder, which is located at resources/project-logo-display/
1 2cd resources/project-logo-display/
Add the required libraries and dependencies:
1 2yarn add @atlaskit/css-reset \ @atlaskit/button \ @atlaskit/primitives \ @atlaskit/tokens \ @atlaskit/tooltip \ @atlaskit/visually-hidden \ @forge/bridge \ styled-components
Setup and initialise common frameworks and libraries (example, Atlassian Design Tokens, Styled Components) by updating the resources/project-logo-display/src/index.js
file:
1 2import React from "react"; import ReactDOM from "react-dom"; import { createGlobalStyle } from "styled-components"; import { view } from "@forge/bridge"; import { token } from "@atlaskit/tokens"; import App from "./App"; import "@atlaskit/css-reset"; // Enables theming view.theme.enable(); const GlobalStyle = createGlobalStyle` body { background: ${token("elevation.surface")}; } `; ReactDOM.render( <React.StrictMode> <GlobalStyle /> <App /> </React.StrictMode>, document.getElementById("root") );
Go to resources/project-logo-display/src/components
and create the necessary components in the Frame component folder which includes:
resources/project-logo-display/src/components/LogoDisplay.jsx
: Container to preview logo style changes.
1 2import React from "react"; import { fruitSelectionMap } from "../constants"; import styled from "styled-components"; import { token } from "@atlaskit/tokens"; const LogoBackground = styled.div` width: 280px; height: 280px; background-color: ${({ color }) => color ? token(`color.background.accent.${color}.subtler`) : token("color.background.neutral")}; border-radius: ${({ borderRadius }) => borderRadius}px; `; const iconUrls = fruitSelectionMap.reduce((acc, fruit) => { acc[fruit.name] = fruit.url; return acc; }, {}); export const LogoDisplay = ({ icon, radius, color }) => { return ( <LogoBackground borderRadius={radius} color={color}> <img alt="logo-img" src={iconUrls[icon]} style={{ width: "280px", height: "280px", display: "block" }} /> </LogoBackground> ); };
resources/project-logo-display/src/components/ColorPalette.jsx
: input component for capturing logo color selections.
1 2import React from "react"; import { Inline, Pressable, xcss } from "@atlaskit/primitives"; import Tooltip from "@atlaskit/tooltip"; import VisuallyHidden from "@atlaskit/visually-hidden"; const baseStyles = xcss({ borderWidth: "border.width", borderStyle: "solid", borderColor: "color.border", borderRadius: "border.radius", height: "20px", width: "20px", display: "flex", alignItems: "center", justifyContent: "center", }); const borderSelected = xcss({ borderColor: "color.border.bold", }); const colorMap = [ "red", "orange", "yellow", "lime", "green", "teal", "blue", "purple", "magenta", ].reduce((acc, color) => { acc[color] = xcss({ backgroundColor: `color.background.accent.${color}.subtler`, ":hover": { backgroundColor: `color.background.accent.${color}.subtler.hovered`, }, ":active": { backgroundColor: `color.background.accent.${color}.subtler.pressed`, }, }); return acc; }, {}); const ColorButton = ({ color, isSelected, onClick }) => { const pressableStyles = [baseStyles, colorMap[color]]; return ( <Tooltip content={color}> <Pressable interactionName={`color-${color}`} xcss={ isSelected ? [...pressableStyles, borderSelected] : pressableStyles } aria-pressed={isSelected} onClick={onClick} > <VisuallyHidden>{color}</VisuallyHidden> </Pressable> </Tooltip> ); }; export const ColorPalette = ({ value, onChange }) => { return ( <Inline space="space.100"> {Object.keys(colorMap).map((color) => { return ( <ColorButton key={color} color={color} isSelected={color === value} onClick={() => { onChange(color); }} /> ); })} </Inline> ); };
Update the resources/project-logo-display/src/App.js
file to connect the basic frontend components described above for the initial draft on the Frame
component side.
You App.js
file should look like the following:
1 2import React from "react"; import { ColorPalette } from "./components/ColorPalette"; import { LogoDisplay } from "./components/LogoDisplay"; import { Stack } from "@atlaskit/primitives"; function App() { const [config, setConfig] = React.useState({ icon: "Avocado", radius: 20, color: "red", }); const handleConfigChange = ({ key, value }) => { setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; // TODO: setup logic to track log update requests from the UI Kit side. return ( <Stack space="space.200"> <ColorPalette value={config.color} onChange={(value) => handleConfigChange({ key: "color", value }) } /> <LogoDisplay icon={config.icon} radius={config.radius} color={config.color} /> </Stack> ); } export default App;
Now, we are ready to include the Logo Display Frame Component into the main UI Kit app.
Run the following command to ensure the Frame
component frontend resources are properly build and compiled.
1 2yarn build
Ensure the build artefacts are generated in the resources/project-logo-display/build
folder, as this folder is linked to the resources
section in the manifest.yml
file.
In the src/frontend/index.jsx
, update the UI Kit app to include the Frame
component with the Frame
tag with resource prop pointing to the key attribute of the target Frame
resources as specified in the manifest.yml
file.
1 2// ..... const App = () => { const [config, setConfig] = React.useState({ icon: "Avocado", radius: 20 }); // TODO: setup logo update and preview logic const handleConfigChange = ({ key, value }) => { console.log(key, value); setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; return ( <Stack space="space.400"> <Heading as="h1">Project logo designer</Heading> <Inline space="space.400"> <Stack space="space.1000"> <IconSelection value={config.icon} onChange={(value) => handleConfigChange({ key: "icon", value }) } /> <BorderRadiusSlider value={config.radius} onChange={(value) => handleConfigChange({ key: "radius", value }) } /> </Stack> <Frame resource="logo-display" /> </Inline> </Stack> ); }; // .....
You can use the Events API on @forge/bridge
to communicate between the UI Kit (main app) and the Frame component. The communication mechanism is utilised in this example to enable the logo design controls (logo picker, radius slider, and so on) to modify the logo preview component within the Frame
component.
Create the React hooks to abstract the underlying communication implementation. Copy the following hooks.js
implementation into both UI Kit and Frame component sides:
Go to the following files, src/frontend/hooks.js
and resources/project-logo-display/src/hooks.js
and add the following code:
1 2import { useEffect, useState, useCallback } from "react"; import { events } from "@forge/bridge"; const LOGO_CONFIG_UPDATES_EVENT = "LOGO_CONFIG_UPDATES"; export const useLogoConfigUpdates = () => { const [configUpdates, setConfigUpdates] = useState([]); useEffect(() => { const sub = events.on(LOGO_CONFIG_UPDATES_EVENT, (message) => { if ( message && message.updates !== undefined && message.updates.length > 0 ) { setConfigUpdates(message.updates); } }); return () => { sub.then((subscription) => subscription.unsubscribe()); }; }, [setConfigUpdates]); const emitConfigUpdates = useCallback((updates) => { events.emit(LOGO_CONFIG_UPDATES_EVENT, { updates }); }, []); return [configUpdates, emitConfigUpdates]; };
Go to src/frontend/index.jsx
to dispatch radius slider and icon selector changes from the UI Kit side.
1 2import { useLogoConfigUpdates } from "./hooks"; // ..... const App = () => { const [config, setConfig] = React.useState({ icon: "Avocado", radius: 20 }); const [_, emitLogoConfigUpdates] = useLogoConfigUpdates(); React.useEffect(() => { // sending through logo config updates through events API whenever // config object changes is detected. emitLogoConfigUpdates( Object.entries(config).map(([key, value]) => ({ key, value })) ); }, [config]); const handleConfigChange = ({ key, value }) => { setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; return ( <Stack space="space.400"> <Heading as="h1">Project logo designer</Heading> <Inline space="space.400"> <Stack space="space.1000"> <IconSelection value={config.icon} onChange={(value) => handleConfigChange({ key: "icon", value }) } /> <BorderRadiusSlider value={config.radius} onChange={(value) => handleConfigChange({ key: "radius", value }) } /> </Stack> <Frame resource="logo-display" /> </Inline> </Stack> ); }; // .....
Go to resources/project-logo-display/src/App.js
and listen for logo configuration updates, and adjust the logo preview within the Frame
component accordingly.
1 2import React from "react"; import { ColorPalette } from "./components/ColorPalette"; import { LogoDisplay } from "./components/LogoDisplay"; import { Stack } from "@atlaskit/primitives"; import { useLogoConfigUpdates } from "./hooks"; function App() { const [config, setConfig] = React.useState({ icon: "Avocado", radius: 20, color: "red", }); // track logo update requests from the UI Kit side. const [logoConfigUpdates] = useLogoConfigUpdates(); React.useEffect(() => { if (logoConfigUpdates && logoConfigUpdates.length > 0) { // upon receiving the config updates, apply the updates to the current // config state to update the logo preview. setConfig((prevConfig) => { const updatedConfig = { ...prevConfig }; logoConfigUpdates.forEach((update) => { updatedConfig[update.key] = update.value; }); return updatedConfig; }); } }, [logoConfigUpdates, setConfig]); const handleConfigChange = ({ key, value }) => { setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; return ( <Stack space="space.200"> <ColorPalette value={config.color} onChange={(value) => handleConfigChange({ key: "color", value }) } /> <LogoDisplay icon={config.icon} radius={config.radius} color={config.color} /> </Stack> ); } export default App;
Alternatively, we can use the Frame
helpers, createFrame
and frame.onPropsUpdate
, which enable communication based on component properties, similar to React.
To understand more about these helper functions, and the prerequisites needed, see this section.
The full implementation of this step can also be found in the forge-ui-kit-frame-project-logo-designer repo.
We will first use the createFrame
helper from @forge/react
to create the Frame
component with custom properties support in the UI Kit side:
1 2import ForgeReconciler, { createFrame } from "@forge/react"; // Create a custom Frame component in the module level that can be used as standard React component in the app. const LogoDisplayFrame = createFrame("logo-display");
In this case, the custom properties that we will pass into the custom Frame component——LogoDisplayFrame
, are radius
and icon
. LogoDisplayFrame
is bound specifically to the logo-display
resource. The src/frontend/index.jsx
file will look similar to the following:
1 2... import ForgeReconciler, { ... createFrame, } from "@forge/react"; const LogoDisplayFrame = createFrame("logo-display"); const LogoDesignControlPanel = ({ logoConfig: initialConfig }) => { const [icon, setIcon] = React.useState(initialConfig.icon); const [radius, setRadius] = React.useState(initialConfig.radius); return ( <Stack space="space.400"> <Heading as="h1">Project logo designer</Heading> <Inline space="space.400"> <Box xcss={xcss({ width: "25%" })}> <Stack space="space.400"> <IconSelection value={icon} onChange={setIcon} /> <BorderRadiusSlider value={radius} onChange={setRadius} /> </Stack> </Box> {} <LogoDisplayFrame radius={radius} icon={icon} /> </Inline> </Stack> ); }; const App = () => { const logoConfig = useGetLogoConfigFromStorage(); if (!logoConfig) { return null; } return <LogoDesignControlPanel logoConfig={logoConfig} />; }; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode> );
Now, go to resources/project-logo-display/src/hooks
folder and create a new file called useFrameProps.js
to create a custom React hook that utilises the frame.onPropsUpdate
helper in the @forge/bridge
package to allow React based frame resources to subscribe and consume the properties sent from the UI Kit side.
1 2import { useState, useEffect } from "react"; import { view } from "@forge/bridge"; export const useFrameProps = () => { const [frameProps, setFrameProps] = useState(null); useEffect(() => { let deregister = null; const registerPropsUpdate = async () => { deregister = await view.frame.onPropsUpdate((frameProps) => { // this will be called whenever the props changed in the // custom Frame component created through the `createFrame` helper. setFrameProps(frameProps); }); }; registerPropsUpdate(); return () => { if (deregister) { // it is important to unsubscribe from handling property change events // as desribed in: https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/events/#unsubscribe deregister(); } }; }, [setFrameProps]); return [frameProps !== null, frameProps]; };
Locate the file resources/project-logo-display/src/App.js
. In this file, you'll use the useFrameProps
hook. This hook helps you get the values of custom frame properties, specifically icon
and radius
. These properties come from the custom Frame
component named LogoDisplayFrame
. Remember, LogoDisplayFrame
was made with the createFrame
helper, as explained in step 1.
1 2... import { ... useFrameProps, } from "./hooks"; const LogoDesignControlPanel = ({ logoConfig: initialConfig, context }) => { const [color, setColor] = React.useState(initialConfig.color); const handleOnSave = useSetLogoConfigToStorage(); const [loaded, frameProps] = useFrameProps(); if (!loaded) { return null; } const { icon, radius } = frameProps; const convertRadius = (radius) => (radius / ORIGINAL_IMAGE_SIZE) * MACRO_IMAGE_SIZE; return ( <Stack space="space.200" alignInline="start"> <Stack alignInline="center" space="space.200"> <ColorPalette value={color} onChange={setColor} /> <Stack space="space.200"> <LogoDisplay icon={icon} radius={radius} color={color} size={ORIGINAL_IMAGE_SIZE} /> <Button appearance="primary" onClick={() => handleOnSave({ icon, radius, color })} > Save changes </Button> </Stack> </Stack> </Stack> ); }; const App = () => { ... }; export default App;
Frame
component can invoke the FaaS function resources (resolvers) that are defined within its containing UI Kit App. In this example, Storage is utilised to persist and load the logo configuration, and the Storage API operations will be wrapped inside the Forge resolvers, and will be invoked from both UI Kit side and inside the Frame
component.
Go to src/resolvers/index.js
to create resolver functions for persisting and fetching logo configurations. Replace the exisitng code with the following:
1 2import Resolver from "@forge/resolver"; import { storage } from "@forge/api"; const LOGO_CONFIG_STORAGE_KEY = "LOGO_CONFIG"; // Move default configuration to the shared backend to remove the duplicated // logics in the frontend. const defaultLogoConfig = { icon: "Avocado", radius: 20, color: "red", }; const resolver = new Resolver(); resolver.define("setLogoConfig", async ({ payload: { config } }) => { return await storage.set(LOGO_CONFIG_STORAGE_KEY, config); }); resolver.define("getLogoConfig", async () => { let config = null; try { config = await storage.get(LOGO_CONFIG_STORAGE_KEY); } catch (e) {} return config || defaultLogoConfig; }); export const handler = resolver.getDefinitions();
Ensure the resolver function defined above is linked in the manifest.yml
file.
1 2# file path: manifest.yml modules: # ... function: - key: resolve handler: index.handler
Go to src/hooks.js
and resources/project-logo-display/src/hooks.js
and add the following code to wrap the resolver functions with react hooks to enhance usability in react.
1 2import { useEffect, useState, useCallback } from "react"; import { events, invoke } from "@forge/bridge"; // .... export const useGetLogoConfigFromStorage = () => { const [config, setConfig] = useState(null); useEffect(() => { // this works in both UI Kit and inside Frame component. invoke("getLogoConfig").then((config) => { setConfig(config); }); }, [setConfig]); return config; }; export const useSetLogoConfigToStorage = () => { return useCallback((config) => { invoke("setLogoConfig", { config }); }, []); };
In the src/frontend/index.jsx
add the following code to set up UI Kit to load the logo configuration with storage API using the hooks defined in the previous steps.
1 2import React, { useEffect } from "react"; import ForgeReconciler, { Inline, Stack, Heading, Frame, } from "@forge/react"; import { IconSelection } from "./components/IconSelection"; import { BorderRadiusSlider } from "./components/BorderRadiusSlider"; import { useLogoConfigUpdates, useGetLogoConfigFromStorage, } from "./hooks"; const LogoDesignControlPanel = ({ logoConfig: initialConfig }) => { const [config, setConfig] = React.useState(initialConfig); const [_, emitLogoConfigUpdates] = useLogoConfigUpdates(); useEffect(() => { emitLogoConfigUpdates( Object.entries(config).map(([key, value]) => ({ key, value })) ); }, [config]); const handleConfigChange = ({ key, value }) => { setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; return ( <Stack space="space.400"> <Heading as="h1">Project logo designer</Heading> <Inline space="space.400"> <Stack space="space.1000"> <IconSelection value={config.icon} onChange={(value) => handleConfigChange({ key: "icon", value }) } /> <BorderRadiusSlider value={config.radius} onChange={(value) => handleConfigChange({ key: "radius", value }) } /> </Stack> <Frame resource="logo-display" /> </Inline> </Stack> ); }; const App = () => { // Load the configuration from the storage API const logoConfig = useGetLogoConfigFromStorage(); if (!logoConfig) { return null; } // Move the logo design control logic into a dedicated component. return <LogoDesignControlPanel logoConfig={logoConfig} />; }; ForgeReconciler.render( <React.StrictMode> <App /> </React.StrictMode> );
Go to resources/project-logo-display/src/App.js
and add the following code to set up the Frame
component to load and persist the logo configuration with storage API using the hooks defined in the previous steps.
1 2import React, { useEffect } from "react"; import { ColorPalette } from "./components/ColorPalette"; import { LogoDisplay } from "./components/LogoDisplay"; import { Stack } from "@atlaskit/primitives"; import Button from "@atlaskit/button"; import { useLogoConfigUpdates, useGetLogoConfigFromStorage, useSetLogoConfigToStorage, } from "./hooks"; const LogoDesignPanel = ({ logoConfig: initialConfig }) => { const [config, setConfig] = React.useState(initialConfig); const [logoConfigUpdates] = useLogoConfigUpdates(); const storeLogoConfig = useSetLogoConfigToStorage(); useEffect(() => { if (logoConfigUpdates && logoConfigUpdates.length > 0) { setConfig((prevConfig) => { const updatedConfig = { ...prevConfig }; logoConfigUpdates.forEach((update) => { updatedConfig[update.key] = update.value; }); return updatedConfig; }); } }, [logoConfigUpdates, setConfig]); const handleConfigChange = ({ key, value }) => { setConfig((prevConfig) => ({ ...prevConfig, [key]: value })); }; return ( <Stack space="space.200" alignInline="start"> <Stack alignInline="center" space="space.200"> <ColorPalette value={config.color} onChange={(value) => handleConfigChange({ key: "color", value }) } /> <Stack space="space.200"> <LogoDisplay icon={config.icon} radius={config.radius} color={config.color} /> {/* Persist the logo configuration to storage api */} <Button appearance="primary" onClick={() => storeLogoConfig(config)} > Save changes </Button> </Stack> </Stack> </Stack> ); }; function App() { // Load the configuration from the storage API const logoConfig = useGetLogoConfigFromStorage(); if (!logoConfig) { return null; } return <LogoDesignPanel logoConfig={logoConfig} />; } export default App;
To use your app, it must be installed onto an Atlassian site. The
forge deploy
command builds, compiles, and deploys your code; it'll also report any compilation errors.
The forge install
command then installs the deployed app onto an Atlassian site with the
required API access.
You must run the forge deploy
command before forge install
because an installation
links your deployed app to an Atlassian site.
Navigate to the app's top-level directory and deploy your app by running:
1 2forge deploy
Install your app by running:
1 2forge install
Select your Atlassian product using the arrow keys and press the enter key.
Enter the URL for your development site. For example, example.atlassian.net. View a list of your active sites at Atlassian administration.
Once the successful installation message appears, your app is installed and ready
to use on the specified site.
You can always delete your app from the site by running the forge uninstall
command.
Running the forge install
command only installs your app onto the selected product.
To install onto multiple products, repeat these steps again, selecting another product each time.
Note that the Atlassian Marketplace
does not support cross-product apps yet.
You must run forge deploy
before running forge install
in any of the Forge environments.
With your app installed, it’s time to see the app on a page.
/
That’s it. You now have a Forge app that renders a preview of the designed icon within the Frame
component.
Rate this page: