@pacaf/wizard-ux 3.4.3 → 3.5.1
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/dist/assets/index-BL1yODjj.js +141 -0
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/server/routes/state.mjs +48 -0
- package/server/steps/08-connectors.mjs +93 -45
- package/server/steps/09-verify-deploy.mjs +4 -0
- package/server/steps/10-add-to-solution.mjs +28 -0
- package/server/steps/index.mjs +2 -1
- package/dist/assets/index-DJRPvUY4.js +0 -141
package/dist/index.html
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
30
30
|
</style>
|
|
31
|
-
<script type="module" crossorigin src="/assets/index-
|
|
31
|
+
<script type="module" crossorigin src="/assets/index-BL1yODjj.js"></script>
|
|
32
32
|
</head>
|
|
33
33
|
<body>
|
|
34
34
|
<div id="root"><div id="boot"><div class="ring"></div></div></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pacaf/wizard-ux",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Browser-based setup wizard for Power Apps Code Apps (parallel to @pacaf/wizard CLI).",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"react-dom": "^19.0.0",
|
|
39
39
|
"react-resizable-panels": "^2.1.7",
|
|
40
40
|
"react-router-dom": "^7.1.0",
|
|
41
|
-
"@pacaf/wizard": "3.4.
|
|
41
|
+
"@pacaf/wizard": "3.4.4"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/react": "^19.0.0",
|
package/server/routes/state.mjs
CHANGED
|
@@ -56,6 +56,52 @@ function readPowerAppInfo(rootDir, state) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Read the Power Platform environment GUID from power.config.json so we can
|
|
60
|
+
// build a Maker Portal deep link. The GUID is required by make.powerapps.com
|
|
61
|
+
// URLs (the org URL like https://contoso.crm.dynamics.com is NOT accepted).
|
|
62
|
+
function readEnvironmentId(rootDir, state) {
|
|
63
|
+
const projectDir = resolve(rootDir, String(state.PROJECT_DIR || '.'));
|
|
64
|
+
const powerConfigPath = join(projectDir, 'power.config.json');
|
|
65
|
+
if (!existsSync(powerConfigPath)) return '';
|
|
66
|
+
try {
|
|
67
|
+
const c = JSON.parse(readFileSync(powerConfigPath, 'utf-8'));
|
|
68
|
+
const direct = String(
|
|
69
|
+
c.environmentId || c.environment?.id || c.targetEnvironmentId || '',
|
|
70
|
+
).trim().toLowerCase();
|
|
71
|
+
if (direct) return direct;
|
|
72
|
+
const appUrl = String(c.localAppUrl || c.appUrl || c.playUrl || '').trim();
|
|
73
|
+
const m = appUrl.match(/\/play\/e\/([0-9a-f-]{36})\/app\//i);
|
|
74
|
+
return m ? m[1].toLowerCase() : '';
|
|
75
|
+
} catch {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build the manual "add Code App to solution" guidance consumed by Step 10 in
|
|
81
|
+
// WizardUX. Returns a direct Maker Portal deep link to the user's solution
|
|
82
|
+
// (falling back to the environment's solutions list, then the portal root) so
|
|
83
|
+
// the user can add the deployed Code App by hand. Never exposes raw GUIDs to
|
|
84
|
+
// the UI — only the human-readable solution display name and app name.
|
|
85
|
+
function readSolutionInfo(rootDir, state) {
|
|
86
|
+
const environmentId = readEnvironmentId(rootDir, state);
|
|
87
|
+
const solutionId = String(state.SOLUTION_ID || '').trim().toLowerCase();
|
|
88
|
+
const displayName = String(state.SOLUTION_DISPLAY_NAME || '').trim();
|
|
89
|
+
const uniqueName = String(state.SOLUTION_UNIQUE_NAME || '').trim();
|
|
90
|
+
const appName = String(state.APP_NAME || '').trim();
|
|
91
|
+
|
|
92
|
+
let makerPortalUrl = 'https://make.powerapps.com/';
|
|
93
|
+
let linkTarget = 'portal'; // 'solution' | 'solutions' | 'portal'
|
|
94
|
+
if (environmentId && solutionId) {
|
|
95
|
+
makerPortalUrl = `https://make.powerapps.com/environments/${environmentId}/solutions/${solutionId}`;
|
|
96
|
+
linkTarget = 'solution';
|
|
97
|
+
} else if (environmentId) {
|
|
98
|
+
makerPortalUrl = `https://make.powerapps.com/environments/${environmentId}/solutions`;
|
|
99
|
+
linkTarget = 'solutions';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { displayName, uniqueName, appName, makerPortalUrl, linkTarget };
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
export default async function stateRoutes(app, opts) {
|
|
60
106
|
const { rootDir } = opts;
|
|
61
107
|
|
|
@@ -63,12 +109,14 @@ export default async function stateRoutes(app, opts) {
|
|
|
63
109
|
const state = readState(rootDir);
|
|
64
110
|
const completed = getCompletedStep(state);
|
|
65
111
|
const powerApp = readPowerAppInfo(rootDir, state);
|
|
112
|
+
const solution = readSolutionInfo(rootDir, state);
|
|
66
113
|
return {
|
|
67
114
|
state,
|
|
68
115
|
completed,
|
|
69
116
|
next: Math.min(completed + 1, TOTAL_STEPS),
|
|
70
117
|
totalSteps: TOTAL_STEPS,
|
|
71
118
|
powerApp,
|
|
119
|
+
solution,
|
|
72
120
|
};
|
|
73
121
|
});
|
|
74
122
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Step 8 - Connectors. Browser-native connector selection and optional data-source binding.
|
|
2
|
-
|
|
2
|
+
// Dataverse is NOT in this list and is never an opt-in toggle: every Code App is bound to
|
|
3
|
+
// Dataverse at the environment level via the mandatory Dataverse URL captured in Step 2.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
5
|
import { execFileSync } from 'node:child_process';
|
|
4
6
|
import { dirname, join, resolve } from 'node:path';
|
|
5
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -19,8 +21,9 @@ const CREATE_MANUAL = '__manual__';
|
|
|
19
21
|
const SKIP_CONNECTION = '__skip__';
|
|
20
22
|
const BAP_PERMISSION_RE = /does not have permission to access|checkAccess|HTTP error status: 403/i;
|
|
21
23
|
|
|
24
|
+
// Dataverse is intentionally excluded: it is mandatory and bound at the environment level,
|
|
25
|
+
// not chosen here. See bindDataverse() for the always-on handling.
|
|
22
26
|
const COMMON_CONNECTORS = [
|
|
23
|
-
{ apiId: 'shared_commondataserviceforapps', name: 'Dataverse' },
|
|
24
27
|
{ apiId: 'shared_office365users', name: 'Office 365 Users' },
|
|
25
28
|
{ apiId: 'shared_sharepointonline', name: 'SharePoint' },
|
|
26
29
|
{ apiId: 'shared_office365', name: 'Office 365 Outlook' },
|
|
@@ -156,6 +159,46 @@ function runFileCapture(log, file, args, opts = {}) {
|
|
|
156
159
|
});
|
|
157
160
|
}
|
|
158
161
|
|
|
162
|
+
// Read the planned Dataverse tables produced by the planning workflow
|
|
163
|
+
// (dataverse/register-datasources.plan.json). Missing/invalid plan = no tables yet.
|
|
164
|
+
function dataverseRegistrationPlan(projectDir) {
|
|
165
|
+
const planPath = join(projectDir, 'dataverse', 'register-datasources.plan.json');
|
|
166
|
+
if (!existsSync(planPath)) return { planPath, tables: [] };
|
|
167
|
+
try {
|
|
168
|
+
const plan = JSON.parse(readFileSync(planPath, 'utf-8'));
|
|
169
|
+
const tables = Array.isArray(plan.dataverseTables) ? plan.dataverseTables.filter(Boolean) : [];
|
|
170
|
+
return { planPath, tables };
|
|
171
|
+
} catch {
|
|
172
|
+
return { planPath, tables: [] };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Dataverse is always bound to a Code App — never optional. When planned tables exist and we
|
|
177
|
+
// have a verified user PAC profile, register them now; otherwise emit clear, non-optional status.
|
|
178
|
+
async function bindDataverse(log, { pac, projectDir, canRegister }) {
|
|
179
|
+
log.info('Dataverse is always bound to this Code App at the environment level (the Dataverse URL you confirmed in Step 2). It is never optional.');
|
|
180
|
+
const { tables } = dataverseRegistrationPlan(projectDir);
|
|
181
|
+
if (tables.length === 0) {
|
|
182
|
+
log.info('No Dataverse tables are planned yet. Once your planning payload defines tables, register them with: npm run register:dataverse');
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
if (!canRegister || !pac) {
|
|
186
|
+
log.info(`${tables.length} planned Dataverse table(s) found. Register them with: npm run register:dataverse (or pac code add-data-source -a dataverse -t <table>).`);
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const registered = [];
|
|
190
|
+
for (const table of tables) {
|
|
191
|
+
const result = await runFileCapture(log, pac, ['code', 'add-data-source', '-a', 'dataverse', '-t', table], { cwd: projectDir });
|
|
192
|
+
if (!result.ok || BAP_PERMISSION_RE.test(result.stderr)) {
|
|
193
|
+
log.warn(`Dataverse table ${table} - registration failed. Retry later with: pac code add-data-source -a dataverse -t ${table}`);
|
|
194
|
+
} else {
|
|
195
|
+
log.ok(`Dataverse table ${table} - data source registered`);
|
|
196
|
+
registered.push(table);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return registered;
|
|
200
|
+
}
|
|
201
|
+
|
|
159
202
|
function parseCustomConnectors(rawEntries) {
|
|
160
203
|
const connectors = [];
|
|
161
204
|
const seen = new Set();
|
|
@@ -204,7 +247,7 @@ export default {
|
|
|
204
247
|
meta: {
|
|
205
248
|
number: 8,
|
|
206
249
|
title: 'Bind Connectors',
|
|
207
|
-
description: '
|
|
250
|
+
description: 'Dataverse is always bound at the environment level (Step 2). Here you optionally choose additional connector references and register their data sources.',
|
|
208
251
|
canRunInBrowser: true,
|
|
209
252
|
optional: true,
|
|
210
253
|
needsSecret: true,
|
|
@@ -266,8 +309,6 @@ export default {
|
|
|
266
309
|
showIf: { id: 'DEFER_CONNECTORS', equals: false },
|
|
267
310
|
});
|
|
268
311
|
|
|
269
|
-
if (connector.apiId === 'shared_commondataserviceforapps') continue;
|
|
270
|
-
|
|
271
312
|
const discovered = discoverConnections(pac, connector.apiId);
|
|
272
313
|
const savedConnectionId = state.CONNECTOR_CONNECTION_IDS?.[connector.apiId] || '';
|
|
273
314
|
const defaultValue = savedConnectionId || (discovered.length === 1 ? discovered[0].connectionId : SKIP_CONNECTION);
|
|
@@ -324,6 +365,12 @@ export default {
|
|
|
324
365
|
if (answers.DEFER_CONNECTORS !== false) {
|
|
325
366
|
log.ok('Connector binding deferred until prototype validation is complete');
|
|
326
367
|
if (customRaw.length > 0) log.info(`Recorded connector notes: ${customRaw.join(', ')}`);
|
|
368
|
+
// Dataverse is never deferred — it is bound at the environment level regardless.
|
|
369
|
+
await bindDataverse(log, {
|
|
370
|
+
pac: null,
|
|
371
|
+
projectDir: resolve(String(state.PROJECT_DIR || PROJECT_DIR)),
|
|
372
|
+
canRegister: false,
|
|
373
|
+
});
|
|
327
374
|
return {
|
|
328
375
|
stateUpdate: {
|
|
329
376
|
CONNECTOR_BINDING_DEFERRED: true,
|
|
@@ -347,59 +394,55 @@ export default {
|
|
|
347
394
|
...customConnectors.map((connector) => connector.apiId),
|
|
348
395
|
])).filter((apiId) => connectorMap.has(apiId));
|
|
349
396
|
|
|
350
|
-
|
|
351
|
-
log.warn('No connectors selected. Nothing to bind.');
|
|
352
|
-
return {
|
|
353
|
-
stateUpdate: {
|
|
354
|
-
CONNECTOR_BINDING_DEFERRED: false,
|
|
355
|
-
CONNECTOR_API_IDS: [],
|
|
356
|
-
CUSTOM_CONNECTORS: customRaw,
|
|
357
|
-
},
|
|
358
|
-
completedStep: 8,
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
397
|
+
const projectDir = resolve(String(state.PROJECT_DIR || PROJECT_DIR));
|
|
362
398
|
const prefix = state.PUBLISHER_PREFIX || '';
|
|
363
399
|
const solutionName = state.SOLUTION_UNIQUE_NAME || '';
|
|
364
|
-
if (!prefix) throw new Error('Publisher prefix is missing. Complete Step 5 before binding connectors.');
|
|
365
|
-
if (!solutionName) throw new Error('Solution unique name is missing. Complete Step 6 before binding connectors.');
|
|
366
|
-
|
|
367
|
-
log.info('Checking existing connection references...');
|
|
368
|
-
let existingRefs = [];
|
|
369
|
-
if (!isUserAuth) {
|
|
370
|
-
try {
|
|
371
|
-
existingRefs = await listConnectionReferences(prefix);
|
|
372
|
-
} catch (err) {
|
|
373
|
-
log.warn(`Could not query connection references: ${err.message}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
400
|
|
|
377
|
-
if (
|
|
378
|
-
log.
|
|
379
|
-
log.info('Create connection references manually in the Maker Portal:');
|
|
380
|
-
log.info(` 1. Go to make.powerapps.com → your Dev environment → Solutions → ${solutionName}`);
|
|
381
|
-
log.info(' 2. Add existing → Connection reference → for each connector');
|
|
382
|
-
log.info(' 3. Or create them during pac code add-data-source.');
|
|
401
|
+
if (selectedApiIds.length === 0) {
|
|
402
|
+
log.info('No additional connectors selected. Dataverse is still bound automatically.');
|
|
383
403
|
} else {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
404
|
+
if (!prefix) throw new Error('Publisher prefix is missing. Complete Step 5 before binding connectors.');
|
|
405
|
+
if (!solutionName) throw new Error('Solution unique name is missing. Complete Step 6 before binding connectors.');
|
|
406
|
+
|
|
407
|
+
log.info('Checking existing connection references...');
|
|
408
|
+
let existingRefs = [];
|
|
409
|
+
if (!isUserAuth) {
|
|
387
410
|
try {
|
|
388
|
-
|
|
389
|
-
existingRefs.push(created);
|
|
411
|
+
existingRefs = await listConnectionReferences(prefix);
|
|
390
412
|
} catch (err) {
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
log.warn(`Could not query connection references: ${err.message}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (isUserAuth) {
|
|
418
|
+
log.warn('User auth does not support automated connection reference creation via the Dataverse API.');
|
|
419
|
+
log.info('Create connection references manually in the Maker Portal:');
|
|
420
|
+
log.info(` 1. Go to make.powerapps.com → your Dev environment → Solutions → ${solutionName}`);
|
|
421
|
+
log.info(' 2. Add existing → Connection reference → for each connector');
|
|
422
|
+
log.info(' 3. Or create them during pac code add-data-source.');
|
|
423
|
+
} else {
|
|
424
|
+
log.info('Creating missing connection references in the selected solution...');
|
|
425
|
+
for (const apiId of selectedApiIds) {
|
|
426
|
+
const connector = connectorMap.get(apiId);
|
|
427
|
+
try {
|
|
428
|
+
const created = await createConnectionReference(log, connector, prefix, solutionName, existingRefs);
|
|
429
|
+
existingRefs.push(created);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
if (/already exists|database constraint/i.test(err.message)) log.ok(`${connector.name} - connection reference already exists`);
|
|
432
|
+
else log.warn(`${connector.name} - connection reference failed: ${err.message}`);
|
|
433
|
+
}
|
|
393
434
|
}
|
|
394
435
|
}
|
|
395
436
|
}
|
|
396
437
|
|
|
397
438
|
const connectionIds = {};
|
|
439
|
+
let dataversePac = null;
|
|
440
|
+
let dataverseCanRegister = false;
|
|
441
|
+
|
|
398
442
|
if (answers.REGISTER_DATA_SOURCES === true) {
|
|
399
443
|
const pac = SHELL.pacPath();
|
|
400
444
|
if (!pac) throw new Error('PAC CLI was not found. Install PAC CLI before registering data sources.');
|
|
401
445
|
|
|
402
|
-
const projectDir = resolve(String(state.PROJECT_DIR || PROJECT_DIR));
|
|
403
446
|
if (!existsSync(join(projectDir, 'power.config.json'))) {
|
|
404
447
|
throw new Error(`power.config.json was not found in ${projectDir}. Complete Step 7 before registering data sources.`);
|
|
405
448
|
}
|
|
@@ -407,8 +450,10 @@ export default {
|
|
|
407
450
|
const credentialValues = isUserAuth ? null : resolveCredentialValues(state);
|
|
408
451
|
const verification = verifyUserProfile(pac, projectDir, state, credentialValues);
|
|
409
452
|
log.ok(`Verified user profile ${verification.profileName}`);
|
|
453
|
+
dataversePac = pac;
|
|
454
|
+
dataverseCanRegister = true;
|
|
410
455
|
|
|
411
|
-
for (const apiId of selectedApiIds
|
|
456
|
+
for (const apiId of selectedApiIds) {
|
|
412
457
|
const connector = connectorMap.get(apiId);
|
|
413
458
|
const connectionId = connectionIdFromAnswers(answers, connector);
|
|
414
459
|
if (!connectionId) {
|
|
@@ -425,9 +470,12 @@ export default {
|
|
|
425
470
|
}
|
|
426
471
|
}
|
|
427
472
|
} else {
|
|
428
|
-
log.info('
|
|
473
|
+
log.info('Connector data-source registration skipped. Connection references were handled in the solution.');
|
|
429
474
|
}
|
|
430
475
|
|
|
476
|
+
// Dataverse is ALWAYS present — registered here when planned tables and a verified profile exist.
|
|
477
|
+
await bindDataverse(log, { pac: dataversePac, projectDir, canRegister: dataverseCanRegister });
|
|
478
|
+
|
|
431
479
|
return {
|
|
432
480
|
stateUpdate: {
|
|
433
481
|
CONNECTOR_BINDING_DEFERRED: false,
|
|
@@ -118,6 +118,10 @@ export default {
|
|
|
118
118
|
title: 'Verify & Deploy',
|
|
119
119
|
description: 'Build the project, optionally push it to Power Platform, and surface the live app URL when available.',
|
|
120
120
|
canRunInBrowser: true,
|
|
121
|
+
// Do NOT auto-advance away from the deploy step: the user needs time to read
|
|
122
|
+
// the pac code push log (solution association, app URL, warnings) before
|
|
123
|
+
// moving on to the manual "add to solution" step.
|
|
124
|
+
noAutoAdvance: true,
|
|
121
125
|
},
|
|
122
126
|
|
|
123
127
|
questions() {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Step 10 — Add the Code App to your Solution (manual).
|
|
2
|
+
//
|
|
3
|
+
// This is a MANUAL step, not a terminal/automation step. `pac code push -s`
|
|
4
|
+
// only associates the Code App with a solution when that solution already
|
|
5
|
+
// exists in the target environment, and even then the binding can silently
|
|
6
|
+
// fall back to the Default solution. Rather than pretend that automation is
|
|
7
|
+
// reliable, this step hands the user a direct Maker Portal deep link and the
|
|
8
|
+
// exact clicks needed to add the Code App to their target solution by hand.
|
|
9
|
+
//
|
|
10
|
+
// Because it is manual, it has no questions and no apply(): `canRunInBrowser`
|
|
11
|
+
// is false and `manual` is true so the WizardUX StepRunner renders a guided
|
|
12
|
+
// panel (deep link + "Outside Dataverse" guidance + illustration) instead of
|
|
13
|
+
// the question/run machinery. The deep link and solution display name are
|
|
14
|
+
// supplied to the frontend by GET /api/state (see routes/state.mjs).
|
|
15
|
+
export default {
|
|
16
|
+
meta: {
|
|
17
|
+
number: 10,
|
|
18
|
+
title: 'Add App to Solution',
|
|
19
|
+
description:
|
|
20
|
+
'Required manual step — open the Maker Portal and add your deployed Code App to your target solution.',
|
|
21
|
+
canRunInBrowser: false,
|
|
22
|
+
manual: true,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
questions() {
|
|
26
|
+
return [];
|
|
27
|
+
},
|
|
28
|
+
};
|
package/server/steps/index.mjs
CHANGED
|
@@ -13,8 +13,9 @@ import step6 from './06-solution.mjs';
|
|
|
13
13
|
import step7 from './07-scaffold.mjs';
|
|
14
14
|
import step8 from './08-connectors.mjs';
|
|
15
15
|
import step9 from './09-verify-deploy.mjs';
|
|
16
|
+
import step10 from './10-add-to-solution.mjs';
|
|
16
17
|
|
|
17
|
-
export const STEPS = [step1, step2, step3, step4, step5, step6, step7, step8, step9];
|
|
18
|
+
export const STEPS = [step1, step2, step3, step4, step5, step6, step7, step8, step9, step10];
|
|
18
19
|
export const TOTAL_STEPS = STEPS.length;
|
|
19
20
|
|
|
20
21
|
export function getStep(n) {
|