Skip to content

Commit 9a0abac

Browse files
authored
refactor: move hooks to templates (#14195)
* refactor: move hooks to templates
1 parent fac0cb8 commit 9a0abac

File tree

24 files changed

+976
-22
lines changed

24 files changed

+976
-22
lines changed

templates/vsc/js/dashboard-tab/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export const getSampleData = () => {
9191

9292
### Step 2: Create a widget file
9393

94-
Create a widget file in the `src/widgets` folder. Inherit the `BaseWidget` class from `@microsoft/teamsfx-react`. The following table lists the methods that you can override to customize your widget.
94+
Create a widget file in the `src/widgets` folder. Inherit the `BaseWidget` class. The following table lists the methods that you can override to customize your widget.
9595

9696
| Methods | Function |
9797
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -108,7 +108,7 @@ Here's a sample widget implementation:
108108
```javascript
109109
//SampleWidget.jsx
110110
import { Button, Text } from "@fluentui/react-components";
111-
import { BaseWidget } from "@microsoft/teamsfx-react";
111+
import { BaseWidget } from "./BaseWidget";
112112
import { getSampleData } from "../services/sampleService";
113113

114114
export class SampleWidget extends BaseWidget {

templates/vsc/js/dashboard-tab/package.json.tpl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
"@fluentui/react-components": "^9.55.1",
1212
"@fluentui/react-icons": "^2.0.186",
1313
"@microsoft/teams-js": "^2.31.1",
14-
"@microsoft/teamsfx": "^3.0.0",
15-
"@microsoft/teamsfx-react": "^4.0.0",
1614
"react": "^18.2.0",
1715
"react-dom": "^18.2.0",
1816
"react-router-dom": "^6.8.0"

templates/vsc/js/dashboard-tab/src/App.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
teamsDarkTheme,
1010
teamsHighContrastTheme,
1111
} from "@fluentui/react-components";
12-
import { useTeams } from "@microsoft/teamsfx-react";
12+
import { useTeams } from "./internal/useTeams";
1313

1414
import SampleDashboard from "./dashboards/SampleDashboard";
1515
import { TeamsFxContext } from "./internal/context";
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import React, { Component } from "react";
2+
3+
import { mergeStyles } from "@fluentui/react";
4+
5+
/**
6+
* Returns the CSS class name for the dashboard.
7+
* @returns The CSS class name for the dashboard.
8+
* @internal
9+
*/
10+
function dashboardStyle(isMobile) {
11+
return mergeStyles({
12+
display: "grid",
13+
gap: "20px",
14+
padding: "20px",
15+
gridTemplateRows: "1fr",
16+
gridTemplateColumns: "4fr 6fr",
17+
...(isMobile === true ? { gridTemplateColumns: "1fr", gridTemplateRows: "1fr" } : {}),
18+
});
19+
}
20+
21+
/**
22+
* The base component that provides basic functionality to create a dashboard.
23+
*/
24+
export class BaseDashboard extends Component {
25+
/**
26+
* @internal
27+
*/
28+
constructor(props) {
29+
super(props);
30+
this.state = {
31+
isMobile: undefined,
32+
showLogin: undefined,
33+
observer: undefined,
34+
};
35+
this.ref = React.createRef();
36+
}
37+
38+
/**
39+
* Called after the component is mounted. You can do initialization that requires DOM nodes here. You can also make network requests here if you need to load data from a remote endpoint.
40+
*/
41+
async componentDidMount() {
42+
// Observe the dashboard div for resize events
43+
const observer = new ResizeObserver((entries) => {
44+
for (const entry of entries) {
45+
if (entry.target === this.ref.current) {
46+
const { width } = entry.contentRect;
47+
this.setState({ isMobile: width < 600 });
48+
}
49+
}
50+
});
51+
observer.observe(this.ref.current);
52+
this.setState({ observer });
53+
}
54+
55+
/**
56+
* Called before the component is unmounted and destroyed. You can do necessary cleanup here, such as invalidating timers, canceling network requests, or removing any DOM elements.
57+
*/
58+
componentWillUnmount() {
59+
// Unobserve the dashboard div for resize events
60+
if (this.state.observer && this.ref.current) {
61+
this.state.observer.unobserve(this.ref.current);
62+
}
63+
}
64+
65+
/**
66+
* Override this method to customize the styling of the dashboard.
67+
* @returns The CSS class name for the dashboard.
68+
*/
69+
styling() {
70+
return "";
71+
}
72+
73+
/**
74+
* Override this method to define the layout of the dashboard.
75+
* @returns The JSX element that defines the layout of the dashboard.
76+
*/
77+
layout() {
78+
return null;
79+
}
80+
81+
/**
82+
* Defines the default layout for the dashboard.
83+
*/
84+
render() {
85+
return (
86+
<div
87+
ref={this.ref}
88+
className={mergeStyles(dashboardStyle(this.state.isMobile), this.styling())}
89+
>
90+
{this.layout()}
91+
</div>
92+
);
93+
}
94+
}

templates/vsc/js/dashboard-tab/src/dashboards/SampleDashboard.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BaseDashboard } from "@microsoft/teamsfx-react";
1+
import { BaseDashboard } from "./BaseDashboard";
22

33
import ChartWidget from "../widgets/ChartWidget";
44
import ListWidget from "../widgets/ListWidget";
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useEffect, useState } from "react";
2+
import { unstable_batchedUpdates as batchedUpdates } from "react-dom";
3+
import { app, pages } from "@microsoft/teams-js";
4+
import {
5+
teamsLightTheme,
6+
teamsDarkTheme,
7+
teamsHighContrastTheme,
8+
} from "@fluentui/react-components";
9+
10+
const getTheme = () => {
11+
const urlParams = new URLSearchParams(window.location.search);
12+
const theme = urlParams.get("theme");
13+
return theme == null ? undefined : theme;
14+
};
15+
16+
/**
17+
* Microsoft Teams React hook
18+
* @param {Object} options optional options
19+
* @returns A tuple with properties and methods
20+
* properties:
21+
* - inTeams: boolean = true if inside Microsoft Teams
22+
* - fullscreen: boolean = true if in full screen mode
23+
* - theme: Fluent UI Theme
24+
* - themeString: string - representation of the theme (default, dark or contrast)
25+
* - context - the Microsoft Teams JS SDK context
26+
* methods:
27+
* - setTheme - manually set the theme
28+
*/
29+
export function useTeams(options) {
30+
const [loading, setLoading] = useState(undefined);
31+
const [inTeams, setInTeams] = useState(undefined);
32+
const [fullScreen, setFullScreen] = useState(undefined);
33+
const [theme, setTheme] = useState(teamsLightTheme);
34+
const [themeString, setThemeString] = useState("default");
35+
const [initialTheme] = useState(
36+
options && options.initialTheme ? options.initialTheme : getTheme()
37+
);
38+
const [context, setContext] = useState(undefined);
39+
40+
const themeChangeHandler = (theme) => {
41+
setThemeString(theme || "default");
42+
switch (theme) {
43+
case "dark":
44+
setTheme(teamsDarkTheme);
45+
break;
46+
case "contrast":
47+
setTheme(teamsHighContrastTheme);
48+
break;
49+
case "default":
50+
default:
51+
setTheme(teamsLightTheme);
52+
}
53+
};
54+
55+
const overrideThemeHandler = options?.setThemeHandler
56+
? options.setThemeHandler
57+
: themeChangeHandler;
58+
59+
useEffect(() => {
60+
// set initial theme based on options or query string
61+
if (initialTheme) {
62+
overrideThemeHandler(initialTheme);
63+
}
64+
65+
app
66+
.initialize()
67+
.then(() => {
68+
app
69+
.getContext()
70+
.then((context) => {
71+
batchedUpdates(() => {
72+
setInTeams(true);
73+
setContext(context);
74+
setFullScreen(context.page.isFullScreen);
75+
});
76+
overrideThemeHandler(context.app.theme);
77+
app.registerOnThemeChangeHandler(overrideThemeHandler);
78+
pages.registerFullScreenHandler((isFullScreen) => {
79+
setFullScreen(isFullScreen);
80+
});
81+
setLoading(false);
82+
})
83+
.catch(() => {
84+
setLoading(false);
85+
setInTeams(false);
86+
});
87+
})
88+
.catch(() => {
89+
setLoading(false);
90+
setInTeams(false);
91+
});
92+
}, [initialTheme, overrideThemeHandler]);
93+
94+
return [
95+
{
96+
inTeams,
97+
fullScreen,
98+
theme,
99+
themeString,
100+
context,
101+
loading,
102+
},
103+
{
104+
setTheme: (theme) => {
105+
overrideThemeHandler(theme);
106+
},
107+
},
108+
];
109+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React, { Component } from "react";
2+
3+
import { mergeStyles, mergeStyleSets } from "@fluentui/react";
4+
import { tokens } from "@fluentui/react-components";
5+
6+
/**
7+
* Style definitions for the widget elements
8+
* @internal
9+
*/
10+
const classNames = mergeStyleSets({
11+
root: {
12+
display: "grid",
13+
padding: "1.25rem 2rem 1.25rem 2rem",
14+
backgroundColor: tokens.colorNeutralBackground1,
15+
border: "1px solid var(--colorTransparentStroke)",
16+
boxShadow: tokens.shadow4,
17+
borderRadius: tokens.borderRadiusMedium,
18+
gap: tokens.spacingHorizontalL,
19+
gridTemplateRows: "max-content 1fr max-content",
20+
},
21+
header: {
22+
display: "grid",
23+
height: "max-content",
24+
"& div": {
25+
display: "grid",
26+
gap: tokens.spacingHorizontalS,
27+
alignItems: "center",
28+
gridTemplateColumns: "min-content 1fr min-content",
29+
},
30+
"& svg": {
31+
height: "1.5rem",
32+
width: "1.5rem",
33+
},
34+
"& span": {
35+
fontWeight: tokens.fontWeightSemibold,
36+
lineHeight: tokens.lineHeightBase200,
37+
fontSize: tokens.fontSizeBase200,
38+
},
39+
},
40+
footer: {
41+
"& button": {
42+
width: "fit-content",
43+
},
44+
},
45+
});
46+
47+
/**
48+
* The base component that provides basic functionality to create a widget.
49+
*/
50+
export class BaseWidget extends Component {
51+
/**
52+
* Constructor of BaseWidget.
53+
* @param {Object} props - The props of the component.
54+
*/
55+
constructor(props) {
56+
super(props);
57+
this.state = { loading: undefined };
58+
}
59+
60+
/**
61+
* Called after the component is mounted. You can do initialization that requires DOM nodes here. You can also make network requests here if you need to load data from a remote endpoint.
62+
*/
63+
async componentDidMount() {
64+
this.setState({ ...(await this.getData()), loading: false });
65+
}
66+
67+
/**
68+
* Override this method to fetch data for the widget.
69+
* @returns {Promise<Object>} The data for the widget.
70+
*/
71+
async getData() {
72+
return {};
73+
}
74+
75+
/**
76+
* Override this method to customize the styling of the widget.
77+
* @returns {string} The CSS class name for the widget.
78+
*/
79+
styling() {
80+
return "";
81+
}
82+
83+
/**
84+
* Override this method to define the header of the widget.
85+
* @returns {JSX.Element} The JSX element that defines the header of the widget.
86+
*/
87+
header() {
88+
return null;
89+
}
90+
91+
/**
92+
* Override this method to define the body of the widget.
93+
* @returns {JSX.Element} The JSX element that defines the body of the widget.
94+
*/
95+
body() {
96+
return null;
97+
}
98+
99+
/**
100+
* Override this method to define the footer of the widget.
101+
* @returns {JSX.Element} The JSX element that defines the footer of the widget.
102+
*/
103+
footer() {
104+
return null;
105+
}
106+
107+
/**
108+
* Renders the widget.
109+
* @returns {JSX.Element} The JSX element that defines the widget.
110+
*/
111+
render() {
112+
return (
113+
<div className={mergeStyles(classNames.root, this.styling())}>
114+
{this.header() && <div className={classNames.header}>{this.header()}</div>}
115+
{this.body() && <div className={classNames.body}>{this.body()}</div>}
116+
{this.footer() && <div className={classNames.footer}>{this.footer()}</div>}
117+
</div>
118+
);
119+
}
120+
}

templates/vsc/js/dashboard-tab/src/widgets/ChartWidget.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
DataPie24Regular,
88
MoreHorizontal32Regular,
99
} from "@fluentui/react-icons";
10-
import { BaseWidget } from "@microsoft/teamsfx-react";
10+
import { BaseWidget } from "./BaseWidget";
1111

1212
import { getChart1Points, getChart2Points, getTimeRange } from "../services/chartService";
1313

templates/vsc/js/dashboard-tab/src/widgets/ListWidget.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "../styles/ListWidget.css";
22

33
import { Button, Text } from "@fluentui/react-components";
44
import { List28Filled, MoreHorizontal32Regular } from "@fluentui/react-icons";
5-
import { BaseWidget } from "@microsoft/teamsfx-react";
5+
import { BaseWidget } from "./BaseWidget";
66

77
import { getListData } from "../services/listService";
88

templates/vsc/js/non-sso-tab-default-bot/tab/package.json.tpl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
"dependencies": {
99
"@fluentui/react-components": "^9.55.1",
1010
"@microsoft/teams-js": "^2.31.1",
11-
"@microsoft/teamsfx": "^3.0.0",
12-
"@microsoft/teamsfx-react": "^4.0.0",
1311
"axios": "^0.21.1",
1412
"react": "^18.2.0",
1513
"react-dom": "^18.2.0",

0 commit comments

Comments
 (0)