Skip to content

Commit

Permalink
feat(report): support ability to export to CodePen (#2252)
Browse files Browse the repository at this point in the history
#### Description of changes
These changes add the option to export reports to CodePen in addition to an HTML file. There is a [relevant open issue](#1739) detailing a need for this type of functionality. 

I wasn't quite sure what the design should look like for this so I've went ahead and used a split `PrimaryButton` from `office-ui-fabric-react` in the `ExportDialog ` component but I'm definitely open to alternative approaches!


![a11y-report-export](https://user-images.githubusercontent.com/8262156/75588928-d8c8e600-5a36-11ea-930e-155c459a8bae.gif)


I was also considering splitting out the HTML and CSS before sending the payload to CodePen but I figured I'd get some feedback on the changes within this PR before moving forward with more in-depth functionality.

<!--
  A great PR description includes:
    * A high level overview (usually a sentence or two) describing what the PR changes
    * What is the motivation for the change? This can be as simple as "addresses issue #123"
    * Were there any alternative approaches you considered? What tradeoffs did you consider?
    * What **doesn't** the change try to do? Are there any parts that you've intentionally left out-of-scope for a later PR to handle? What are the issues/work items tracking that later work?
    * Is there any other context that reviewers should consider? For example, other related issues/PRs, or any particularly tricky/subtle bits of implementation that need closer-than-normal review?
-->

#### Pull request checklist
<!-- If a checklist item is not applicable to this change, write "n/a" in the checkbox -->
- [x] Addresses an existing issue: #1739
- [x] Ran `yarn fastpass`
- [x] Added/updated relevant unit test(s) (and ran `yarn test`)
- [x] Verified code coverage for the changes made. Check coverage report at: `<rootDir>/test-results/unit/coverage`
- [x] PR title *AND* final merge commit title both start with a semantic tag (`fix:`, `chore:`, `feat(feature-name):`, `refactor:`). Check workflow guide at: `<rootDir>/docs/workflow.md`
- [x] (UI changes only) Added screenshots/GIFs to description above
- [x] (UI changes only) Verified usability with NVDA/JAWS
  • Loading branch information
sean-beard authored Apr 14, 2020
1 parent 10096c7 commit 1c8ff9b
Show file tree
Hide file tree
Showing 26 changed files with 568 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { PreviewFeatureFlagsHandler } from '../../../handlers/preview-feature-fl
import { NoDisplayableFeatureFlagMessage } from '../../no-displayable-preview-features-message';
import { PreviewFeaturesToggleList } from '../../preview-features-toggle-list';

export const previewFeaturesAutomationId = 'preview-features-container';

export type PreviewFeaturesContainerDeps = {
detailsViewActionMessageCreator: DetailsViewActionMessageCreator;
};
Expand All @@ -29,11 +31,15 @@ export const PreviewFeaturesContainer = NamedFC<PreviewFeaturesContainerProps>(
);

if (displayableFeatureFlags.length === 0) {
return <NoDisplayableFeatureFlagMessage />;
return (
<div data-automation-id={previewFeaturesAutomationId}>
<NoDisplayableFeatureFlagMessage />
</div>
);
}

return (
<div>
<div data-automation-id={previewFeaturesAutomationId}>
<div className="preview-features-description">
{DisplayableStrings.previewFeaturesDescription}
</div>
Expand Down
81 changes: 74 additions & 7 deletions src/DetailsView/components/export-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { PrimaryButton } from 'office-ui-fabric-react';
import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react';
import { TextField } from 'office-ui-fabric-react';
import { FlaggedComponent } from 'common/components/flagged-component';
import { FeatureFlags } from 'common/feature-flags';
import { FeatureFlagStoreData } from 'common/types/store-data/feature-flag-store-data';
import { Dialog, DialogFooter, DialogType, PrimaryButton, TextField } from 'office-ui-fabric-react';
import * as React from 'react';
import { ReportExportServiceProvider } from 'report-export/report-export-service-provider';
import { ExportFormat } from 'report-export/types/report-export-service';
import { ExportResultType } from '../../common/extension-telemetry-events';
import { FileURLProvider } from '../../common/file-url-provider';
import { NamedFC } from '../../common/react/named-fc';
Expand All @@ -19,26 +22,34 @@ export interface ExportDialogProps {
onDescriptionChange: (value: string) => void;
exportResultsType: ExportResultType;
onExportClick: () => void;
featureFlagStoreData: FeatureFlagStoreData;
}

export interface ExportDialogDeps {
detailsViewActionMessageCreator: DetailsViewActionMessageCreator;
fileURLProvider: FileURLProvider;
reportExportServiceProvider: ReportExportServiceProvider;
}

export const ExportDialog = NamedFC<ExportDialogProps>('ExportDialog', props => {
const [format, setFormat] = React.useState<ExportFormat | null>(null);

const onDismiss = (): void => {
props.onClose();
};

const onExportLinkClick = (event: React.MouseEvent<HTMLDivElement>): void => {
const onExportLinkClick = (
event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>,
exportFormat: ExportFormat,
): void => {
const { detailsViewActionMessageCreator } = props.deps;
props.onDescriptionChange(props.description);
detailsViewActionMessageCreator.exportResultsClicked(
props.exportResultsType,
props.html,
event,
);
setFormat(exportFormat);
props.onExportClick();
props.onClose();
};
Expand All @@ -48,6 +59,59 @@ export const ExportDialog = NamedFC<ExportDialogProps>('ExportDialog', props =>
};

const fileURL = props.deps.fileURLProvider.provideURL([props.html], 'text/html');
const exportService = props.deps.reportExportServiceProvider.forKey(format);
const ExportForm = exportService ? exportService.exportForm : null;

const getSingleExportToHtmlButton = () => {
return (
<PrimaryButton
onClick={event =>
onExportLinkClick(event as React.MouseEvent<HTMLAnchorElement>, 'download')
}
download={props.fileName}
href={fileURL}
>
Export
</PrimaryButton>
);
};

const getMultiOptionExportButton = () => {
return (
<>
<PrimaryButton
text="Export"
split
splitButtonAriaLabel="Export HTML to any of these format options"
aria-roledescription="split button"
menuProps={{
items: props.deps.reportExportServiceProvider.all().map(service => ({
key: service.key,
text: service.displayName,
onClick: (e: React.MouseEvent<HTMLButtonElement>) => {
onExportLinkClick(e, service.key);
},
})),
}}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
onExportLinkClick(e, 'download');
}}
download={props.fileName}
href={fileURL}
/>
{ExportForm && (
<ExportForm
fileName={props.fileName}
description={props.description}
html={props.html}
onSubmit={() => {
setFormat(null);
}}
/>
)}
</>
);
};

return (
<Dialog
Expand All @@ -73,9 +137,12 @@ export const ExportDialog = NamedFC<ExportDialogProps>('ExportDialog', props =>
ariaLabel="Provide result description"
/>
<DialogFooter>
<PrimaryButton onClick={onExportLinkClick} download={props.fileName} href={fileURL}>
Export
</PrimaryButton>
<FlaggedComponent
featureFlag={FeatureFlags.exportReport}
featureFlagStoreData={props.featureFlagStoreData}
enableJSXElement={getMultiOptionExportButton()}
disableJSXElement={getSingleExportToHtmlButton()}
/>
</DialogFooter>
</Dialog>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import * as React from 'react';
import { DisplayableStrings } from 'common/constants/displayable-strings';
import * as styles from './no-displayable-preview-features-message.scss';

export const componentId = 'no-displayable-feature-flag-message';
export const NoDisplayableFeatureFlagMessage = NamedFC('NoDisplayableFeatureFlagMessage', () => (
<>
<div id={componentId} className={styles.noPreviewFeatureMessage}>
<div className={styles.noPreviewFeatureMessage}>
{DisplayableStrings.noPreviewFeatureDisplayMessage}
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function getReportExportComponentForAssessment(props: CommandBarProps): J
updatePersistedDescription: value =>
props.deps.detailsViewActionMessageCreator.addResultDescription(value),
getExportDescription: () => props.assessmentStoreData.resultDescription,
featureFlagStoreData: props.featureFlagStoreData,
};

return <ReportExportComponent {...reportExportComponentProps} />;
Expand Down Expand Up @@ -71,6 +72,7 @@ export function getReportExportComponentForFastPass(props: CommandBarProps): JSX
),
updatePersistedDescription: () => null,
getExportDescription: () => '',
featureFlagStoreData: props.featureFlagStoreData,
};

return <ReportExportComponent {...reportExportComponentProps} />;
Expand Down
5 changes: 4 additions & 1 deletion src/DetailsView/components/report-export-component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { ExportResultType } from 'common/extension-telemetry-events';
import { FeatureFlagStoreData } from 'common/types/store-data/feature-flag-store-data';
import { escape } from 'lodash';
import { ActionButton } from 'office-ui-fabric-react';
import * as React from 'react';
import { ReportGenerator } from 'reports/report-generator';

import { ExportResultType } from '../../common/extension-telemetry-events';
import { ExportDialog, ExportDialogDeps } from './export-dialog';

export type ReportExportComponentDeps = {
Expand All @@ -20,6 +21,7 @@ export interface ReportExportComponentProps {
htmlGenerator: (descriptionPlaceholder: string) => string;
updatePersistedDescription: (value: string) => void;
getExportDescription: () => string;
featureFlagStoreData: FeatureFlagStoreData;
}

export interface ReportExportComponentState {
Expand Down Expand Up @@ -88,6 +90,7 @@ export class ReportExportComponent extends React.Component<
onDescriptionChange={this.onExportDescriptionChange}
exportResultsType={exportResultsType}
onExportClick={this.generateHtml}
featureFlagStoreData={this.props.featureFlagStoreData}
/>
</>
);
Expand Down
2 changes: 2 additions & 0 deletions src/DetailsView/details-view-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { NoContentAvailableViewRenderer } from 'DetailsView/no-content-available
import { NullStoreActionMessageCreator } from 'electron/adapters/null-store-action-message-creator';
import { loadTheme, setFocusVisibility } from 'office-ui-fabric-react';
import * as ReactDOM from 'react-dom';
import { ReportExportServiceProviderImpl } from 'report-export/report-export-service-provider-impl';
import { AssessmentReportHtmlGenerator } from 'reports/assessment-report-html-generator';
import { AssessmentReportModelBuilderFactory } from 'reports/assessment-report-model-builder-factory';
import { AutomatedChecksReportSectionFactory } from 'reports/components/report-sections/automated-checks-report-section-factory';
Expand Down Expand Up @@ -405,6 +406,7 @@ if (isNaN(tabId) === false) {
issueFilingServiceProvider: IssueFilingServiceProviderImpl,
getGuidanceTagsFromGuidanceLinks: GetGuidanceTagsFromGuidanceLinks,
reportGenerator,
reportExportServiceProvider: ReportExportServiceProviderImpl,
getCardViewData: getCardViewData,
getPropertyConfigById: getPropertyConfiguration,
collapsibleControl: CardsCollapsibleControl,
Expand Down
10 changes: 10 additions & 0 deletions src/common/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export class FeatureFlags {
public static readonly showInstanceVisibility = 'showInstanceVisibility';
public static readonly manualInstanceDetails = 'manualInstanceDetails';
public static readonly debugTools = 'debugTools';

public static readonly exportReport = 'exportReport';
}

export interface FeatureFlagDetail {
Expand Down Expand Up @@ -99,6 +101,14 @@ export function getAllFeatureFlagDetails(): FeatureFlagDetail[] {
isPreviewFeature: false,
forceDefault: false,
},
{
id: FeatureFlags.exportReport,
defaultValue: false,
displayableName: 'More export options',
displayableDescription: 'Enables exporting reports to external services',
isPreviewFeature: true,
forceDefault: false,
},
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const CommandBar = NamedFC<CommandBarProps>('CommandBar', props => {
}
updatePersistedDescription={() => null}
getExportDescription={() => ''}
featureFlagStoreData={featureFlagStoreData}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/electron/views/renderer-initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import { ReportGenerator } from 'reports/report-generator';
import { ReportHtmlGenerator } from 'reports/report-html-generator';
import { ReportNameGenerator } from 'reports/report-name-generator';

import { ReportExportServiceProviderImpl } from 'report-export/report-export-service-provider-impl';
import { UserConfigurationActions } from '../../background/actions/user-configuration-actions';
import { getPersistedData, PersistedData } from '../../background/get-persisted-data';
import { IndexedDBDataKeys } from '../../background/IndexedDBDataKeys';
Expand Down Expand Up @@ -403,6 +404,7 @@ getPersistedData(indexedDBInstance, indexedDBDataKeysToFetch).then(
reportGenerator: reportGenerator,
fileURLProvider: new FileURLProvider(new WindowUtils(), provideBlob),
getDateFromTimestamp: DateProvider.getDateFromTimestamp,
reportExportServiceProvider: ReportExportServiceProviderImpl,
};

window.insightsUserConfiguration = new UserConfigurationController(interpreter);
Expand Down
8 changes: 8 additions & 0 deletions src/report-export/report-export-service-provider-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { ReportExportServiceProvider } from 'report-export/report-export-service-provider';
import { CodePenReportExportService } from 'report-export/services/codepen-report-export-service';

export const ReportExportServiceProviderImpl = new ReportExportServiceProvider([
CodePenReportExportService,
]);
15 changes: 15 additions & 0 deletions src/report-export/report-export-service-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { ExportFormat, ReportExportService } from 'report-export/types/report-export-service';

export class ReportExportServiceProvider {
constructor(private readonly services: ReportExportService[]) {}

public all(): ReportExportService[] {
return this.services;
}

public forKey(key: ExportFormat): ReportExportService {
return this.services.find(service => service.key === key);
}
}
57 changes: 57 additions & 0 deletions src/report-export/services/codepen-report-export-service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as React from 'react';

import {
ExportFormat,
ExportFormProps,
ReportExportService,
} from 'report-export/types/report-export-service';

const CodePenReportExportServiceKey: ExportFormat = 'codepen';

class ExportForm extends React.Component<ExportFormProps> {
private buttonRef: React.RefObject<HTMLButtonElement>;

constructor(props) {
super(props);
this.buttonRef = React.createRef();
}

public componentDidMount(): void {
if (this.buttonRef.current) {
this.buttonRef.current.click();
this.props.onSubmit();
}
}

public render(): JSX.Element {
return (
<form
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
rel="noopener"
style={{ visibility: 'hidden' }}
>
<input
name="data"
type="hidden"
value={JSON.stringify({
title: this.props.fileName,
description: this.props.description,
html: this.props.html,
editors: '100', // collapse CSS and JS editors
})}
/>
<button type="submit" ref={this.buttonRef} />
</form>
);
}
}

export const CodePenReportExportService: ReportExportService = {
key: CodePenReportExportServiceKey,
displayName: 'CodePen',
exportForm: ExportForm,
};
18 changes: 18 additions & 0 deletions src/report-export/types/report-export-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export type ExportFormat = 'download' | 'codepen';

export type ExportFormProps = ExportProps & { onSubmit: () => void };

export interface ExportProps {
html: string;
fileName: string;
description: string;
}

export interface ReportExportService {
key: ExportFormat;
displayName: string;
exportForm: React.ComponentType<ExportFormProps> | null;
}
Loading

0 comments on commit 1c8ff9b

Please sign in to comment.