Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ jobs:
SKIPPED_FILES=""

# Define packages that require coverage (libs with vitest configs)
COVERED_PACKAGES="libs/designer libs/designer-ui libs/designer-v2 libs/logic-apps-shared libs/data-mapper libs/data-mapper-v2 libs/chatbot libs/a2a-core libs/vscode-extension apps/vs-code-designer apps/vs-code-react apps/iframe-app"
COVERED_PACKAGES="libs/designer libs/designer-ui libs/designer-ui-v2 libs/designer-v2 libs/logic-apps-shared libs/data-mapper libs/data-mapper-v2 libs/chatbot libs/a2a-core libs/vscode-extension apps/vs-code-designer apps/vs-code-react apps/iframe-app"

for file in $CHANGED_FILES; do
# Skip if file doesn't exist (might have been deleted)
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jobs:
args: [--frozen-lockfile, --strict-peer-dependencies]

# - run: npx nx affected --target=lint --parallel
- name: Check shared UI boundaries
env:
POLICY_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
node scripts/check-designer-ui-policy.js "$POLICY_BASE"
node --test scripts/check-designer-ui-policy.test.js
pnpm exec eslint libs/designer-ui-v2/src
- run: pnpm turbo run build --cache-dir=.turbo
- run: pnpm turbo run test:lib --cache-dir=.turbo
- run: pnpm turbo run build:extension --cache-dir=.turbo
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ dist-ssr
.docusaurus
.cache-loader
CHANGELOG.MD
!libs/designer-ui-v2/CHANGELOG.md
# Misc
.DS_Store
.env
Expand Down
74 changes: 74 additions & 0 deletions Localize/lang/strings.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions apps/Standalone/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"@fluentui/font-icons-mdl2": "8.5.13",
"@fluentui/foundation-legacy": "8.2.15",
"@fluentui/react": "8.110.2",
"@fluentui/react-components": "9.70.0",
"@fluentui/react-components": "9.74.11",
"@fluentui/react-datepicker-compat": "0.6.22",
"@fluentui/react-timepicker-compat": "0.4.28",
"@fluentui/react-focus": "8.8.19",
"@fluentui/react-hooks": "8.6.20",
"@fluentui/react-icons": "2.0.224",
"@fluentui/react-icons": "2.0.315",
"@fluentui/react-migration-v8-v9": "^9.6.6",
"@fluentui/react-portal-compat": "^9.0.138",
"@fluentui/style-utilities": "8.9.6",
Expand Down Expand Up @@ -60,7 +60,7 @@
"e2e": "vite --config vite.e2e.config.ts",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"test:lib": "vitest run --config vite.config.ts",
"test:lib": "vitest run --config vitest.config.ts",
"test:ephemeral": "vitest run --config vitest.ephemeral.config.ts"
},
"type": "module"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// @vitest-environment jsdom
import '../../../../../../../libs/shared-test-utils/fluentui-react-icons-mock';
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { FluentProvider, tokens, webDarkTheme, webLightTheme } from '@fluentui/react-components';
import { ActionNodeCard, DesignerUiScope } from '../../../../../../../libs/designer-ui-v2/src';

const tokenReferences = (directory: string): string[] =>
readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const file = path.join(directory, entry.name);
if (entry.isDirectory()) {
return tokenReferences(file);
}
return /\.tsx?$/.test(entry.name) ? [...readFileSync(file, 'utf8').matchAll(/\btokens\.(\w+)/g)].map((match) => match[1]) : [];
});

