@khester/create-dynamics-app 2.9.0 → 3.0.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/README.md +30 -5
- package/dist/artifacts/registry.d.ts +7 -5
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +44 -18
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/artifacts/types.d.ts +15 -8
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +15 -6
- package/dist/index.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +9 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/utils/copyTemplate.d.ts +1 -1
- package/dist/utils/copyTemplate.js +1 -1
- package/package.json +1 -1
- package/templates/power-pages-island/README.md +81 -0
- package/templates/power-pages-island/package.json +27 -0
- package/templates/power-pages-island/public/index.html +13 -0
- package/templates/power-pages-island/src/App.tsx +25 -0
- package/templates/power-pages-island/src/Component.tsx +89 -0
- package/templates/power-pages-island/src/core/services/MockApiService.ts +58 -0
- package/templates/power-pages-island/src/core/services/portalSeam.assert.ts +14 -0
- package/templates/power-pages-island/src/island-env.d.ts +12 -0
- package/templates/power-pages-island/src/islands.tsx +38 -0
- package/templates/power-pages-island/tsconfig.json +15 -0
- package/templates/power-pages-island/webpack.config.js +22 -0
- package/templates/teams-app/README.md +1 -0
- package/templates/teams-app/_variants/tab.meeting/appPackage/manifest.template.json +46 -0
- package/templates/teams-app/_variants/tab.meeting/auth.html +15 -0
- package/templates/teams-app/_variants/tab.meeting/env.local.template +3 -0
- package/templates/teams-app/_variants/tab.meeting/src/App.tsx +59 -0
- package/templates/teams-app/_variants/tab.meeting/src/auth-bridge.ts +52 -0
- package/templates/teams-app/_variants/tab.meeting/src/components/SidePanel.tsx +124 -0
- package/templates/teams-app/_variants/tab.meeting/src/components/StageView.tsx +93 -0
- package/templates/teams-app/_variants/tab.meeting/src/hooks/useHashRoute.ts +16 -0
- package/templates/teams-app/_variants/tab.meeting/src/hooks/useTeamsAuth.ts +121 -0
- package/templates/teams-app/_variants/tab.meeting/src/pages/Config.tsx +60 -0
- package/templates/teams-app/_variants/tab.meeting/src/pages/Remove.tsx +9 -0
- package/templates/teams-app/_variants/tab.meeting/src/vite-env.d.ts +10 -0
- package/templates/teams-app/_variants/tab.meeting/vite.config.ts +19 -0
- package/templates/power-pages-starter/.env.example +0 -6
- package/templates/power-pages-starter/README.md +0 -95
- package/templates/power-pages-starter/package.json +0 -43
- package/templates/power-pages-starter/public/index.html +0 -15
- package/templates/power-pages-starter/src/components/ContactForm.css +0 -84
- package/templates/power-pages-starter/src/components/ContactForm.tsx +0 -258
- package/templates/power-pages-starter/src/index.tsx +0 -32
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +0 -162
- package/templates/power-pages-starter/src/styles/index.css +0 -76
- package/templates/power-pages-starter/tsconfig.json +0 -20
- package/templates/power-pages-starter/webpack.config.js +0 -57
- package/templates/{power-pages-starter → power-pages-island}/gitignore +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardHeader,
|
|
5
|
+
Text,
|
|
6
|
+
Button,
|
|
7
|
+
makeStyles,
|
|
8
|
+
tokens,
|
|
9
|
+
Input,
|
|
10
|
+
Badge,
|
|
11
|
+
MessageBar,
|
|
12
|
+
MessageBarBody,
|
|
13
|
+
} from '@fluentui/react-components';
|
|
14
|
+
import { People24Regular, Share24Regular, Send24Regular } from '@fluentui/react-icons';
|
|
15
|
+
import { meeting } from '@microsoft/teams-js';
|
|
16
|
+
|
|
17
|
+
const useStyles = makeStyles({
|
|
18
|
+
container: { padding: tokens.spacingHorizontalL, minHeight: '100vh' },
|
|
19
|
+
header: { marginBottom: tokens.spacingVerticalL },
|
|
20
|
+
card: { marginBottom: tokens.spacingVerticalM },
|
|
21
|
+
input: { display: 'flex', gap: tokens.spacingHorizontalS, marginTop: tokens.spacingVerticalM },
|
|
22
|
+
messages: { maxHeight: '200px', overflowY: 'auto', marginBottom: tokens.spacingVerticalM },
|
|
23
|
+
message: {
|
|
24
|
+
padding: tokens.spacingHorizontalS,
|
|
25
|
+
background: tokens.colorNeutralBackground3,
|
|
26
|
+
borderRadius: '4px',
|
|
27
|
+
marginBottom: tokens.spacingVerticalXS,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The in-meeting side panel. "Share to Stage" shares the app's ROOT url onto the
|
|
33
|
+
* meeting stage — Teams re-opens it with frameContext `meetingStage`, so App.tsx
|
|
34
|
+
* renders <StageView/>. (The 07 teaching sample shared `/stage`, which 404s on a
|
|
35
|
+
* static SWA host and is never routed.) The shareAppContentToStage callback is
|
|
36
|
+
* `(error, result)` — surface the error instead of discarding it, or a failed
|
|
37
|
+
* share (e.g. a missing MeetingStage.Write.Chat RSC grant) is silent.
|
|
38
|
+
*/
|
|
39
|
+
export function SidePanel() {
|
|
40
|
+
const styles = useStyles();
|
|
41
|
+
const [messages, setMessages] = useState<string[]>(['Meeting started', 'Welcome everyone!']);
|
|
42
|
+
const [newMessage, setNewMessage] = useState('');
|
|
43
|
+
const [shareError, setShareError] = useState<string | null>(null);
|
|
44
|
+
|
|
45
|
+
const handleShare = () => {
|
|
46
|
+
setShareError(null);
|
|
47
|
+
try {
|
|
48
|
+
meeting.shareAppContentToStage((error) => {
|
|
49
|
+
if (error) {
|
|
50
|
+
console.error('shareAppContentToStage failed:', error);
|
|
51
|
+
setShareError(error.message ?? 'Could not share to the meeting stage.');
|
|
52
|
+
}
|
|
53
|
+
}, `${window.location.origin}/#/`);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('Share to stage:', e);
|
|
56
|
+
setShareError('Sharing to the stage is only available inside a Teams meeting (desktop).');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const sendMessage = () => {
|
|
61
|
+
if (newMessage.trim()) {
|
|
62
|
+
setMessages([...messages, newMessage]);
|
|
63
|
+
setNewMessage('');
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={styles.container}>
|
|
69
|
+
<div className={styles.header}>
|
|
70
|
+
<Text size={500} weight="semibold">
|
|
71
|
+
Meeting Side Panel
|
|
72
|
+
</Text>
|
|
73
|
+
<Badge appearance="filled" color="success" style={{ marginLeft: 8 }}>
|
|
74
|
+
Live
|
|
75
|
+
</Badge>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<Card className={styles.card}>
|
|
79
|
+
<CardHeader
|
|
80
|
+
header={
|
|
81
|
+
<Text weight="semibold">
|
|
82
|
+
<People24Regular /> Participants
|
|
83
|
+
</Text>
|
|
84
|
+
}
|
|
85
|
+
/>
|
|
86
|
+
<div style={{ padding: tokens.spacingHorizontalM }}>
|
|
87
|
+
<Text>3 participants in meeting</Text>
|
|
88
|
+
</div>
|
|
89
|
+
</Card>
|
|
90
|
+
|
|
91
|
+
<Card className={styles.card}>
|
|
92
|
+
<CardHeader header={<Text weight="semibold">Meeting Chat</Text>} />
|
|
93
|
+
<div style={{ padding: tokens.spacingHorizontalM }}>
|
|
94
|
+
<div className={styles.messages}>
|
|
95
|
+
{messages.map((m, i) => (
|
|
96
|
+
<div key={i} className={styles.message}>
|
|
97
|
+
<Text size={200}>{m}</Text>
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
<div className={styles.input}>
|
|
102
|
+
<Input
|
|
103
|
+
value={newMessage}
|
|
104
|
+
onChange={(_, d) => setNewMessage(d.value)}
|
|
105
|
+
placeholder="Type a message..."
|
|
106
|
+
style={{ flex: 1 }}
|
|
107
|
+
/>
|
|
108
|
+
<Button icon={<Send24Regular />} onClick={sendMessage} />
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</Card>
|
|
112
|
+
|
|
113
|
+
<Button appearance="primary" icon={<Share24Regular />} onClick={handleShare}>
|
|
114
|
+
Share to Stage
|
|
115
|
+
</Button>
|
|
116
|
+
|
|
117
|
+
{shareError && (
|
|
118
|
+
<MessageBar intent="error" style={{ marginTop: tokens.spacingVerticalM }}>
|
|
119
|
+
<MessageBarBody>{shareError}</MessageBarBody>
|
|
120
|
+
</MessageBar>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Card, Text, Button, makeStyles, tokens, Badge } from '@fluentui/react-components';
|
|
3
|
+
import { Play24Regular, Pause24Regular, Next24Regular, Previous24Regular } from '@fluentui/react-icons';
|
|
4
|
+
|
|
5
|
+
const useStyles = makeStyles({
|
|
6
|
+
container: {
|
|
7
|
+
padding: tokens.spacingHorizontalXXL,
|
|
8
|
+
textAlign: 'center',
|
|
9
|
+
minHeight: '100vh',
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
},
|
|
15
|
+
title: { marginBottom: tokens.spacingVerticalL },
|
|
16
|
+
card: { maxWidth: '600px', width: '100%', padding: tokens.spacingHorizontalL },
|
|
17
|
+
slide: {
|
|
18
|
+
background: tokens.colorNeutralBackground3,
|
|
19
|
+
borderRadius: '8px',
|
|
20
|
+
padding: tokens.spacingHorizontalXXL,
|
|
21
|
+
marginBottom: tokens.spacingVerticalL,
|
|
22
|
+
minHeight: '200px',
|
|
23
|
+
display: 'flex',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
justifyContent: 'center',
|
|
26
|
+
},
|
|
27
|
+
controls: { display: 'flex', gap: tokens.spacingHorizontalM, justifyContent: 'center' },
|
|
28
|
+
status: { marginTop: tokens.spacingVerticalM },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const slides = [
|
|
32
|
+
{ title: 'Welcome', content: 'Welcome to the meeting presentation' },
|
|
33
|
+
{ title: 'Agenda', content: 'Today we will cover: Q4 results, roadmap, Q&A' },
|
|
34
|
+
{ title: 'Results', content: 'Q4 exceeded expectations by 15%' },
|
|
35
|
+
{ title: 'Thank You', content: 'Questions?' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The shared meeting stage. Reached when Teams opens the app on the stage
|
|
40
|
+
* (frameContext === 'meetingStage'); pure presentational, no teams-js calls.
|
|
41
|
+
*/
|
|
42
|
+
export function StageView() {
|
|
43
|
+
const styles = useStyles();
|
|
44
|
+
const [currentSlide, setCurrentSlide] = useState(0);
|
|
45
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
46
|
+
|
|
47
|
+
const next = () => setCurrentSlide((c) => Math.min(c + 1, slides.length - 1));
|
|
48
|
+
const prev = () => setCurrentSlide((c) => Math.max(c - 1, 0));
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={styles.container}>
|
|
52
|
+
<Text size={600} weight="semibold" className={styles.title}>
|
|
53
|
+
Meeting Stage
|
|
54
|
+
</Text>
|
|
55
|
+
<Badge appearance="filled" color="brand">
|
|
56
|
+
Shared with everyone
|
|
57
|
+
</Badge>
|
|
58
|
+
|
|
59
|
+
<Card className={styles.card}>
|
|
60
|
+
<div className={styles.slide}>
|
|
61
|
+
<div>
|
|
62
|
+
<Text size={500} weight="semibold" block>
|
|
63
|
+
{slides[currentSlide].title}
|
|
64
|
+
</Text>
|
|
65
|
+
<Text size={300} style={{ marginTop: 16 }}>
|
|
66
|
+
{slides[currentSlide].content}
|
|
67
|
+
</Text>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className={styles.controls}>
|
|
72
|
+
<Button icon={<Previous24Regular />} onClick={prev} disabled={currentSlide === 0} />
|
|
73
|
+
<Button
|
|
74
|
+
icon={isPlaying ? <Pause24Regular /> : <Play24Regular />}
|
|
75
|
+
appearance="primary"
|
|
76
|
+
onClick={() => setIsPlaying(!isPlaying)}
|
|
77
|
+
/>
|
|
78
|
+
<Button
|
|
79
|
+
icon={<Next24Regular />}
|
|
80
|
+
onClick={next}
|
|
81
|
+
disabled={currentSlide === slides.length - 1}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className={styles.status}>
|
|
86
|
+
<Text size={200}>
|
|
87
|
+
Slide {currentSlide + 1} of {slides.length}
|
|
88
|
+
</Text>
|
|
89
|
+
</div>
|
|
90
|
+
</Card>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tiny hash router — `#/config` → `/config`. Hash routing keeps the tab a single
|
|
5
|
+
* static bundle (no server rewrites needed on Static Web Apps), which is why the
|
|
6
|
+
* manifest's configurationUrl points at `/#/config`.
|
|
7
|
+
*/
|
|
8
|
+
export function useHashRoute(): string {
|
|
9
|
+
const [hash, setHash] = useState(window.location.hash);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const onChange = () => setHash(window.location.hash);
|
|
12
|
+
window.addEventListener('hashchange', onChange);
|
|
13
|
+
return () => window.removeEventListener('hashchange', onChange);
|
|
14
|
+
}, []);
|
|
15
|
+
return hash.replace(/^#/, '') || '/';
|
|
16
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { app, authentication } from '@microsoft/teams-js';
|
|
3
|
+
import { PublicClientApplication, type AccountInfo } from '@azure/msal-browser';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Teams-first auth: try Teams SSO (`authentication.getAuthToken()`), fall back to
|
|
7
|
+
* MSAL. Inside the Teams iframe, interactive sign-in must go through the auth
|
|
8
|
+
* bridge (`auth.html`) — `loginPopup` is blocked there; outside Teams (plain
|
|
9
|
+
* browser tab) MSAL redirect works directly.
|
|
10
|
+
*
|
|
11
|
+
* VITE_CLIENT_ID / VITE_TENANT_ID come from `.env.local` (see env.local.template);
|
|
12
|
+
* `teams-app-manager.sh identity` writes them after registering the Entra app.
|
|
13
|
+
*/
|
|
14
|
+
const clientId = import.meta.env.VITE_CLIENT_ID ?? '';
|
|
15
|
+
const tenantId = import.meta.env.VITE_TENANT_ID ?? 'common';
|
|
16
|
+
|
|
17
|
+
const msalInstance = new PublicClientApplication({
|
|
18
|
+
auth: {
|
|
19
|
+
clientId,
|
|
20
|
+
authority: `https://login.microsoftonline.com/${tenantId}`,
|
|
21
|
+
redirectUri: window.location.origin,
|
|
22
|
+
},
|
|
23
|
+
cache: { cacheLocation: 'localStorage' },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export interface AuthState {
|
|
27
|
+
token: string | null;
|
|
28
|
+
account: AccountInfo | null;
|
|
29
|
+
inTeams: boolean;
|
|
30
|
+
loading: boolean;
|
|
31
|
+
error: Error | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useTeamsAuth(): AuthState & { login: () => Promise<void> } {
|
|
35
|
+
const [state, setState] = useState<AuthState>({
|
|
36
|
+
token: null,
|
|
37
|
+
account: null,
|
|
38
|
+
inTeams: false,
|
|
39
|
+
loading: true,
|
|
40
|
+
error: null,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const getToken = useCallback(async () => {
|
|
44
|
+
// 1. Teams SSO — works once the app registration exposes api://{host}/{clientId}
|
|
45
|
+
// with the Teams client ids pre-authorized.
|
|
46
|
+
try {
|
|
47
|
+
await app.initialize();
|
|
48
|
+
const ssoToken = await authentication.getAuthToken();
|
|
49
|
+
setState({ token: ssoToken, account: null, inTeams: true, loading: false, error: null });
|
|
50
|
+
return;
|
|
51
|
+
} catch {
|
|
52
|
+
// Not in Teams, or SSO not consented yet — fall through to MSAL.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 2. MSAL silent — a previous interactive sign-in left an account in the cache.
|
|
56
|
+
try {
|
|
57
|
+
await msalInstance.initialize();
|
|
58
|
+
const redirect = await msalInstance.handleRedirectPromise().catch(() => null);
|
|
59
|
+
const account = redirect?.account ?? msalInstance.getAllAccounts()[0];
|
|
60
|
+
if (account) {
|
|
61
|
+
const result = await msalInstance.acquireTokenSilent({
|
|
62
|
+
scopes: ['User.Read'],
|
|
63
|
+
account,
|
|
64
|
+
});
|
|
65
|
+
setState({
|
|
66
|
+
token: result.accessToken,
|
|
67
|
+
account: result.account,
|
|
68
|
+
inTeams: false,
|
|
69
|
+
loading: false,
|
|
70
|
+
error: null,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setState((prev) => ({ ...prev, loading: false }));
|
|
75
|
+
} catch (error) {
|
|
76
|
+
setState((prev) => ({ ...prev, loading: false, error: error as Error }));
|
|
77
|
+
}
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
/** Interactive sign-in. In Teams → auth bridge popup; in a browser → MSAL redirect. */
|
|
81
|
+
const login = useCallback(async () => {
|
|
82
|
+
if (!clientId) {
|
|
83
|
+
setState((prev) => ({
|
|
84
|
+
...prev,
|
|
85
|
+
error: new Error('VITE_CLIENT_ID is not set — copy env.local.template to .env.local'),
|
|
86
|
+
}));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let inTeams = false;
|
|
90
|
+
try {
|
|
91
|
+
await app.initialize();
|
|
92
|
+
inTeams = true;
|
|
93
|
+
} catch {
|
|
94
|
+
// Plain browser tab.
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (inTeams) {
|
|
99
|
+
// Teams opens auth.html in a real window where MSAL's redirect flow is
|
|
100
|
+
// allowed; the bridge calls notifySuccess/notifyFailure when done.
|
|
101
|
+
await authentication.authenticate({
|
|
102
|
+
url: `${window.location.origin}/auth.html`,
|
|
103
|
+
width: 600,
|
|
104
|
+
height: 535,
|
|
105
|
+
});
|
|
106
|
+
await getToken();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
await msalInstance.initialize();
|
|
110
|
+
await msalInstance.loginRedirect({ scopes: ['User.Read'] });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
setState((prev) => ({ ...prev, error: error as Error }));
|
|
113
|
+
}
|
|
114
|
+
}, [getToken]);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
void getToken();
|
|
118
|
+
}, [getToken]);
|
|
119
|
+
|
|
120
|
+
return { ...state, login };
|
|
121
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Text, MessageBar, MessageBarBody } from '@fluentui/react-components';
|
|
3
|
+
import { app, pages } from '@microsoft/teams-js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The tab-configuration page (manifest configurableTabs.configurationUrl → /#/config),
|
|
7
|
+
* shown when someone adds the app as an in-meeting tab. A meeting tab has no per-channel
|
|
8
|
+
* settings to collect, so this just registers a save handler that pins the content URL —
|
|
9
|
+
* the app itself picks the side-panel vs stage view from the Teams frameContext at runtime.
|
|
10
|
+
*/
|
|
11
|
+
export const Config: React.FC = () => {
|
|
12
|
+
const [inTeams, setInTeams] = React.useState(false);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
const init = async () => {
|
|
16
|
+
try {
|
|
17
|
+
await app.initialize();
|
|
18
|
+
setInTeams(true);
|
|
19
|
+
pages.config.registerOnSaveHandler((saveEvent) => {
|
|
20
|
+
// First-time configuration has no page id yet, so mint a unique entityId;
|
|
21
|
+
// it returns as context.page.id when the pinned tab loads.
|
|
22
|
+
const entityId = `meeting-${Date.now()}`;
|
|
23
|
+
void pages.config
|
|
24
|
+
.setConfig({
|
|
25
|
+
suggestedDisplayName: 'Meeting App',
|
|
26
|
+
entityId,
|
|
27
|
+
contentUrl: `${window.location.origin}/`,
|
|
28
|
+
websiteUrl: `${window.location.origin}/`,
|
|
29
|
+
removeUrl: `${window.location.origin}/#/remove`,
|
|
30
|
+
})
|
|
31
|
+
.then(() => saveEvent.notifySuccess())
|
|
32
|
+
.catch(() => saveEvent.notifyFailure('Failed to save the meeting tab configuration'));
|
|
33
|
+
});
|
|
34
|
+
void pages.config.setValidityState(true);
|
|
35
|
+
} catch {
|
|
36
|
+
setInTeams(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
void init();
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 16, maxWidth: 480 }}>
|
|
44
|
+
<Text size={500} weight="semibold">
|
|
45
|
+
Add to meeting
|
|
46
|
+
</Text>
|
|
47
|
+
<Text>
|
|
48
|
+
This app runs in the meeting side panel and can share content to the meeting stage. Select
|
|
49
|
+
Save to add it.
|
|
50
|
+
</Text>
|
|
51
|
+
{!inTeams && (
|
|
52
|
+
<MessageBar intent="info">
|
|
53
|
+
<MessageBarBody>
|
|
54
|
+
Running outside Teams — add this app from within a Teams meeting to configure it.
|
|
55
|
+
</MessageBarBody>
|
|
56
|
+
</MessageBar>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Text } from '@fluentui/react-components';
|
|
3
|
+
|
|
4
|
+
/** Shown by Teams when the tab is removed (configurableTabs removeUrl). */
|
|
5
|
+
export const Remove: React.FC = () => (
|
|
6
|
+
<div style={{ padding: 16 }}>
|
|
7
|
+
<Text>Removing this tab removes the app from this meeting. The app itself stays installed.</Text>
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
// Teams tabs must be served over HTTPS; `vite --https` or a tunnel in dev.
|
|
8
|
+
server: { port: 3000 },
|
|
9
|
+
build: {
|
|
10
|
+
outDir: 'dist',
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
// MPA: the tab plus the auth bridge Teams opens for interactive sign-in.
|
|
13
|
+
input: {
|
|
14
|
+
main: fileURLToPath(new URL('./index.html', import.meta.url)),
|
|
15
|
+
auth: fileURLToPath(new URL('./auth.html', import.meta.url)),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# Power Pages Application
|
|
2
|
-
|
|
3
|
-
This is a Power Pages application built with Dynamics UI Kit components.
|
|
4
|
-
|
|
5
|
-
## Getting Started
|
|
6
|
-
|
|
7
|
-
1. **Environment Setup**
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
cp .env.example .env
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Edit `.env` and set your `PORTAL_URL` to your Power Pages portal URL.
|
|
14
|
-
|
|
15
|
-
2. **Install Dependencies**
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
3. **Development**
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm run dev
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
This starts the development server at http://localhost:3000
|
|
28
|
-
|
|
29
|
-
4. **Build for Production**
|
|
30
|
-
```bash
|
|
31
|
-
npm run build
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Project Structure
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
src/
|
|
38
|
-
├── components/ # React components
|
|
39
|
-
│ ├── ContactForm.tsx # Main contact form component
|
|
40
|
-
│ └── ContactForm.css # Component styles
|
|
41
|
-
├── providers/ # Context providers
|
|
42
|
-
│ └── PowerPagesProvider.tsx # Power Pages API provider
|
|
43
|
-
├── styles/ # Global styles
|
|
44
|
-
│ └── index.css # Global CSS
|
|
45
|
-
└── index.tsx # Application entry point
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Features
|
|
49
|
-
|
|
50
|
-
- **Contact Management**: Create and manage contacts using Dynamics UI Kit components
|
|
51
|
-
- **Power Pages Integration**: Seamless integration with Power Pages API
|
|
52
|
-
- **Responsive Design**: Mobile-friendly responsive layout
|
|
53
|
-
- **Type Safety**: Full TypeScript support
|
|
54
|
-
|
|
55
|
-
## Deployment
|
|
56
|
-
|
|
57
|
-
To deploy your application to Power Pages:
|
|
58
|
-
|
|
59
|
-
1. Build the application:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npm run build
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
2. Upload the `dist` folder contents to your Power Pages portal's web files
|
|
66
|
-
|
|
67
|
-
3. Create a new web page in Power Pages and reference your built JavaScript and CSS files
|
|
68
|
-
|
|
69
|
-
## Customization
|
|
70
|
-
|
|
71
|
-
### Adding New Components
|
|
72
|
-
|
|
73
|
-
1. Create your component in `src/components/`
|
|
74
|
-
2. Use Dynamics UI Kit components for consistent styling
|
|
75
|
-
3. Import and use the `usePowerPagesApi` hook for data operations
|
|
76
|
-
|
|
77
|
-
### Styling
|
|
78
|
-
|
|
79
|
-
- Global styles are in `src/styles/index.css`
|
|
80
|
-
- Component-specific styles should be co-located with components
|
|
81
|
-
- Follow the existing CSS naming conventions
|
|
82
|
-
|
|
83
|
-
## Available Scripts
|
|
84
|
-
|
|
85
|
-
- `npm run dev` - Start development server
|
|
86
|
-
- `npm run build` - Build for production
|
|
87
|
-
- `npm run typecheck` - Type check without building
|
|
88
|
-
- `npm run clean` - Clean build directory
|
|
89
|
-
- `npm run lint` - Lint code
|
|
90
|
-
|
|
91
|
-
## Learn More
|
|
92
|
-
|
|
93
|
-
- [Dynamics UI Kit Documentation](https://github.com/your-org/dynamics-ui-kit)
|
|
94
|
-
- [Power Pages Documentation](https://docs.microsoft.com/power-pages/)
|
|
95
|
-
- [React Documentation](https://reactjs.org/)
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "power-pages-app",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"description": "Power Pages application built with Dynamics UI Kit",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"build": "webpack --mode=production",
|
|
8
|
-
"dev": "webpack serve --mode=development",
|
|
9
|
-
"typecheck": "tsc --noEmit",
|
|
10
|
-
"clean": "rimraf dist",
|
|
11
|
-
"lint": "eslint src --ext .ts,.tsx"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@khester/dynamics-ui-api-client": "^1.0.0",
|
|
15
|
-
"@khester/dynamics-ui-components": "^1.0.0",
|
|
16
|
-
"react": "^18.2.0",
|
|
17
|
-
"react-dom": "^18.2.0"
|
|
18
|
-
},
|
|
19
|
-
"devDependencies": {
|
|
20
|
-
"@types/react": "^18.2.0",
|
|
21
|
-
"@types/react-dom": "^18.2.0",
|
|
22
|
-
"css-loader": "^6.8.1",
|
|
23
|
-
"html-webpack-plugin": "^5.5.3",
|
|
24
|
-
"style-loader": "^3.3.3",
|
|
25
|
-
"ts-loader": "^9.5.1",
|
|
26
|
-
"typescript": "^5.3.3",
|
|
27
|
-
"webpack": "^5.89.0",
|
|
28
|
-
"webpack-cli": "^5.1.4",
|
|
29
|
-
"webpack-dev-server": "^4.15.1"
|
|
30
|
-
},
|
|
31
|
-
"browserslist": {
|
|
32
|
-
"production": [
|
|
33
|
-
">0.2%",
|
|
34
|
-
"not dead",
|
|
35
|
-
"not op_mini all"
|
|
36
|
-
],
|
|
37
|
-
"development": [
|
|
38
|
-
"last 1 chrome version",
|
|
39
|
-
"last 1 firefox version",
|
|
40
|
-
"last 1 safari version"
|
|
41
|
-
]
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<link rel="icon" href="data:," />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
-
<meta name="theme-color" content="#000000" />
|
|
8
|
-
<meta name="description" content="Power Pages Application built with Dynamics UI Kit" />
|
|
9
|
-
<title>Power Pages App - Dynamics UI Kit</title>
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
13
|
-
<div id="root"></div>
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|