Public gallery improvements (#409)

- [x] Don't show extension in name field for publish
- [x] Open "Your published work" tab after publishing
- [x] Continue showing dialog for Report Abuse status
- [x] For showing COC in Public Gallery tab show backdrop of thumbnails
- [x] Liked -> My Favorites & Your published work -> My published work
This commit is contained in:
Tanuj Mittal 2021-01-29 22:34:38 +05:30 committed by GitHub
parent 59113d7bbf
commit 9278654479
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 190 additions and 66 deletions

View File

@ -3,7 +3,13 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
import {
ChoiceGroup,
FontIcon,
IChoiceGroupProps,
IProgressIndicatorProps,
ProgressIndicator,
} from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps {
label: string;
@ -27,6 +33,7 @@ export interface DialogProps {
choiceGroupProps?: IChoiceGroupProps;
textFieldProps?: TextFieldProps;
linkProps?: LinkProps;
progressIndicatorProps?: IProgressIndicatorProps;
primaryButtonText: string;
secondaryButtonText: string;
onPrimaryButtonClick: () => void;
@ -69,6 +76,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps;
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText,
disabled: this.props.primaryButtonDisabled || false,
@ -91,6 +99,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}

View File

@ -127,7 +127,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
{this.props.isFavorite !== undefined &&
this.generateIconButtonWithTooltip(
this.props.isFavorite ? "HeartFill" : "Heart",
this.props.isFavorite ? "Unlike" : "Like",
this.props.isFavorite ? "Unfavorite" : "Favorite",
"left",
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
)}

View File

@ -185,7 +185,7 @@ exports[`GalleryCardComponent renders 1`] = `
"gapSpace": 0,
}
}
content="Like"
content="Favorite"
id="TooltipHost-IconButton-Heart"
styles={
Object {
@ -197,14 +197,14 @@ exports[`GalleryCardComponent renders 1`] = `
}
>
<CustomizedIconButton
ariaLabel="Like"
ariaLabel="Favorite"
iconProps={
Object {
"iconName": "Heart",
}
}
onClick={[Function]}
title="Like"
title="Favorite"
/>
</StyledTooltipHostBase>
<StyledTooltipHostBase

View File

@ -7,14 +7,20 @@ import {
} from "./GalleryAndNotebookViewerComponent";
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
private key: string;
public parameters: ko.Observable<number>;
constructor(private props: GalleryAndNotebookViewerComponentProps) {
this.reset();
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <GalleryAndNotebookViewerComponent {...this.props} />;
return <GalleryAndNotebookViewerComponent key={this.key} {...this.props} />;
}
public reset(): void {
this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`;
}
public triggerRender(): void {

View File

@ -7,3 +7,12 @@
width: 100%;
font-family: @DataExplorerFont;
}
.publicGalleryTabContainer {
position: relative;
}
.publicGalleryTabOverlayContent {
background: white;
padding: 20px;
}

View File

@ -10,6 +10,7 @@ import {
IRectangle,
Label,
List,
Overlay,
Pivot,
PivotItem,
SearchBox,
@ -74,14 +75,14 @@ interface GalleryTabInfo {
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
public static readonly OfficialSamplesTitle = "Official samples";
public static readonly PublicGalleryTitle = "Public gallery";
public static readonly FavoritesTitle = "Liked";
public static readonly PublishedTitle = "Your published work";
public static readonly FavoritesTitle = "My favorites";
public static readonly PublishedTitle = "My published work";
private static readonly rowsPerPage = 5;
private static readonly mostViewedText = "Most viewed";
private static readonly mostDownloadedText = "Most downloaded";
private static readonly mostFavoritedText = "Most liked";
private static readonly mostFavoritedText = "Most favorited";
private static readonly mostRecentText = "Most recent";
private readonly sortingOptions: IDropdownOption[];
@ -222,8 +223,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: this.isEmptyData(data)
? this.createEmptyTabContent(
"ContactHeart",
"You have not liked anything",
"Like any notebook from Official Samples or Public gallery"
"You have not favorited anything",
"Favorite any notebook from Official samples or Public gallery"
)
: this.createSearchBarHeader(this.createCardsTabContent(data)),
};
@ -245,7 +246,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
const content = (
<Stack tokens={{ childrenGap: 10 }}>
<Stack tokens={{ childrenGap: 20 }}>
{published?.length > 0 &&
this.createPublishedNotebooksSectionContent(
undefined,
@ -276,7 +277,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: JSX.Element
): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5 }}>
<Stack tokens={{ childrenGap: 10 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
{description && <Text>{description}</Text>}
{content}
@ -285,15 +286,22 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
};
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
) : (
this.createSearchBarHeader(this.createCardsTabContent(data))
return (
<div className="publicGalleryTabContainer">
{this.createSearchBarHeader(this.createCardsTabContent(data))}
{acceptedCodeOfConduct === false && (
<Overlay isDarkThemed>
<div className="publicGalleryTabOverlayContent">
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
</div>
</Overlay>
)}
</div>
);
}

View File

@ -3,14 +3,11 @@
*/
import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core";
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import { IChoiceGroupProps, Icon, IProgressIndicatorProps, Link, ProgressIndicator } from "office-ui-fabric-react";
import * as React from "react";
import { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
@ -21,7 +18,7 @@ import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookViewerComponentProps {
container?: Explorer;
@ -178,6 +175,32 @@ export class NotebookViewerComponent
};
}
// DialogHost
showOkModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
progressIndicatorProps?: IProgressIndicatorProps
): void {
this.setState({
dialogProps: {
isModal: true,
visible: true,
title,
subText: msg,
primaryButtonText: okLabel,
onPrimaryButtonClick: () => {
this.setState({ dialogProps: undefined });
onOk && onOk();
},
secondaryButtonText: undefined,
onSecondaryButtonClick: undefined,
progressIndicatorProps,
},
});
}
// DialogHost
showOkCancelModalDialog(
title: string,
@ -186,8 +209,10 @@ export class NotebookViewerComponent
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
progressIndicatorProps?: IProgressIndicatorProps,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void {
this.setState({
dialogProps: {
@ -205,8 +230,10 @@ export class NotebookViewerComponent
this.setState({ dialogProps: undefined });
onCancel && onCancel();
},
progressIndicatorProps,
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
},
});
}

View File

@ -91,6 +91,7 @@ import { appInsights } from "../Shared/appInsights";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@ -2810,10 +2811,36 @@ export default class Explorer {
}
}
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
public async openGallery(
selectedTab?: GalleryTab,
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
) {
let title: string = "Gallery";
let hashLocation: string = "gallery";
const galleryTabOptions: any = {
// GalleryTabOptions
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTab.OfficialSamples,
notebookUrl,
galleryItem,
isFavorite,
// TabOptions
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
documentClientUtility: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
};
const galleryTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Gallery,
(tab) => tab.hashLocation() == hashLocation
@ -2822,31 +2849,12 @@ export default class Explorer {
if (galleryTab) {
this.tabsManager.activateTab(galleryTab);
(galleryTab as any).reset(galleryTabOptions);
} else {
if (!this.galleryTab) {
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
}
const newTab = new this.galleryTab.default({
// GalleryTabOptions
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
notebookUrl,
galleryItem,
isFavorite,
// TabOptions
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
documentClientUtility: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
});
const newTab = new this.galleryTab.default(galleryTabOptions);
this.tabsManager.activateNewTab(newTab);
}
}

View File

@ -11,6 +11,7 @@ import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
@ -163,6 +164,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
);
} else {
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
this.container.openGallery(GalleryTab.Published);
}
}
} catch (error) {

View File

@ -172,11 +172,12 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
this.nameProps = {
label: "Name",
ariaLabel: "Name",
defaultValue: this.props.notebookName,
defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"),
required: true,
onChange: (event, newValue) => {
this.props.onChangeName(newValue);
this.setState({ notebookName: newValue });
const notebookName = newValue + ".ipynb";
this.props.onChangeName(notebookName);
this.setState({ notebookName });
},
};

View File

@ -25,7 +25,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
<StackItem>
<StyledTextFieldBase
ariaLabel="Name"
defaultValue="SampleNotebook.ipynb"
defaultValue="SampleNotebook"
label="Name"
onChange={[Function]}
required={true}

View File

@ -11,6 +11,7 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount;
container: Explorer;
junoClient: JunoClient;
selectedTab: GalleryViewerTab;
notebookUrl?: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
@ -21,24 +22,42 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
*/
export default class GalleryTab extends TabsBase {
private container: Explorer;
private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps;
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
constructor(options: GalleryTabOptions) {
super(options);
this.container = options.container;
const props: GalleryAndNotebookViewerComponentProps = {
this.galleryAndNotebookViewerComponentProps = {
container: options.container,
junoClient: options.junoClient,
notebookUrl: options.notebookUrl,
galleryItem: options.galleryItem,
isFavorite: options.isFavorite,
selectedTab: GalleryViewerTab.OfficialSamples,
selectedTab: options.selectedTab,
sortBy: SortBy.MostViewed,
searchText: undefined,
};
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
this.galleryAndNotebookViewerComponentProps
);
}
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props);
public reset(options: GalleryTabOptions) {
this.container = options.container;
this.galleryAndNotebookViewerComponentProps.container = options.container;
this.galleryAndNotebookViewerComponentProps.junoClient = options.junoClient;
this.galleryAndNotebookViewerComponentProps.notebookUrl = options.notebookUrl;
this.galleryAndNotebookViewerComponentProps.galleryItem = options.galleryItem;
this.galleryAndNotebookViewerComponentProps.isFavorite = options.isFavorite;
this.galleryAndNotebookViewerComponentProps.selectedTab = options.selectedTab;
this.galleryAndNotebookViewerComponentProps.sortBy = SortBy.MostViewed;
this.galleryAndNotebookViewerComponentProps.searchText = undefined;
this.galleryAndNotebookViewerComponentAdapter.reset();
this.galleryAndNotebookViewerComponentAdapter.triggerRender();
}
protected getContainer(): Explorer {

View File

@ -7,7 +7,7 @@ import {
GalleryViewerComponent,
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer/Explorer";
import { IChoiceGroupOption, IChoiceGroupProps } from "office-ui-fabric-react";
import { IChoiceGroupOption, IChoiceGroupProps, IProgressIndicatorProps } from "office-ui-fabric-react";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
import { handleError } from "../Common/ErrorHandlingUtils";
import { HttpStatusCodes } from "../Common/Constants";
@ -81,6 +81,14 @@ export interface GalleryViewerProps {
}
export interface DialogHost {
showOkModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
progressIndicatorProps?: IProgressIndicatorProps
): void;
showOkCancelModalDialog(
title: string,
msg: string,
@ -88,8 +96,10 @@ export interface DialogHost {
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
progressIndicatorProps?: IProgressIndicatorProps,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
): void;
}
@ -108,8 +118,17 @@ export function reportAbuse(
undefined,
"Report Abuse",
async () => {
const clearSubmitReportNotification = NotificationConsoleUtils.logConsoleProgress(
`Submitting your report on ${data.name} violating code of conduct`
dialogHost.showOkCancelModalDialog(
"Report Abuse",
`Submitting your report on ${data.name} violating code of conduct`,
"Reporting...",
undefined,
"Cancel",
undefined,
{},
undefined,
undefined,
true
);
try {
@ -118,9 +137,16 @@ export function reportAbuse(
throw new Error(`Received HTTP ${response.status} when submitting report for ${data.name}`);
}
NotificationConsoleUtils.logConsoleInfo(
`Your report on ${data.name} has been submitted. Thank you for reporting the violation.`
dialogHost.showOkModalDialog(
"Report Abuse",
`Your report on ${data.name} has been submitted. Thank you for reporting the violation.`,
"OK",
undefined,
{
percentComplete: 1,
}
);
onComplete(response.data);
} catch (error) {
handleError(
@ -128,12 +154,21 @@ export function reportAbuse(
"GalleryUtils/reportAbuse",
`Failed to submit report on ${data.name} violating code of conduct`
);
}
clearSubmitReportNotification();
dialogHost.showOkModalDialog(
"Report Abuse",
`Failed to submit report on ${data.name} violating code of conduct`,
"OK",
undefined,
{
percentComplete: 1,
}
);
}
},
"Cancel",
undefined,
undefined,
{
label: "How does this content violate the code of conduct?",
options: abuseCategories,