Skip to content

Commit

Permalink
feat(Templates): Styling for Templates Parameters (#4978)
Browse files Browse the repository at this point in the history
* moved over from workflowParameterField to TemplatesParameterField

* added .less styling

* hide parameters tab on no parameters

* finisehd css for tempaltesParmeterField

* removed parameter description as it is not part of parameters field

* added tests for templatesParametersField

* fixed display parameter tests

* fixed tests in parametersTab and brought back unwanted changes

* updated submodule to fix merge conflict
  • Loading branch information
Elaina-Lee committed Jun 14, 2024
1 parent 47b7c07 commit 8cf13cd
Show file tree
Hide file tree
Showing 20 changed files with 360 additions and 117 deletions.
1 change: 1 addition & 0 deletions libs/designer-ui/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export * from './settings/settingsection';
export * from './staticResult';
export * from './table';
export * from './telemetry/models';
export * from './templates/templatesParametersField';
// export * from './textbox';
export * from './text';
export * from './tip';
Expand Down
1 change: 1 addition & 0 deletions libs/designer-ui/src/lib/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
@import "./floatingactionmenu/_floatingactionmenu.less";
@import "./monitoring/monitoring.less";
@import "./monitoring/statuspill/statuspill.less";
@import "./templates/templatesParametersField.less";
@import "./tip/tip.less";
@import "./arrayeditor/arrayeditor.less";
@import "./ariaSearchResults/ariaSearchResultsAlert.less";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { TemplatesParameterFieldProps } from '../templatesParametersField';
import { TemplatesParameterField } from '../templatesParametersField';
import { initializeIcons } from '@fluentui/react';
import * as React from 'react';
import { useIntl } from 'react-intl';
import * as ReactShallowRenderer from 'react-test-renderer/shallow';
import { describe, beforeEach, afterEach, it, expect } from 'vitest';
describe('ui/templates/templatesParameterField', () => {
let minimal: TemplatesParameterFieldProps;
let minimalWithError: TemplatesParameterFieldProps;
let renderer: ReactShallowRenderer.ShallowRenderer;

beforeEach(() => {
minimal = {
definition: { value: 'blue', name: 'test1', type: 'String', description: 'description1', displayName: 'display name' },
validationError: undefined,
};
minimalWithError = {
...minimal,
validationError: 'validation failed',
};
renderer = ReactShallowRenderer.createRenderer();
initializeIcons();
});

afterEach(() => {
renderer.unmount();
});

it('should construct.', () => {
renderer.render(<TemplatesParameterField {...minimal} />);
const parameterFields = renderer.getRenderOutput();
expect(parameterFields).toBeDefined();
});

it('should render all fields when passed a parameter definition.', () => {
const intl = useIntl();
renderer.render(<TemplatesParameterField {...minimal} />);
const parameterFields = renderer.getRenderOutput();
expect(parameterFields.props.children).toHaveLength(3);

const [displayName, description, valueField]: any[] = React.Children.toArray(parameterFields.props.children);

expect(displayName.props.className).toBe('msla-templates-parameter-heading');
expect(displayName.props.children).toBeTruthy();
const [label]: any[] = React.Children.toArray(displayName.props.children);
expect(label.props.className).toBe('msla-templates-parameter-heading-text');
expect(label.props.text).toBe(minimal.definition.displayName);

expect(description.props.className).toBe('msla-templates-parameter-description');
expect(description.props.children).toBeTruthy();

const [text]: any[] = React.Children.toArray(description.props.children);
expect(text.props.className).toBe('msla-templates-parameter-description-text');
expect(text.props.children).toBe('description1');

const defaultValueDescription = intl.formatMessage({
defaultMessage: 'Enter value for parameter.',
id: '7jAQar',
description: 'Parameter Field Default Value Placeholder Text',
});

const valueLabelText = `Value (${minimal.definition.type})`;
const valueLabelId = `${minimal.definition.name}-value`;

expect(valueField.props.className).toBe('msla-templates-parameter-field');
expect(valueField.props.children).toHaveLength(2);

const [label2, textField]: any[] = React.Children.toArray(valueField.props.children);
expect(label2.props.text).toBe(valueLabelText);
expect(label2.props.htmlFor).toBe(valueLabelId);

expect(textField.props.id).toBe(valueLabelId);
expect(textField.props.ariaLabel).toBe(valueLabelText);
expect(textField.props.placeholder).toBe(defaultValueDescription);
expect(textField.props.value).toBe(minimal.definition.value);
});
it('should render the error message when there is a validation error', () => {
renderer.render(<TemplatesParameterField {...minimalWithError} />);
const parameterFields = renderer.getRenderOutput();
expect(parameterFields.props.children).toHaveLength(3);

const [_displayName, _description, valueField]: any[] = React.Children.toArray(parameterFields.props.children);
const valueLabelText = `Value (${minimal.definition.type})`;
const valueLabelId = `${minimal.definition.name}-value`;

expect(valueField.props.className).toBe('msla-templates-parameter-field');
expect(valueField.props.children).toHaveLength(2);

const [label2, textField]: any[] = React.Children.toArray(valueField.props.children);
expect(label2.props.text).toBe(valueLabelText);
expect(label2.props.htmlFor).toBe(valueLabelId);

expect(textField.props.id).toBe(valueLabelId);
expect(textField.props.errorMessage).toBe(minimalWithError.validationError);
});

it('should render nothing when type passed is invalid.', () => {
const props = { ...minimal, definition: { ...minimal.definition, type: 'random' } };
renderer.render(<TemplatesParameterField {...props} />);
const parameterFields = renderer.getRenderOutput();
const [, type]: any[] = React.Children.toArray(parameterFields.props.children);
expect(type.props.selectedKey).toBeUndefined();
});
});
32 changes: 32 additions & 0 deletions libs/designer-ui/src/lib/templates/templatesParametersField.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.msla-templates-parameters {
display: flex;
flex-direction: column;
gap: 16px;

.msla-templates-parameter-heading {
margin-bottom: 8px;
}

.msla-templates-parameter-heading-text {
font-weight: 600;
font-size: 14px;
}

.msla-templates-parameter-description {
margin-bottom: 16px;
}

.msla-templates-parameter-description-text {
font-size: 13px;
color: @ms-color-secondary;
}

.ms-List-cell {
padding-bottom: 16px;
}

.msla-templates-parameter-field {
display: flex;
min-height: 24px;
}
}
115 changes: 115 additions & 0 deletions libs/designer-ui/src/lib/templates/templatesParametersField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { ILabelStyles, IStyle, ITextFieldStyles } from '@fluentui/react';
import { TextField } from '@fluentui/react';
import { useState } from 'react';
import { useIntl } from 'react-intl';
import { Text } from '@fluentui/react-components';
import { Label } from '../label';
import type { EventHandler } from '../eventhandler';
import type { Template } from '@microsoft/logic-apps-shared';

const labelStyles: Partial<ILabelStyles> = {
root: {
display: 'inline-block',
minWidth: '120px',
verticalAlign: 'top',
padding: '0px',
},
};

const fieldStyles: IStyle = {
display: 'inline-block',
flexGrow: 1,
flexShrink: 1,
flexBasis: 'auto',
};

const textFieldStyles: Partial<ITextFieldStyles> = {
root: fieldStyles,
};

export interface TemplatesParameterUpdateEvent {
newDefinition: Template.ParameterDefinition;
useLegacy?: boolean;
}

export type TemplatesParameterUpdateHandler = EventHandler<TemplatesParameterUpdateEvent>;

export interface TemplatesParameterFieldProps {
definition: Template.ParameterDefinition;
validationError: string | undefined;
onChange?: TemplatesParameterUpdateHandler;
useLegacy?: boolean;
isReadOnly?: boolean;
required?: boolean;
}

export const TemplatesParameterField = ({
definition,
validationError,
onChange,
required = true,
isReadOnly,
useLegacy,
}: TemplatesParameterFieldProps): JSX.Element => {
const [value, setValue] = useState<string | undefined>(stringifyValue(definition.value));
const intl = useIntl();

const parameterValueId = `${definition.name}-value`;

const valueTitle = intl.formatMessage({
defaultMessage: 'Value',
id: 'ClZW2r',
description: 'Parameter Field Value Title',
});
const valueDescription = intl.formatMessage({
defaultMessage: 'Enter value for parameter.',
id: 'rSIBjh',
description: 'Parameter Field Value Placeholder Text',
});

const onValueChange = (_event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
handleValueChange(newValue);
};

const handleValueChange = (value?: string) => {
setValue(value);

onChange?.({
newDefinition: {
...definition,
value,
},
useLegacy,
});
};

return (
<>
<div className="msla-templates-parameter-heading">
<Label className="msla-templates-parameter-heading-text" text={definition.displayName} isRequiredField={required} />
</div>
<div className="msla-templates-parameter-description">
<Text className="msla-templates-parameter-description-text">{definition.description}</Text>
</div>

<div className="msla-templates-parameter-field">
<Label styles={labelStyles} text={`${valueTitle} (${definition.type})`} htmlFor={parameterValueId} />
<TextField
data-testid={parameterValueId}
id={parameterValueId}
ariaLabel={`${valueTitle} (${definition.type})`}
placeholder={valueDescription}
value={value}
errorMessage={validationError}
styles={textFieldStyles}
onChange={onValueChange}
disabled={isReadOnly}
/>
</div>
</>
);
};

function stringifyValue(value: any): string {
return typeof value !== 'string' ? JSON.stringify(value) : value;
}
2 changes: 2 additions & 0 deletions libs/designer-ui/src/lib/themes.less
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@
@ms-color-secondaryBackground: #252423;
@ms-color-secondaryBorder: #323130;
@ms-color-edge: #8a8886;

@ms-color-secondary: #8a8886;
13 changes: 6 additions & 7 deletions libs/designer/src/lib/core/state/templates/templateSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { templatesPathFromState, type RootState } from './store';
import type { WorkflowParameterUpdateEvent } from '@microsoft/designer-ui';
import type { TemplatesParameterUpdateEvent } from '@microsoft/designer-ui';
import { validateParameterValueWithSwaggerType } from '../../../core/utils/validation';
import type { TemplateServiceOptions } from '../../../core/templates/TemplatesDesignerContext';

Expand Down Expand Up @@ -128,19 +128,18 @@ export const templateSlice = createSlice({
updateKind: (state, action: PayloadAction<string>) => {
state.kind = action.payload;
},
updateTemplateParameterValue: (state, action: PayloadAction<WorkflowParameterUpdateEvent>) => {
updateTemplateParameterValue: (state, action: PayloadAction<TemplatesParameterUpdateEvent>) => {
const {
id,
newDefinition: { type, value, required },
newDefinition: { name, type, value, required },
} = action.payload;

const validationError = validateParameterValue({ type, value }, required);

state.parameters.definitions[id] = {
...(getRecordEntry(state.parameters.definitions, id) ?? ({} as any)),
state.parameters.definitions[name] = {
...(getRecordEntry(state.parameters.definitions, name) ?? ({} as any)),
value,
};
state.parameters.validationErrors[id] = validationError;
state.parameters.validationErrors[name] = validationError;
},
},
extraReducers: (builder) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useSelector } from "react-redux";
import type { RootState } from "./store";
import { useSelector } from 'react-redux';
import type { RootState } from './store';

export const useAreServicesInitialized = () => {
return useSelector((state: RootState) => state.template.servicesInitialized ?? false);
};
return useSelector((state: RootState) => state.template.servicesInitialized ?? false);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import type { IApiManagementService, IAppServiceService, IConnectionParameterEditorService, IConnectionService, IFunctionService, IGatewayService, ILoggerService, IOAuthService } from '@microsoft/logic-apps-shared';
import type {
IApiManagementService,
IAppServiceService,
IConnectionParameterEditorService,
IConnectionService,
IFunctionService,
IGatewayService,
ILoggerService,
IOAuthService,
} from '@microsoft/logic-apps-shared';
import { createContext } from 'react';

export interface TemplatesDesignerContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('templates/TemplatesDataProvider', () => {
<ReactQueryProvider>
<TemplatesDesignerProvider locale="en-US" theme={'light'}>
<TemplatesDataProvider
resourceDetails={{subscriptionId: 'sub', resourceGroup: 'rg', location: 'us'}}
resourceDetails={{ subscriptionId: 'sub', resourceGroup: 'rg', location: 'us' }}
isConsumption={false}
existingWorkflowName={'workflowName'}
services={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,23 @@ import { describe, expect, it } from 'vitest';
import { normalizeConnectorId } from '../helper';

describe('templates/utils/helper', () => {

describe('normalizeConnectorId', () => {
const armConnectorId = '/subscriptions/#subscription#/providers/Microsoft.Web/locations/#location#/managedApis/sql';
const spConnectorId = '/serviceProviders/sql';
const subscriptionId = '00000000-0000-0000-0000-000000000000';
const subscriptionId = '00000000-0000-0000-0000-000000000000';
const location = 'eastus';

it('should replace subscriptionId and location correctly in arm connector id', async () => {
const expectedConnectorId = `/subscriptions/${subscriptionId}/providers/Microsoft.Web/locations/${location}/managedApis/sql`;
expect(normalizeConnectorId(armConnectorId, subscriptionId, location)).toEqual(expectedConnectorId);
const expectedConnectorId = `/subscriptions/${subscriptionId}/providers/Microsoft.Web/locations/${location}/managedApis/sql`;
expect(normalizeConnectorId(armConnectorId, subscriptionId, location)).toEqual(expectedConnectorId);

expect(normalizeConnectorId(armConnectorId, '', '')).toEqual('/subscriptions//providers/Microsoft.Web/locations//managedApis/sql');
expect(normalizeConnectorId(armConnectorId, '', '')).toEqual('/subscriptions//providers/Microsoft.Web/locations//managedApis/sql');
});

it('should not change connectorId when not an arm resource', async () => {
expect(normalizeConnectorId('', subscriptionId, location)).toEqual('');
expect(normalizeConnectorId('/serviceProviders/sql', subscriptionId, location)).toEqual('/serviceProviders/sql');
expect(normalizeConnectorId('/dataOperations', '', '')).toEqual('/dataOperations');
expect(normalizeConnectorId('', subscriptionId, location)).toEqual('');
expect(normalizeConnectorId('/serviceProviders/sql', subscriptionId, location)).toEqual('/serviceProviders/sql');
expect(normalizeConnectorId('/dataOperations', '', '')).toEqual('/dataOperations');
});
});
});
Loading

0 comments on commit 8cf13cd

Please sign in to comment.