nl-d365boilerplate-vite 1.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/.env.example +22 -0
- package/README.md +75 -0
- package/bin/cli.js +84 -0
- package/components.json +22 -0
- package/eslint.config.js +23 -0
- package/getToken.js +197 -0
- package/index.html +30 -0
- package/package.json +69 -0
- package/src/App.tsx +28 -0
- package/src/assets/images/novalogica-logo.svg +24 -0
- package/src/components/nl-header.tsx +165 -0
- package/src/components/page-layout.tsx +78 -0
- package/src/components/page-render.tsx +16 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button.tsx +165 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +156 -0
- package/src/components/ui/input.tsx +23 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/theme-toggle.tsx +28 -0
- package/src/config/pages.config.ts +34 -0
- package/src/contexts/dataverse-context.tsx +12 -0
- package/src/contexts/navigation-context.tsx +14 -0
- package/src/hooks/useAccounts.ts +194 -0
- package/src/hooks/useDataverse.ts +11 -0
- package/src/hooks/useNavigation.ts +41 -0
- package/src/index.css +147 -0
- package/src/lib/nav-items.ts +25 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +12 -0
- package/src/pages/Demo.tsx +465 -0
- package/src/pages/Documentation.tsx +850 -0
- package/src/pages/Home.tsx +132 -0
- package/src/pages/index.ts +4 -0
- package/src/providers/dataverse-provider.tsx +81 -0
- package/src/providers/navigation-provider.tsx +33 -0
- package/src/providers/theme-provider.tsx +92 -0
- package/src/public/novalogica-logo.svg +24 -0
- package/tsconfig.app.json +32 -0
- package/tsconfig.json +17 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +26 -0
package/.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ==========================================
|
|
2
|
+
# novalogica Dynamics 365 React Boilerplate
|
|
3
|
+
# Environment Configuration Template
|
|
4
|
+
# ==========================================
|
|
5
|
+
|
|
6
|
+
# Your D365 organization URL
|
|
7
|
+
VITE_D365_API_URL=https://your-org.crm4.dynamics.com
|
|
8
|
+
|
|
9
|
+
# Client ID from your Azure App Registration
|
|
10
|
+
CLIENT_ID=your-client-id-here
|
|
11
|
+
|
|
12
|
+
# OAuth URL (usually no changes needed)
|
|
13
|
+
AUTH_URL=https://login.microsoftonline.com/common/oauth2/authorize?resource=https://your-org.crm4.dynamics.com
|
|
14
|
+
|
|
15
|
+
CALLBACK_URL=https://callbackurl
|
|
16
|
+
|
|
17
|
+
# Your D365 access token (this will be automatically managed by the app)
|
|
18
|
+
VITE_D365_TOKEN=your-access-token-here
|
|
19
|
+
|
|
20
|
+
# Token expiration (automatically managed)
|
|
21
|
+
EXPIRES_IN=4411
|
|
22
|
+
TOKEN_TIMESTAMP=0
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# React + TypeScript + Vite
|
|
2
|
+
|
|
3
|
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
4
|
+
|
|
5
|
+
Currently, two official plugins are available:
|
|
6
|
+
|
|
7
|
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
|
8
|
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
9
|
+
|
|
10
|
+
## React Compiler
|
|
11
|
+
|
|
12
|
+
The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
|
|
13
|
+
|
|
14
|
+
Note: This will impact Vite dev & build performances.
|
|
15
|
+
|
|
16
|
+
## Expanding the ESLint configuration
|
|
17
|
+
|
|
18
|
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
export default defineConfig([
|
|
22
|
+
globalIgnores(['dist']),
|
|
23
|
+
{
|
|
24
|
+
files: ['**/*.{ts,tsx}'],
|
|
25
|
+
extends: [
|
|
26
|
+
// Other configs...
|
|
27
|
+
|
|
28
|
+
// Remove tseslint.configs.recommended and replace with this
|
|
29
|
+
tseslint.configs.recommendedTypeChecked,
|
|
30
|
+
// Alternatively, use this for stricter rules
|
|
31
|
+
tseslint.configs.strictTypeChecked,
|
|
32
|
+
// Optionally, add this for stylistic rules
|
|
33
|
+
tseslint.configs.stylisticTypeChecked,
|
|
34
|
+
|
|
35
|
+
// Other configs...
|
|
36
|
+
],
|
|
37
|
+
languageOptions: {
|
|
38
|
+
parserOptions: {
|
|
39
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
40
|
+
tsconfigRootDir: import.meta.dirname,
|
|
41
|
+
},
|
|
42
|
+
// other options...
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
// eslint.config.js
|
|
52
|
+
import reactX from 'eslint-plugin-react-x'
|
|
53
|
+
import reactDom from 'eslint-plugin-react-dom'
|
|
54
|
+
|
|
55
|
+
export default defineConfig([
|
|
56
|
+
globalIgnores(['dist']),
|
|
57
|
+
{
|
|
58
|
+
files: ['**/*.{ts,tsx}'],
|
|
59
|
+
extends: [
|
|
60
|
+
// Other configs...
|
|
61
|
+
// Enable lint rules for React
|
|
62
|
+
reactX.configs['recommended-typescript'],
|
|
63
|
+
// Enable lint rules for React DOM
|
|
64
|
+
reactDom.configs.recommended,
|
|
65
|
+
],
|
|
66
|
+
languageOptions: {
|
|
67
|
+
parserOptions: {
|
|
68
|
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
69
|
+
tsconfigRootDir: import.meta.dirname,
|
|
70
|
+
},
|
|
71
|
+
// other options...
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
|
|
10
|
+
const targetDirName = process.argv[2];
|
|
11
|
+
|
|
12
|
+
if (!targetDirName) {
|
|
13
|
+
console.error("Please specify the project directory:");
|
|
14
|
+
console.error(" npm create nl-d365boilerplate-vite <project-directory>");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const targetPath = path.join(process.cwd(), targetDirName);
|
|
19
|
+
const templatePath = path.join(__dirname, "..");
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(targetPath)) {
|
|
22
|
+
console.error(`Directory ${targetDirName} already exists.`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(`Creating a new app in ${targetPath}...`);
|
|
27
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
28
|
+
|
|
29
|
+
const SKIP_FILES = [
|
|
30
|
+
"node_modules",
|
|
31
|
+
".git",
|
|
32
|
+
".env",
|
|
33
|
+
"package-lock.json",
|
|
34
|
+
"bin",
|
|
35
|
+
"dist",
|
|
36
|
+
".npmignore",
|
|
37
|
+
".DS_Store",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function copyDir(src, dest) {
|
|
41
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
42
|
+
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const srcPath = path.join(src, entry.name);
|
|
45
|
+
const destPath = path.join(dest, entry.name);
|
|
46
|
+
|
|
47
|
+
if (SKIP_FILES.includes(entry.name)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
53
|
+
copyDir(srcPath, destPath);
|
|
54
|
+
} else {
|
|
55
|
+
fs.copyFileSync(srcPath, destPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Copy files
|
|
61
|
+
copyDir(templatePath, targetPath);
|
|
62
|
+
|
|
63
|
+
// Update package.json
|
|
64
|
+
const pkgJsonPath = path.join(targetPath, "package.json");
|
|
65
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
66
|
+
|
|
67
|
+
pkg.name = targetDirName;
|
|
68
|
+
pkg.version = "0.0.0";
|
|
69
|
+
pkg.private = true;
|
|
70
|
+
delete pkg.bin;
|
|
71
|
+
delete pkg.files;
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
74
|
+
|
|
75
|
+
console.log("\nSuccess! Created project at " + targetPath);
|
|
76
|
+
console.log("Inside that directory, you can run several commands:\n");
|
|
77
|
+
console.log(" npm install");
|
|
78
|
+
console.log(" Installs dependencies.\n");
|
|
79
|
+
console.log(" npm run dev");
|
|
80
|
+
console.log(" Starts the development server.\n");
|
|
81
|
+
console.log("\nWe suggest that you begin by typing:\n");
|
|
82
|
+
console.log(` cd ${targetDirName}`);
|
|
83
|
+
console.log(" npm install");
|
|
84
|
+
console.log(" npm run dev");
|
package/components.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/index.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"registries": {}
|
|
22
|
+
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
package/getToken.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataverse Auth Token Manager
|
|
3
|
+
* Handles automatic OAuth authentication, token validation, and renewal
|
|
4
|
+
* Usage: node getToken.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import puppeteer from "puppeteer";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import readline from "readline";
|
|
10
|
+
import dotenv from "dotenv";
|
|
11
|
+
|
|
12
|
+
dotenv.config();
|
|
13
|
+
|
|
14
|
+
const CONFIG = {
|
|
15
|
+
AUTH_URL: process.env.AUTH_URL,
|
|
16
|
+
CALLBACK_URL: process.env.CALLBACK_URL,
|
|
17
|
+
SCOPE: process.env.SCOPE || "",
|
|
18
|
+
EXPIRY_MARGIN: 60,
|
|
19
|
+
BROWSER_TIMEOUT: 300000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isTokenValid() {
|
|
23
|
+
const { VITE_D365_TOKEN, EXPIRES_IN, TOKEN_TIMESTAMP } = process.env;
|
|
24
|
+
|
|
25
|
+
if (!VITE_D365_TOKEN || !EXPIRES_IN || !TOKEN_TIMESTAMP) {
|
|
26
|
+
console.log("⚠️ No valid token found");
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
31
|
+
const expirationTime =
|
|
32
|
+
parseInt(TOKEN_TIMESTAMP) + parseInt(EXPIRES_IN) - CONFIG.EXPIRY_MARGIN;
|
|
33
|
+
const isValid = currentTime < expirationTime;
|
|
34
|
+
|
|
35
|
+
if (isValid) {
|
|
36
|
+
const remainingMinutes = Math.floor((expirationTime - currentTime) / 60);
|
|
37
|
+
console.log(`ℹ️ Token valid (${remainingMinutes} minutes remaining)`);
|
|
38
|
+
} else {
|
|
39
|
+
console.log("⚠️ Token expired - renewal needed");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return isValid;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateEnvFile(updates) {
|
|
46
|
+
const envPath = ".env";
|
|
47
|
+
let envContent = fs.existsSync(envPath)
|
|
48
|
+
? fs.readFileSync(envPath, "utf8")
|
|
49
|
+
: "";
|
|
50
|
+
|
|
51
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
52
|
+
const regex = new RegExp(`^${key}=.*`, "m");
|
|
53
|
+
const newLine = `${key}=${value}`;
|
|
54
|
+
|
|
55
|
+
envContent = envContent.match(regex)
|
|
56
|
+
? envContent.replace(regex, newLine)
|
|
57
|
+
: envContent + `\n${newLine}`;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
fs.writeFileSync(envPath, envContent.trim() + "\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function promptUser(query) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
const rl = readline.createInterface({
|
|
66
|
+
input: process.stdin,
|
|
67
|
+
output: process.stdout,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
rl.question(query, (answer) => {
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(answer.trim());
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function ensureClientId() {
|
|
78
|
+
let clientId = process.env.CLIENT_ID;
|
|
79
|
+
|
|
80
|
+
if (!clientId) {
|
|
81
|
+
console.log(
|
|
82
|
+
"🔑 CLIENT_ID required - find it in Azure Portal > App Registrations",
|
|
83
|
+
);
|
|
84
|
+
clientId = await promptUser(
|
|
85
|
+
"Enter your Azure App Registration Client ID: ",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!clientId) {
|
|
89
|
+
throw new Error("CLIENT_ID is required for OAuth authentication");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateEnvFile({ CLIENT_ID: clientId });
|
|
93
|
+
console.log("✅ CLIENT_ID saved to .env file");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return clientId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildAuthUrl(clientId) {
|
|
100
|
+
const url = new URL(CONFIG.AUTH_URL);
|
|
101
|
+
url.searchParams.set("client_id", clientId);
|
|
102
|
+
url.searchParams.set("redirect_uri", CONFIG.CALLBACK_URL);
|
|
103
|
+
url.searchParams.set("response_type", "token");
|
|
104
|
+
url.searchParams.set("scope", CONFIG.SCOPE);
|
|
105
|
+
return url.toString();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractTokenFromUrl(url) {
|
|
109
|
+
const tokenMatch = url.match(/access_token=([^&]+)/);
|
|
110
|
+
const expiresMatch = url.match(/expires_in=([^&]+)/);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
token: tokenMatch ? decodeURIComponent(tokenMatch[1]) : null,
|
|
114
|
+
expiresIn: expiresMatch ? parseInt(expiresMatch[1], 10) : null,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function performBrowserAuth(authUrl) {
|
|
119
|
+
console.log("🔄 Opening browser for OAuth authentication...");
|
|
120
|
+
console.log("ℹ️ Complete the sign-in process in the browser");
|
|
121
|
+
|
|
122
|
+
const browser = await puppeteer.launch({
|
|
123
|
+
headless: false,
|
|
124
|
+
defaultViewport: null,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const [page] = await browser.pages();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
131
|
+
const timeout = setTimeout(() => {
|
|
132
|
+
reject(new Error("Authentication timeout"));
|
|
133
|
+
}, CONFIG.BROWSER_TIMEOUT);
|
|
134
|
+
|
|
135
|
+
page.on("request", (request) => {
|
|
136
|
+
const reqUrl = request.url();
|
|
137
|
+
|
|
138
|
+
if (reqUrl.startsWith(CONFIG.CALLBACK_URL)) {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
console.log("✅ OAuth callback detected");
|
|
141
|
+
|
|
142
|
+
const { token, expiresIn } = extractTokenFromUrl(reqUrl);
|
|
143
|
+
|
|
144
|
+
if (token && expiresIn) {
|
|
145
|
+
resolve({ token, expiresIn });
|
|
146
|
+
} else {
|
|
147
|
+
reject(new Error("Failed to extract token from callback"));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await page.goto(authUrl, { waitUntil: "networkidle2" });
|
|
154
|
+
return await tokenPromise;
|
|
155
|
+
} finally {
|
|
156
|
+
await browser.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function authenticateUser() {
|
|
161
|
+
const clientId = await ensureClientId();
|
|
162
|
+
const authUrl = buildAuthUrl(clientId);
|
|
163
|
+
const { token, expiresIn } = await performBrowserAuth(authUrl);
|
|
164
|
+
|
|
165
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
166
|
+
updateEnvFile({
|
|
167
|
+
VITE_D365_TOKEN: token,
|
|
168
|
+
EXPIRES_IN: expiresIn.toString(),
|
|
169
|
+
TOKEN_TIMESTAMP: timestamp.toString(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const expiryMinutes = Math.floor(expiresIn / 60);
|
|
173
|
+
console.log("✅ Authentication successful!");
|
|
174
|
+
console.log(`🕒 Token expires in ${expiryMinutes} minutes`);
|
|
175
|
+
console.log("💾 Token saved to .env file");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
(async () => {
|
|
179
|
+
try {
|
|
180
|
+
console.log("🚀 Dataverse Token Manager\n");
|
|
181
|
+
|
|
182
|
+
if (isTokenValid()) {
|
|
183
|
+
console.log("✅ Current token is valid - no action needed");
|
|
184
|
+
console.log("🚀 Run your application with: npm start");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log("🔐 Starting OAuth authentication...");
|
|
189
|
+
await authenticateUser();
|
|
190
|
+
console.log("🎉 Token management completed!");
|
|
191
|
+
console.log("🚀 Run your application with: npm start");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.log(`❌ Error: ${error.message}`);
|
|
194
|
+
console.log("💡 Check your CLIENT_ID and Azure App Registration settings");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
})();
|
package/index.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>nl-d365boilerplate-vite</title>
|
|
8
|
+
<!-- Dataverse ClientGlobalContext - Required for Xrm context -->
|
|
9
|
+
<script type="text/javascript" src="../../ClientGlobalContext.js.aspx"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<script>
|
|
12
|
+
(function () {
|
|
13
|
+
const storageKey = "novalogica-ui-theme";
|
|
14
|
+
const theme = localStorage.getItem(storageKey);
|
|
15
|
+
const root = window.document.documentElement;
|
|
16
|
+
|
|
17
|
+
if (theme === "dark") {
|
|
18
|
+
root.classList.add("dark");
|
|
19
|
+
} else {
|
|
20
|
+
root.classList.remove("dark");
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<body>
|
|
26
|
+
<div id="root"></div>
|
|
27
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
28
|
+
</body>
|
|
29
|
+
|
|
30
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nl-d365boilerplate-vite",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nl-d365boilerplate-vite": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"index.html",
|
|
13
|
+
"vite.config.ts",
|
|
14
|
+
"tsconfig.json",
|
|
15
|
+
"tsconfig.app.json",
|
|
16
|
+
"tsconfig.node.json",
|
|
17
|
+
"postcss.config.js",
|
|
18
|
+
"tailwind.config.js",
|
|
19
|
+
"components.json",
|
|
20
|
+
"eslint.config.js",
|
|
21
|
+
"getToken.js",
|
|
22
|
+
".env.example",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"predev": "node getToken.js",
|
|
27
|
+
"dev": "vite",
|
|
28
|
+
"build": "tsc -b && vite build",
|
|
29
|
+
"preview": "vite preview",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "eslint . --fix"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
35
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
36
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
37
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
38
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
39
|
+
"@tailwindcss/vite": "^4.1.18",
|
|
40
|
+
"class-variance-authority": "^0.7.1",
|
|
41
|
+
"clsx": "^2.1.1",
|
|
42
|
+
"dotenv": "^17.2.3",
|
|
43
|
+
"dynamics-web-api": "^2.4.0",
|
|
44
|
+
"hugeicons-react": "^0.4.0",
|
|
45
|
+
"puppeteer": "^24.36.0",
|
|
46
|
+
"react": "^19.2.0",
|
|
47
|
+
"react-dom": "^19.2.0",
|
|
48
|
+
"tailwind-merge": "^3.4.0",
|
|
49
|
+
"tailwindcss": "^4.1.18",
|
|
50
|
+
"vite-plugin-singlefile": "^2.3.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@eslint/js": "^9.39.1",
|
|
54
|
+
"@types/node": "^24.10.1",
|
|
55
|
+
"@types/react": "^19.2.5",
|
|
56
|
+
"@types/react-dom": "^19.2.3",
|
|
57
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
58
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
59
|
+
"eslint": "^9.39.1",
|
|
60
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
61
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
62
|
+
"globals": "^16.5.0",
|
|
63
|
+
"terser": "^5.46.0",
|
|
64
|
+
"tw-animate-css": "^1.4.0",
|
|
65
|
+
"typescript": "~5.9.3",
|
|
66
|
+
"typescript-eslint": "^8.46.4",
|
|
67
|
+
"vite": "^7.2.4"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ThemeProvider } from "@/providers/theme-provider";
|
|
2
|
+
import { NavigationProvider } from "./providers/navigation-provider";
|
|
3
|
+
import { PageRenderer } from "./components/page-render";
|
|
4
|
+
import NlHeader from "./components/nl-header";
|
|
5
|
+
import { DataverseProvider } from "./providers/dataverse-provider";
|
|
6
|
+
|
|
7
|
+
function App() {
|
|
8
|
+
return (
|
|
9
|
+
<ThemeProvider
|
|
10
|
+
defaultTheme="light"
|
|
11
|
+
storageKey="novalogica-ui-theme"
|
|
12
|
+
disableTransitionOnChange
|
|
13
|
+
>
|
|
14
|
+
<DataverseProvider>
|
|
15
|
+
<NavigationProvider>
|
|
16
|
+
<div className="flex min-h-screen w-full flex-col bg-background">
|
|
17
|
+
<NlHeader />
|
|
18
|
+
<main className="flex-1 flex flex-col">
|
|
19
|
+
<PageRenderer />
|
|
20
|
+
</main>
|
|
21
|
+
</div>
|
|
22
|
+
</NavigationProvider>
|
|
23
|
+
</DataverseProvider>
|
|
24
|
+
</ThemeProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default App;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg width="122" height="122" viewBox="0 0 122 122" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.2285 11.7134C55.6976 5.24425 66.1862 5.24427 72.6553 11.7134L110.287 49.3447C116.756 55.8138 116.756 66.3023 110.287 72.7714L72.7714 110.287C66.3023 116.756 55.8138 116.756 49.3446 110.287L11.7133 72.6553C5.24421 66.1862 5.24423 55.6977 11.7134 49.2286L49.2285 11.7134ZM63.5492 20.8195C62.1092 19.3795 59.7746 19.3795 58.3346 20.8195L20.8194 58.3346C19.3795 59.7746 19.3795 62.1093 20.8194 63.5492L58.4507 101.181C59.8907 102.62 62.2254 102.62 63.6653 101.181L101.18 63.6654C102.62 62.2254 102.62 59.8907 101.18 58.4508L63.5492 20.8195Z" fill="url(#paint0_linear)"/>
|
|
3
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.2867 102.279C27.1379 102.279 19.7214 94.8621 19.7214 85.7133L19.7214 36.2928C19.7214 27.1441 27.1379 19.7276 36.2867 19.7276L85.7072 19.7276C94.8559 19.7276 102.272 27.1441 102.272 36.2928L102.272 85.7133C102.272 94.8621 94.8559 102.279 85.7072 102.279L36.2867 102.279ZM32.5994 85.7133C32.5994 87.7498 34.2502 89.4006 36.2867 89.4006L85.7072 89.4006C87.7436 89.4006 89.3945 87.7498 89.3945 85.7133L89.3945 36.2928C89.3945 34.2564 87.7436 32.6055 85.7072 32.6055L36.2867 32.6055C34.2502 32.6055 32.5994 34.2564 32.5994 36.2928L32.5994 85.7133Z" fill="url(#paint1_linear)" fill-opacity="0.9"/>
|
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M80.6696 19.7277L72.6553 11.7134C66.1862 5.24431 55.6977 5.2443 49.2286 11.7134L11.7134 49.2286C5.2443 55.6977 5.24428 66.1862 11.7134 72.6554L19.7215 80.6635L19.7215 60.728L19.7473 60.7023C19.8031 59.84 20.1605 58.9937 20.8195 58.3347L58.3347 20.8195C59.0085 20.1457 59.8783 19.7872 60.7606 19.744L60.7769 19.7277L80.6696 19.7277Z" fill="url(#paint2_linear)"/>
|
|
5
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.272 41.3305L110.287 49.3446C116.756 55.8138 116.756 66.3023 110.287 72.7714L72.7714 110.287C66.3023 116.756 55.8138 116.756 49.3446 110.287L41.3367 102.279L60.9419 102.279L60.9614 102.259C61.937 102.285 62.9208 101.925 63.6653 101.18L101.18 63.6653C101.883 62.9626 102.243 62.0467 102.26 61.1257L102.272 61.1132L102.272 41.3305Z" fill="url(#paint3_linear)"/>
|
|
6
|
+
<defs>
|
|
7
|
+
<linearGradient id="paint0_linear" x1="120.564" y1="83.8165" x2="9.83416" y2="56.3893" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#429691"/>
|
|
9
|
+
<stop offset="1" stop-color="#95E8C2"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<linearGradient id="paint1_linear" x1="36.3003" y1="5.7623" x2="92.101" y2="98.5145" gradientUnits="userSpaceOnUse">
|
|
12
|
+
<stop stop-color="#429691"/>
|
|
13
|
+
<stop offset="1" stop-color="#95E8C2"/>
|
|
14
|
+
</linearGradient>
|
|
15
|
+
<linearGradient id="paint2_linear" x1="28.5819" y1="32.0553" x2="56.5372" y2="59.2992" gradientUnits="userSpaceOnUse">
|
|
16
|
+
<stop stop-color="#89DDBC"/>
|
|
17
|
+
<stop offset="1" stop-color="#6BBEA9"/>
|
|
18
|
+
</linearGradient>
|
|
19
|
+
<linearGradient id="paint3_linear" x1="41.3367" y1="78.2345" x2="115.138" y2="78.2345" gradientUnits="userSpaceOnUse">
|
|
20
|
+
<stop stop-color="#75C8B0"/>
|
|
21
|
+
<stop offset="1" stop-color="#51A49A"/>
|
|
22
|
+
</linearGradient>
|
|
23
|
+
</defs>
|
|
24
|
+
</svg>
|