@forge/react 11.17.0 → 11.18.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @forge/react
2
2
 
3
+ ## 11.18.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 01550cf: Fix flaky unit test
8
+
9
+ ### Patch Changes
10
+
11
+ - 7504dd5: Fix naming on PersonalSettingsItem
12
+ - Updated dependencies [b0b69a2]
13
+ - Updated dependencies [561f8f4]
14
+ - @forge/bridge@5.18.0-next.0
15
+
3
16
  ## 11.17.0
4
17
 
5
18
  ### Minor Changes
@@ -10,6 +10,6 @@ export declare const CreateButton: TCreateButton<ForgeElement<Record<string, any
10
10
  export declare const CreateMenuItem: TCreateMenuItem<ForgeElement<Record<string, any>>>;
11
11
  export declare const HelpLink: THelpLink<ForgeElement<Record<string, any>>>;
12
12
  export declare const PersonalSettings: TPersonalSettings<ForgeElement<Record<string, any>>>;
13
- export declare const PersonalSettingsItems: TPersonalSettingsItem<ForgeElement<Record<string, any>>>;
13
+ export declare const PersonalSettingsItem: TPersonalSettingsItem<ForgeElement<Record<string, any>>>;
14
14
  export type { GlobalProps, MainProps, SidebarProps, LinkMenuItemProps, ExpandableMenuItemProps, FlyOutMenuItemProps, CreateButtonProps, CreateMenuItemProps, HelpLinkProps, PersonalSettingsProps, PersonalSettingsItemProps } from '@atlaskit/forge-react-types/global';
15
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/global/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,KAAK,EACL,OAAO,EACP,aAAa,EACb,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,MAAM,4CAA+C,CAAC;AACnE,eAAO,MAAM,IAAI,0CAA2C,CAAC;AAC7D,eAAO,MAAM,OAAO,6CAAiD,CAAC;AACtE,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,kBAAkB,wDAAuE,CAAC;AACvG,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,QAAQ,8CAAmD,CAAC;AACzE,eAAO,MAAM,gBAAgB,sDAAmE,CAAC;AACjG,eAAO,MAAM,qBAAqB,0DAA4E,CAAC;AAG/G,YAAY,EACV,WAAW,EACX,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/global/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,QAAQ,EACR,KAAK,EACL,OAAO,EACP,aAAa,EACb,eAAe,EACf,SAAS,EACT,iBAAiB,EACjB,qBAAqB,EACtB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,MAAM,4CAA+C,CAAC;AACnE,eAAO,MAAM,IAAI,0CAA2C,CAAC;AAC7D,eAAO,MAAM,OAAO,6CAAiD,CAAC;AACtE,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,kBAAkB,wDAAuE,CAAC;AACvG,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,YAAY,kDAA2D,CAAC;AACrF,eAAO,MAAM,cAAc,oDAA+D,CAAC;AAC3F,eAAO,MAAM,QAAQ,8CAAmD,CAAC;AACzE,eAAO,MAAM,gBAAgB,sDAAmE,CAAC;AACjG,eAAO,MAAM,oBAAoB,0DAA2E,CAAC;AAG7G,YAAY,EACV,WAAW,EACX,SAAS,EACT,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,oCAAoC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PersonalSettingsItems = exports.PersonalSettings = exports.HelpLink = exports.CreateMenuItem = exports.CreateButton = exports.FlyOutMenuItem = exports.ExpandableMenuItem = exports.LinkMenuItem = exports.Sidebar = exports.Main = exports.Global = void 0;
3
+ exports.PersonalSettingsItem = exports.PersonalSettings = exports.HelpLink = exports.CreateMenuItem = exports.CreateButton = exports.FlyOutMenuItem = exports.ExpandableMenuItem = exports.LinkMenuItem = exports.Sidebar = exports.Main = exports.Global = void 0;
4
4
  exports.Global = 'Global';
5
5
  exports.Main = 'Main';
6
6
  exports.Sidebar = 'Sidebar';
@@ -11,4 +11,4 @@ exports.CreateButton = 'CreateButton';
11
11
  exports.CreateMenuItem = 'CreateMenuItem';
12
12
  exports.HelpLink = 'HelpLink';
13
13
  exports.PersonalSettings = 'PersonalSettings';