describe('real Standalone Fluent provider compatibility', () => {
it('supplies every public Fluent token consumed by the shared presentation in both real themes', () => {
const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../../../libs/designer-ui-v2/src');
const references = new Set(tokenReferences(source));
expect(references.size).toBeGreaterThan(0);
for (const token of references) {
expect(tokens[token as keyof typeof tokens], token).toBeDefined();
expect(webLightTheme[token as keyof typeof webLightTheme], `light ${token}`).toBeDefined();
expect(webDarkTheme[token as keyof typeof webDarkTheme], `dark ${token}`).toBeDefined();
}
});

it.each([
['light', webLightTheme],
['dark', webDarkTheme],
] as const)('supplies the canonical action radius token in %s without a mocked provider', (mode, theme) => {
expect(tokens.borderRadius2XLarge).toBe('var(--borderRadius2XLarge)');
expect(theme.borderRadius2XLarge).toBe('12px');
render(
<FluentProvider theme={theme}>
<DesignerUiScope mode={mode}>
<ActionNodeCard title="Compose" />
</DesignerUiScope>
</FluentProvider>
);
const css = Array.from(document.styleSheets)
.flatMap((sheet) => Array.from(sheet.cssRules))
.map((rule) => rule.cssText)
.join('\n');
expect(css).toContain('--borderRadius2XLarge: 12px');
expect(css).toContain('border-radius: var(--borderRadius2XLarge)');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// @vitest-environment jsdom
import { cleanup, render } from '@testing-library/react';
import type { ReactNode } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import { LocalDesigner } from '../../LocalDesigner/localDesignerV2';
import type { LocalOperationManifestService } from '../../LocalDesigner/localOperationManifestService';

const controls = vi.hoisted(() => ({
options: {} as Record<string, unknown>,
services: {} as { operationManifestService: LocalOperationManifestService },
}));
vi.mock('react-redux', () => ({
useDispatch: () => vi.fn(),
useSelector: (select: (state: unknown) => unknown) =>
select({
workflowLoader: { hostOptions: {}, hostingPlan: 'standard' },
}),
}));
vi.mock('@microsoft/logic-apps-designer-v2', () => ({
DesignerProvider: ({ options, children }: { options: Record<string, unknown>; children: ReactNode }) => {
controls.options = options;
controls.services = options.services as typeof controls.services;
return children;
},
BJSWorkflowProvider: () => null,
Designer: () => null,
CombineInitializeVariableDialog: () => null,
TriggerDescriptionDialog: () => null,
}));
vi.mock('../../LocalDesigner/pseudoCommandBar', () => ({ PseudoCommandBarV2: () => null }));
vi.mock('../../LocalDesigner/customEditorService', () => ({ CustomEditorService: class {} }));
vi.mock('../../LocalDesigner/customConnection/customConnectionParameterEditorService', () => ({
CustomConnectionParameterEditorService: class {},
}));

afterEach(cleanup);

describe('Standalone shared-node local opt-in', () => {
it('uses complete offline Compose metadata in ordinary Local Standard without enabling extraction', async () => {
render(
<MemoryRouter initialEntries={['/v2']}>
<LocalDesigner />
</MemoryRouter>
);
const service = controls.services.operationManifestService;
expect(service.constructor.name).toBe('LocalOperationManifestService');
const info = await service.getOperationInfo({ type: 'Compose', inputs: 'value' }, false);
const manifest = await service.getOperationManifest(info.connectorId, info.operationId);
expect(manifest.properties.summary).toBe('Compose');
expect(manifest.properties.inputs).toBeDefined();
expect(manifest.properties.iconUri).toMatch(/^data:image\/svg/);
});

it.each([
['', false],
['?useSharedNodeUi=false', false],
['?useSharedNodeUi=1', false],
['?useSharedNodeUi=TRUE', false],
['?useSharedNodeUi=true', true],
['?extraction=true&useSharedNodeUi=true', true],
])('passes %s through public provider options without changing host settings', (search, expected) => {
render(
<MemoryRouter initialEntries={[`/v2${search}`]}>
<LocalDesigner />
</MemoryRouter>
);
expect(controls.options.useSharedNodeUi ?? false).toBe(expected);
expect(controls.options.hostOptions).not.toHaveProperty('useSharedNodeUi');
});
});
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
ssr: { noExternal: [/@fluentui\//] },
test: {
projects: [{ extends: true, test: { name: 'azure-standard-management' } }],
environment: 'jsdom',
include: [
'apps/Standalone/src/designer/app/AzureLogicAppsDesigner/__test__/*.spec.ts?(x)',
'apps/Standalone/src/designer/app/LocalDesigner/__test__/standardManagementHost.spec.tsx',
'apps/Standalone/src/designer/app/LocalDesigner/__test__/*.spec.ts?(x)',
],
setupFiles: ['libs/shared-test-utils/fluentui-react-icons-mock.ts'],
server: { deps: { inline: [/@fluentui\//] } },
restoreMocks: true,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type OpenAPIV2,
} from '@microsoft/logic-apps-shared';
import { HttpClient } from '../httpClient';
import { LocalOperationManifestService } from '../localOperationManifestService';
import {
createConsumptionWorkflowConnectorService,
createLocalWorkflowConnectorService,
Expand Down Expand Up @@ -64,6 +65,26 @@ describe('offline Standard workflow manifest', () => {
beforeEach(() => getReactQueryClient().clear());
afterEach(() => getReactQueryClient().clear());

it.each([
['Compose', LocalOperationManifestService],
['Execute_a_navigation_plan', LocalOperationManifestService],
['Compose', LocalWorkflowManifestService],
['Execute_a_navigation_plan', LocalWorkflowManifestService],
])('resolves complete offline Compose metadata for %s', async (_name, Service) => {
const http = new HttpClient();
const get = vi.spyOn(http, 'get');
const service = new Service({ baseUrl: '/url', apiVersion: '2018-11-01', httpClient: http });
const info = await service.getOperationInfo({ type: 'Compose', inputs: 'value' }, false);
InitOperationManifestService(service);
const manifest = await getOperationManifest(info);
expect(manifest?.properties.summary).toBe('Compose');
expect(manifest?.properties.inputs).toBeDefined();
expect(manifest?.properties.outputs).toBeDefined();
expect(manifest?.properties.iconUri).toMatch(/^data:image\/svg/);
expect((await getOperation(info)).properties.iconUri).toBe(manifest?.properties.iconUri);
expect(get).not.toHaveBeenCalled();
});

it('recognizes Standard invocations and requires only the workflow name and body', async () => {
const http = new HttpClient();
const get = vi.spyOn(http, 'get');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
selectWorkflowConnectorService,
} from './localWorkflowManifest';
import { localWorkflowPrefix, localWorkflowRegistry } from './localWorkflowRegistry';
import { LocalOperationManifestService } from './localOperationManifestService';
import {
StandardConnectionService,
StandardOperationManifestService,
StandardSearchService,
BaseOAuthService,
BaseGatewayService,
Expand All @@ -41,6 +41,7 @@ import {
} from '@microsoft/logic-apps-designer-v2';
import { useDispatch, useSelector } from 'react-redux';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';

const httpClient = new HttpClient();
const connectionServiceStandard = new StandardConnectionService({
Expand Down Expand Up @@ -72,7 +73,7 @@ const connectionServiceConsumption = new ConsumptionConnectionService({
httpClient,
});

const operationManifestServiceStandard = new StandardOperationManifestService({
const operationManifestServiceStandard = new LocalOperationManifestService({
apiVersion: '2018-11-01',
baseUrl: '/url',
httpClient,
Expand Down Expand Up @@ -211,6 +212,7 @@ const editorService = new CustomEditorService();
const connectionParameterEditorService = new CustomConnectionParameterEditorService();

export const LocalDesigner = () => {
const { search } = useLocation();
const dispatch = useDispatch<AppDispatch>();
const {
workflowDefinition,
Expand Down Expand Up @@ -286,6 +288,7 @@ export const LocalDesigner = () => {
isMonitoringView,
isDarkMode,
useLegacyWorkflowParameters: isConsumption,
useSharedNodeUi: new URLSearchParams(search).get('useSharedNodeUi') === 'true',
showEdgeDrawing,
suppressDefaultNodeSelectFunctionality: suppressDefaultNodeSelect,
hostOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ConsumptionOperationManifestService, StandardOperationManifestService } from '@microsoft/logic-apps-shared';

const isCompose = (connectorId: string, operationId: string) =>
connectorId.toLowerCase() === 'connectionproviders/dataoperationnew' && operationId.toLowerCase() === 'composenew';

/** Local Standard has no runtime endpoint for Compose; reuse the complete repository-owned offline manifest. */
export class LocalOperationManifestService extends StandardOperationManifestService {
private readonly offline: ConsumptionOperationManifestService;

constructor(options: ConstructorParameters<typeof StandardOperationManifestService>[0]) {
super(options);
this.offline = new ConsumptionOperationManifestService({ ...options, subscriptionId: 'subid', location: 'location' });
}

override async getOperationManifest(connectorId: string, operationId: string) {
return isCompose(connectorId, operationId)
? this.offline.getOperationManifest(connectorId.toLowerCase(), operationId.toLowerCase())
: super.getOperationManifest(connectorId, operationId);
}

override async getOperation(connectorId: string, operationId: string, useCachedData = false) {
return isCompose(connectorId, operationId)
? this.offline.getOperation(connectorId.toLowerCase(), operationId.toLowerCase())
: super.getOperation(connectorId, operationId, useCachedData);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
ConsumptionConnectorService,
StandardConnectorService,
StandardOperationManifestService,
type IConnectorService,
type IHttpClient,
type LogicAppsV2,
type OpenAPIV2,
type OperationManifest,
} from '@microsoft/logic-apps-shared';
import { LocalOperationManifestService } from './localOperationManifestService';
import {
hasLocalWorkflowInvocationMarker,
type LocalWorkflowRegistry,
Expand Down Expand Up @@ -109,9 +109,9 @@ export const localWorkflowManifest: OperationManifest = {
},
};

export class LocalWorkflowManifestService extends StandardOperationManifestService {
export class LocalWorkflowManifestService extends LocalOperationManifestService {
constructor(
options: ConstructorParameters<typeof StandardOperationManifestService>[0],
options: ConstructorParameters<typeof LocalOperationManifestService>[0],
private readonly registry: LocalWorkflowRegistry = localWorkflowRegistry
) {
super(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const modalStyles: Partial<IModalStyles> = {
};

interface CommandBarBindings {
reserveViewport?: boolean;
isDarkMode: boolean;
isDirty: boolean;
numErrors: number;
Expand Down Expand Up @@ -60,6 +61,7 @@ export const PseudoCommandBarV2 = () => {
const state = useSelector((state: DesignerV2.RootState) => state);
const dispatch = useDispatch<DesignerV2.AppDispatch>();
const bindings: CommandBarBindings = {
reserveViewport: state.designerOptions.useSharedNodeUi === true,
isDarkMode: state.designerOptions.isDarkMode ?? false,
isDirty: DesignerV2.useIsDesignerDirty(),
numErrors: DesignerV2.useTotalNumErrors(),
Expand All @@ -75,6 +77,7 @@ export const PseudoCommandBarV2 = () => {
};

const CommandBar = ({
reserveViewport,
isDarkMode,
isDirty,
numErrors,
Expand All @@ -96,7 +99,7 @@ const CommandBar = ({
const haveErrors = useMemo(() => numErrors > 0, [numErrors]);

return (
<div className="pseudo-command-bar">
<div className="pseudo-command-bar" data-designer-viewport-inset={reserveViewport ? 'bottom' : undefined}>
<ActionButton
iconProps={{ iconName: 'Save' }}
text="Save"
Expand Down
Loading
Loading