@khester/create-dynamics-app 2.10.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 +28 -3
- package/dist/artifacts/registry.d.ts +7 -5
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +30 -13
- 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 +7 -5
- 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/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,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { IApiService } from "./core/services/IApiService";
|
|
3
|
+
import { MockApiService } from "./core/services/MockApiService";
|
|
4
|
+
import { PortalSeamService } from "@dataverse-kit/portal-api-service";
|
|
5
|
+
import { Component } from "./Component";
|
|
6
|
+
|
|
7
|
+
// A Power Pages island is served from the portal origin, so a same-origin `/_api`
|
|
8
|
+
// fetch is authenticated by the portal session cookie + anti-forgery token —
|
|
9
|
+
// PortalSeamService handles both (token fetch/cache, 403-retry, idempotency-aware
|
|
10
|
+
// retry). On localhost (the webpack dev server) fall back to the in-memory mock so
|
|
11
|
+
// it renders with no portal.
|
|
12
|
+
//
|
|
13
|
+
// PortalSeamService implements the create-app 6-method `dataverse-seam`, so it is
|
|
14
|
+
// structurally assignable to IApiService with NO cast — and THIS assignment is the
|
|
15
|
+
// load-bearing compile-time drift guard: if the seam shapes ever diverge, this line
|
|
16
|
+
// fails `tsc`. (See core/services/portalSeam.assert.ts for the explicit fixture.)
|
|
17
|
+
export const App: React.FC<{ island?: string }> = () => {
|
|
18
|
+
// `island` is the `data-dvkit-island` value — a hook for routing multiple widgets
|
|
19
|
+
// on a page. The starter renders the accounts widget for any island.
|
|
20
|
+
const api = React.useMemo<IApiService>(() => {
|
|
21
|
+
const onLocalhost = /^(localhost|127\.0\.0\.1)$/.test(window.location.hostname);
|
|
22
|
+
return onLocalhost ? new MockApiService() : new PortalSeamService();
|
|
23
|
+
}, []);
|
|
24
|
+
return <Component api={api} />;
|
|
25
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
DetailsList,
|
|
4
|
+
SelectionMode,
|
|
5
|
+
MessageBar,
|
|
6
|
+
MessageBarType,
|
|
7
|
+
Spinner,
|
|
8
|
+
Stack,
|
|
9
|
+
Text,
|
|
10
|
+
type IColumn,
|
|
11
|
+
} from "@fluentui/react";
|
|
12
|
+
import { initializeIcons } from "@fluentui/react/lib/Icons";
|
|
13
|
+
import type { IApiService } from "./core/services/IApiService";
|
|
14
|
+
|
|
15
|
+
// Initialize Fluent UI icons once (DetailsList sort/column glyphs).
|
|
16
|
+
initializeIcons();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The ONE host-agnostic component. It depends on nothing but an injected
|
|
20
|
+
* `IApiService` — no PCF `context`, no Xrm, no lifecycle, no bundler globals — so
|
|
21
|
+
* the SAME source renders unchanged inside a PCF control and a Dynamics custom
|
|
22
|
+
* page. Each host's thin wrapper builds the right `IApiService` and mounts this.
|
|
23
|
+
*/
|
|
24
|
+
export interface ComponentProps {
|
|
25
|
+
api: IApiService;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ACCOUNTS_FETCHXML = [
|
|
29
|
+
'<fetch top="25">',
|
|
30
|
+
' <entity name="account">',
|
|
31
|
+
' <attribute name="accountid" />',
|
|
32
|
+
' <attribute name="name" />',
|
|
33
|
+
' <attribute name="telephone1" />',
|
|
34
|
+
' <order attribute="name" />',
|
|
35
|
+
" </entity>",
|
|
36
|
+
"</fetch>",
|
|
37
|
+
].join("\n");
|
|
38
|
+
|
|
39
|
+
interface AccountRow {
|
|
40
|
+
accountid: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
telephone1?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const COLUMNS: IColumn[] = [
|
|
46
|
+
{ key: "name", name: "Account", fieldName: "name", minWidth: 180, maxWidth: 320, isResizable: true },
|
|
47
|
+
{ key: "telephone1", name: "Phone", fieldName: "telephone1", minWidth: 140, isResizable: true },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const Component: React.FC<ComponentProps> = ({ api }) => {
|
|
51
|
+
const [rows, setRows] = React.useState<AccountRow[] | null>(null);
|
|
52
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
53
|
+
|
|
54
|
+
React.useEffect(() => {
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
api
|
|
57
|
+
.retrieveMultipleRecords("accounts", ACCOUNTS_FETCHXML)
|
|
58
|
+
.then((res) => {
|
|
59
|
+
if (!cancelled) setRows(res.entities as AccountRow[]);
|
|
60
|
+
})
|
|
61
|
+
.catch((e) => {
|
|
62
|
+
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
cancelled = true;
|
|
66
|
+
};
|
|
67
|
+
}, [api]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Stack tokens={{ childrenGap: 12 }} styles={{ root: { padding: 16 } }}>
|
|
71
|
+
<Text variant="xLarge">Accounts</Text>
|
|
72
|
+
<Text variant="small">
|
|
73
|
+
One host-agnostic component — the same source renders in a PCF control and a
|
|
74
|
+
Dynamics custom page; only the thin host wrapper changes.
|
|
75
|
+
</Text>
|
|
76
|
+
{error && <MessageBar messageBarType={MessageBarType.error}>{error}</MessageBar>}
|
|
77
|
+
{!rows && !error && <Spinner label="Loading accounts…" />}
|
|
78
|
+
{rows && (
|
|
79
|
+
<DetailsList
|
|
80
|
+
items={rows}
|
|
81
|
+
columns={COLUMNS}
|
|
82
|
+
setKey="accounts"
|
|
83
|
+
getKey={(item: AccountRow) => item.accountid}
|
|
84
|
+
selectionMode={SelectionMode.none}
|
|
85
|
+
/>
|
|
86
|
+
)}
|
|
87
|
+
</Stack>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { IApiService } from "./IApiService";
|
|
2
|
+
import { logCrud } from "./crudLogging";
|
|
3
|
+
|
|
4
|
+
// A few seeded in-memory accounts so the control/page renders with no org or
|
|
5
|
+
// token (the PCF test harness and `npm run dev` both land here). Replace this
|
|
6
|
+
// seed as your component grows. The other IApiService impls (Xrm/Fetch) and the
|
|
7
|
+
// shared contract are pulled from templates/_shared/dataverse-seam at scaffold.
|
|
8
|
+
const SEED_ACCOUNTS = [
|
|
9
|
+
{ accountid: "00000000-0000-0000-0000-000000000001", name: "Contoso Ltd", telephone1: "+1 (425) 555-0100" },
|
|
10
|
+
{ accountid: "00000000-0000-0000-0000-000000000002", name: "Fabrikam Inc", telephone1: "+1 (206) 555-0140" },
|
|
11
|
+
{ accountid: "00000000-0000-0000-0000-000000000003", name: "Adventure Works", telephone1: "+1 (312) 555-0190" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export class MockApiService implements IApiService {
|
|
15
|
+
private accounts: Record<string, any> = Object.fromEntries(
|
|
16
|
+
SEED_ACCOUNTS.map((a) => [a.accountid, { ...a }]),
|
|
17
|
+
);
|
|
18
|
+
private nextId = SEED_ACCOUNTS.length + 1;
|
|
19
|
+
|
|
20
|
+
retrieveMultipleRecords(entity: string): Promise<{ entities: any[] }> {
|
|
21
|
+
return logCrud(
|
|
22
|
+
{ op: "READ", entity, resultCount: (r) => r?.entities?.length },
|
|
23
|
+
async () => ({
|
|
24
|
+
entities: entity === "accounts" ? Object.values(this.accounts) : [],
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
createRecord(entity: string, record: any): Promise<any> {
|
|
30
|
+
return logCrud({ op: "CREATE", entity, resultId: (r) => r?.id }, async () => {
|
|
31
|
+
const id = `00000000-0000-0000-0000-${String(this.nextId++).padStart(12, "0")}`;
|
|
32
|
+
if (entity === "accounts") this.accounts[id] = { accountid: id, ...record };
|
|
33
|
+
return { id };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
updateRecord(entity: string, id: string, record: any): Promise<any> {
|
|
38
|
+
return logCrud({ op: "UPDATE", entity, id }, async () => {
|
|
39
|
+
const key = id.replace(/[{}]/g, "");
|
|
40
|
+
if (this.accounts[key]) this.accounts[key] = { ...this.accounts[key], ...record };
|
|
41
|
+
return { success: true };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
deleteRecord(entity: string, id: string): Promise<void> {
|
|
46
|
+
return logCrud({ op: "DELETE", entity, id }, async () => {
|
|
47
|
+
delete this.accounts[id.replace(/[{}]/g, "")];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
executeRequest(requestName: string): Promise<any> {
|
|
52
|
+
return logCrud({ op: "EXECUTE", entity: requestName }, async () => ({}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
associateRecord(entity: string, id: string): Promise<void> {
|
|
56
|
+
return logCrud({ op: "ASSOCIATE", entity, id }, async () => undefined);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Compile-time drift guard (typechecked by `npm run build` → `tsc --noEmit`, and by
|
|
2
|
+
// the create-dynamics-app power-pages-island template build e2e). `PortalSeamService`
|
|
3
|
+
// — the create-app 6-method `dataverse-seam` adapter shipped in
|
|
4
|
+
// `@dataverse-kit/portal-api-service` — MUST stay structurally assignable to THIS
|
|
5
|
+
// template's local seam `IApiService`, so `App.tsx` can inject it with no cast. That
|
|
6
|
+
// package can't import the create-app template interface, so the assertion lives here,
|
|
7
|
+
// where both types resolve. Type-only — no runtime, tree-shaken from the bundle.
|
|
8
|
+
import type { IApiService } from "./IApiService";
|
|
9
|
+
import type { PortalSeamService } from "@dataverse-kit/portal-api-service";
|
|
10
|
+
|
|
11
|
+
// If PortalSeamService drifts from the seam shape, `true` is no longer assignable to
|
|
12
|
+
// `never` and this fails to compile.
|
|
13
|
+
type PortalSeamSatisfiesSeam = PortalSeamService extends IApiService ? true : never;
|
|
14
|
+
export const _portalSeamDriftGuard: PortalSeamSatisfiesSeam = true;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Webpack/ts-loader builds have no `vite/client` types. The shared data-access seam
|
|
2
|
+
// (templates/_shared/dataverse-seam) includes ServiceFactory, which reads
|
|
3
|
+
// `import.meta.env.VITE_USE_PROXY` for the Vite custom-page host. This Power Pages
|
|
4
|
+
// island does NOT use ServiceFactory (App.tsx builds MockApiService / PortalSeamService
|
|
5
|
+
// directly), so it is tree-shaken from the bundle — but `tsc` / ts-loader still
|
|
6
|
+
// typecheck it. This minimal ambient declaration lets that unused file compile here.
|
|
7
|
+
//
|
|
8
|
+
// NOTE: this file MUST live under src/ — the tsconfig is `include: ["src"]`, so a
|
|
9
|
+
// project-root *.d.ts would be excluded and the shim would be missed.
|
|
10
|
+
interface ImportMeta {
|
|
11
|
+
readonly env: Record<string, unknown>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { initLogging } from "@khester/reusable-components";
|
|
4
|
+
import { App } from "./App";
|
|
5
|
+
|
|
6
|
+
// Wire up the localhost-only troubleshooting hooks (window.dumpAppLogs()); no-op in a
|
|
7
|
+
// live portal.
|
|
8
|
+
initLogging();
|
|
9
|
+
|
|
10
|
+
// A traditional Power Pages portal is server-rendered (Liquid). This bundle is an
|
|
11
|
+
// ISLAND: it hydrates React ONLY into the nodes a maker tags in Liquid, leaving the
|
|
12
|
+
// rest of the page untouched — the delta the MS code-site scaffolder (whole-page SPA)
|
|
13
|
+
// can't do. Tag a mount point with `data-dvkit-island="<name>"` (the name is a hook
|
|
14
|
+
// for future per-widget routing) and reference this web file:
|
|
15
|
+
//
|
|
16
|
+
// <div data-dvkit-island="accounts"></div>
|
|
17
|
+
// <script src="/{{projectName}}.js"></script>
|
|
18
|
+
//
|
|
19
|
+
const SELECTOR = "[data-dvkit-island]";
|
|
20
|
+
|
|
21
|
+
function mountIslands(): void {
|
|
22
|
+
document.querySelectorAll<HTMLElement>(SELECTOR).forEach((node) => {
|
|
23
|
+
// Idempotent — a partial-page refresh or a duplicated include must not double-mount.
|
|
24
|
+
if (node.dataset.dvkitMounted === "true") return;
|
|
25
|
+
node.dataset.dvkitMounted = "true";
|
|
26
|
+
createRoot(node).render(
|
|
27
|
+
<React.StrictMode>
|
|
28
|
+
<App island={node.dataset.dvkitIsland} />
|
|
29
|
+
</React.StrictMode>,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (document.readyState === "loading") {
|
|
35
|
+
document.addEventListener("DOMContentLoaded", mountIslands);
|
|
36
|
+
} else {
|
|
37
|
+
mountIslands();
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
12
|
+
"types": []
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
3
|
+
|
|
4
|
+
// A Power Pages ISLAND: one self-contained bundle, uploaded as a portal WEB FILE and
|
|
5
|
+
// referenced from a Liquid page (see README). The output filename is STABLE (no
|
|
6
|
+
// contenthash) so the Liquid `<script src>` URL stays fixed across rebuilds.
|
|
7
|
+
// HtmlWebpackPlugin exists only for the local `webpack serve` preview — on a real
|
|
8
|
+
// portal the Liquid page is the host, so the emitted index.html is never uploaded.
|
|
9
|
+
module.exports = {
|
|
10
|
+
entry: './src/islands.tsx',
|
|
11
|
+
output: {
|
|
12
|
+
path: path.resolve(__dirname, 'dist'),
|
|
13
|
+
filename: '{{projectName}}.js',
|
|
14
|
+
clean: true,
|
|
15
|
+
},
|
|
16
|
+
resolve: { extensions: ['.ts', '.tsx', '.js'] },
|
|
17
|
+
module: {
|
|
18
|
+
rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }],
|
|
19
|
+
},
|
|
20
|
+
plugins: [new HtmlWebpackPlugin({ template: './public/index.html' })],
|
|
21
|
+
devServer: { static: './dist', port: 8080, hot: true },
|
|
22
|
+
};
|
|
@@ -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>
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
.contact-form {
|
|
2
|
-
max-width: 800px;
|
|
3
|
-
margin: 0 auto;
|
|
4
|
-
padding: 24px;
|
|
5
|
-
background: #ffffff;
|
|
6
|
-
border-radius: 8px;
|
|
7
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.contact-form__header {
|
|
11
|
-
margin-bottom: 24px;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.contact-form__title {
|
|
15
|
-
margin: 0;
|
|
16
|
-
font-size: 24px;
|
|
17
|
-
font-weight: 600;
|
|
18
|
-
color: #323130;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.contact-form__content {
|
|
22
|
-
margin-bottom: 24px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.contact-form__row {
|
|
26
|
-
display: grid;
|
|
27
|
-
grid-template-columns: 1fr 1fr;
|
|
28
|
-
gap: 16px;
|
|
29
|
-
margin-bottom: 16px;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.contact-form__field {
|
|
33
|
-
display: flex;
|
|
34
|
-
flex-direction: column;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.contact-form__actions {
|
|
38
|
-
display: flex;
|
|
39
|
-
gap: 12px;
|
|
40
|
-
justify-content: flex-start;
|
|
41
|
-
margin-top: 24px;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.contact-form__success {
|
|
45
|
-
padding: 12px 16px;
|
|
46
|
-
margin-bottom: 16px;
|
|
47
|
-
background-color: #dff6dd;
|
|
48
|
-
border: 1px solid #107c10;
|
|
49
|
-
border-radius: 4px;
|
|
50
|
-
color: #107c10;
|
|
51
|
-
font-weight: 500;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.contact-form__error {
|
|
55
|
-
padding: 12px 16px;
|
|
56
|
-
margin-bottom: 16px;
|
|
57
|
-
background-color: #fef7f1;
|
|
58
|
-
border: 1px solid #d13438;
|
|
59
|
-
border-radius: 4px;
|
|
60
|
-
color: #d13438;
|
|
61
|
-
font-weight: 500;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.contact-form__auth-notice {
|
|
65
|
-
margin-top: 16px;
|
|
66
|
-
padding: 12px 16px;
|
|
67
|
-
background-color: #fff4ce;
|
|
68
|
-
border: 1px solid #ffb900;
|
|
69
|
-
border-radius: 4px;
|
|
70
|
-
color: #8a6d00;
|
|
71
|
-
text-align: center;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
@media (max-width: 768px) {
|
|
75
|
-
.contact-form {
|
|
76
|
-
padding: 16px;
|
|
77
|
-
margin: 16px;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.contact-form__row {
|
|
81
|
-
grid-template-columns: 1fr;
|
|
82
|
-
gap: 12px;
|
|
83
|
-
}
|
|
84
|
-
}
|