14
- exports.PersonalSettingsItems = 'PersonalSettingsItems';
14
+ exports.PersonalSettingsItem = 'PersonalSettingsItem';
@@ -26,14 +26,28 @@ const MOCK_CONTEXT_NO_THEME = {
26
26
  extension: {}
27
27
  };
28
28
  const themeListener = jest.fn();
29
+ const flushUpdates = () => reconcilerTestRenderer_1.default.act(async () => {
30
+ await Promise.resolve();
31
+ await (0, utils_1.sleep)();
32
+ });
33
+ const emitThemeChanged = async (theme) => {
34
+ await reconcilerTestRenderer_1.default.act(async () => {
35
+ utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', { theme });
36
+ await (0, utils_1.sleep)();
37
+ });
38
+ };
29
39
  // react app fragment to load useTheme hook
30
- const renderTest = async () => {
40
+ const renderTest = async (options) => {
41
+ const flushAfterMount = options?.flushAfterMount ?? true;
31
42
  const Test = () => {
32
43
  const theme = (0, useTheme_1.useTheme)();
33
44
  (0, react_1.useEffect)(() => themeListener(theme), [theme]);
34
45
  return (0, jsx_runtime_1.jsx)(react_1.default.Fragment, {});
35
46
  };
36
47
  const { update } = await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(Test, {}));
48
+ if (flushAfterMount) {
49
+ await flushUpdates();
50
+ }
37
51
  return {
38
52
  update: async () => {
39
53
  await update((0, jsx_runtime_1.jsx)(Test, {}));
@@ -62,17 +76,19 @@ describe('useTheme', () => {
62
76
  await renderTest();
63
77
  expect(themeListener).toHaveBeenCalledWith(expect.objectContaining(MOCK_THEME));
64
78
  const newTheme = { colorMode: 'light', colorScheme: 'red' };
65
- utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', { theme: newTheme });
66
- await (0, utils_1.sleep)();
79
+ await emitThemeChanged(newTheme);
67
80
  expect(themeListener).toHaveBeenCalledWith(expect.objectContaining(newTheme));
68
81
  });
69
82
  it('does not cause re-render when theme content is the same', async () => {
70
83
  mockGetContext.mockResolvedValue(MOCK_CONTEXT_WITH_THEME);
71
84
  await renderTest();
72
- expect(themeListener).toHaveBeenCalledTimes(2); // undefined, then theme
85
+ expect(themeListener).toHaveBeenCalledTimes(2); // null, then theme
73
86
  // Emit same theme content but different object reference
74
- utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', {
75
- theme: { ...MOCK_THEME } // New object, same content
87
+ await reconcilerTestRenderer_1.default.act(async () => {
88
+ utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', {
89
+ theme: { ...MOCK_THEME } // New object, same content
90
+ });
91
+ await (0, utils_1.sleep)();
76
92
  });
77
93
  // Should not trigger re-render due to isEqual check
78
94
  expect(themeListener).toHaveBeenCalledTimes(2);
@@ -84,13 +100,21 @@ describe('useTheme', () => {
84
100
  resolveGetContext = resolve;
85
101
  });
86
102
  mockGetContext.mockReturnValue(getContextPromise);
87
- const renderPromise = renderTest();
103
+ const renderPromise = renderTest({ flushAfterMount: false });
104
+ await renderPromise;
88
105
  // Fire event before getContext resolves
89
106
  const eventTheme = { colorMode: 'light', colorScheme: 'green' };
90
- utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', { theme: eventTheme });
107
+ await reconcilerTestRenderer_1.default.act(async () => {
108
+ utils_1.simpleBridgeEvents.emit('FORGE_CORE_THEME_CHANGED', { theme: eventTheme });
109
+ await (0, utils_1.sleep)();
110
+ });
91
111
  // Now resolve getContext
92
- resolveGetContext?.(MOCK_CONTEXT_WITH_THEME);
93
- await renderPromise;
112
+ await reconcilerTestRenderer_1.default.act(async () => {
113
+ resolveGetContext?.(MOCK_CONTEXT_WITH_THEME);
114
+ await getContextPromise;
115
+ await (0, utils_1.sleep)();
116
+ });
117
+ await flushUpdates();
94
118
  // Should have the event theme, not the getContext theme
95
119
  expect(themeListener).toHaveBeenCalledWith(expect.objectContaining(eventTheme));
96
120
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/hooks/useTheme.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,WAAW,EAAQ,MAAM,eAAe,CAAC;AAG/D,aAAK,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAElC,eAAO,MAAM,QAAQ,QAAO,KAAK,GAAG,IAgCnC,CAAC"}
1
+ {"version":3,"file":"useTheme.d.ts","sourceRoot":"","sources":["../../src/hooks/useTheme.ts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,WAAW,EAAQ,MAAM,eAAe,CAAC;AAG/D,aAAK,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAElC,eAAO,MAAM,QAAQ,QAAO,KAAK,GAAG,IAkEnC,CAAC"}
@@ -7,31 +7,61 @@ const bridge_1 = require("@forge/bridge");
7
7
  const isEqual_1 = tslib_1.__importDefault(require("lodash/isEqual"));
8
8
  const useTheme = () => {
9
9
  const [theme, setTheme] = (0, react_1.useState)(null);
10
+ const themeRef = (0, react_1.useRef)(null);
10
11
  const themeLoadedRef = (0, react_1.useRef)(false);
12
+ const loadingContextRef = (0, react_1.useRef)(false);
13
+ const applyTheme = (0, react_1.useCallback)((nextTheme) => {
14
+ if ((0, isEqual_1.default)(themeRef.current, nextTheme)) {
15
+ return;
16
+ }
17
+ themeRef.current = nextTheme;
18
+ setTheme((current) => ((0, isEqual_1.default)(current, nextTheme) ? current : nextTheme));
19
+ }, []);
11
20
  (0, react_1.useEffect)(() => {
21
+ let cancelled = false;
12
22
  void (async () => {
13
- if (!themeLoadedRef.current) {
23
+ if (themeLoadedRef.current || loadingContextRef.current) {
24
+ return;
25
+ }
26
+ loadingContextRef.current = true;
27
+ try {
14
28
  const context = await bridge_1.view.getContext();
15
- if (context?.theme && !themeLoadedRef.current) {
16
- setTheme((currentTheme) => {
17
- return (0, isEqual_1.default)(currentTheme, context.theme) ? currentTheme : context.theme;
18
- });
19
- themeLoadedRef.current = true;
29
+ if (cancelled || !context?.theme || themeLoadedRef.current) {
30
+ return;
20
31
  }
32
+ applyTheme(context.theme);
33
+ themeLoadedRef.current = true;
34
+ }
35
+ finally {
36
+ loadingContextRef.current = false;
21
37
  }
22
38
  })();
23
- }, []);
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [applyTheme]);
24
43
  (0, react_1.useEffect)(() => {
44
+ let unsubscribeFn = null;
45
+ let cancelled = false;
25
46
  const sub = bridge_1.events.on('FORGE_CORE_THEME_CHANGED', ({ theme: updatedTheme }) => {
26
47
  themeLoadedRef.current = true;
27
- setTheme((currentTheme) => {
28
- return (0, isEqual_1.default)(currentTheme, updatedTheme) ? currentTheme : updatedTheme;
29
- });
48
+ applyTheme(updatedTheme);
49
+ });
50
+ void sub.then((subscription) => {
51
+ if (cancelled) {
52
+ subscription.unsubscribe();
53
+ }
54
+ else {
55
+ unsubscribeFn = () => subscription.unsubscribe();
56
+ }
30
57
  });
31
58
  return () => {
32
- void sub.then((subscription) => subscription.unsubscribe());
59
+ cancelled = true;
60
+ if (unsubscribeFn) {
61
+ unsubscribeFn();
62
+ }
33
63
  };
34
- }, []);
64
+ }, [applyTheme]);
35
65
  return theme;
36
66
  };
37
67
  exports.useTheme = useTheme;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forge/react",
3
- "version": "11.17.0",
3
+ "version": "11.18.0-next.0",
4
4
  "description": "Forge React reconciler",
5
5
  "author": "Atlassian",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
@@ -32,7 +32,7 @@
32
32
  "@atlaskit/adf-schema": "^48.0.0",
33
33
  "@atlaskit/adf-utils": "^19.19.0",
34
34
  "@atlaskit/forge-react-types": "^1.7.0",
35
- "@forge/bridge": "^5.17.0",
35
+ "@forge/bridge": "^5.18.0-next.0",
36
36
  "@forge/egress": "^2.3.2",
37
37
  "@forge/i18n": "0.0.7",
38
38
  "@types/react": "^18.2.64",