Skip to content

Commit

Permalink
add spinner in saving project, can avoid multiple commit (microsoft#617)
Browse files Browse the repository at this point in the history
* add "Saving Project" Spinner

* add isFileExists to IStorageProvider

* sync state.project when project data updated

* Persist user input when project already exists
  • Loading branch information
yongbing-chen authored Sep 30, 2020
1 parent 53044f7 commit 355ca0b
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 31 deletions.
1 change: 1 addition & 0 deletions src/common/mockFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export default class MockFactory {
createContainer: jest.fn(),
deleteContainer: jest.fn(),
getAssets: jest.fn(),
isFileExists: jest.fn(),
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/electron/providers/storage/localFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export default class LocalFileSystem implements IStorageProvider {
return this.listItems(path.normalize(folderPath), (stats) => !stats.isDirectory());
}

public isFileExists(filePath: string): Promise<boolean> {
return Promise.resolve(fs.existsSync(path.normalize(filePath)));
}

public listContainers(folderPath: string): Promise<string[]> {
return this.listItems(path.normalize(folderPath), (stats) => stats.isDirectory());
}
Expand Down
8 changes: 8 additions & 0 deletions src/providers/storage/azureBlobStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ export class AzureBlobStorage implements IStorageProvider {
}
}

/**
* check file is exists
* @param filePath
*/
public async isFileExists(filePath: string) :Promise<boolean> {
const client = this.containerClient.getBlobClient(filePath);
return await client.exists();
}
/**
* Lists the containers with in the Azure Blob Storage account
* @param path - NOT USED IN CURRENT IMPLEMENTATION. Lists containers in storage account.
Expand Down
9 changes: 9 additions & 0 deletions src/providers/storage/localFileSystemProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
return IpcRendererProxy.send(`${PROXY_NAME}:listFiles`, [folderPath]);
}

/**
* check file is exists
* @param fileName Name of target file
*/
public isFileExists(fileName: string): Promise<boolean> {
const filePath = [this.options.folderPath, fileName].join("/");
return IpcRendererProxy.send(`${PROXY_NAME}:isFileExists`, [filePath]);
}

/**
* List directories inside another directory
* @param folderName - Directory from which to list directories
Expand Down
3 changes: 3 additions & 0 deletions src/providers/storage/storageProviderFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class TestStorageProvider implements IStorageProvider {
public listFiles(folderPath?: string): Promise<string[]> {
throw new Error("Method not implemented.");
}
isFileExists(filepath: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
public listContainers(folderPath?: string): Promise<string[]> {
throw new Error("Method not implemented.");
}
Expand Down
1 change: 1 addition & 0 deletions src/providers/storage/storageProviderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface IStorageProvider extends IAssetProvider {
isValidProjectConnection(filepath?): Promise<boolean>;

listFiles(folderPath?: string, ext?: string): Promise<string[]>;
isFileExists(filepath: string): Promise<boolean>;
listContainers(folderPath?: string): Promise<string[]>;

createContainer(folderPath: string): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
font-size: 90%;

@media (max-width: 1920px) {
display: flex;
align-items: center;
display: flex;
align-items: center;
}

.hint-content {
Expand All @@ -63,7 +63,7 @@
}

.rv-hint {
white-space: nowrap;
white-space: nowrap;
}
}

Expand All @@ -80,3 +80,18 @@
display: flex;
align-items: center;
}

.project-saving {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.8);
text-align: center;
display: flex;
.project-saving-spinner {
margin: auto;
font-size: 24px;
}
}
61 changes: 41 additions & 20 deletions src/react/components/pages/projectSettings/projectSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Redirect } from "react-router";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { RouteComponentProps } from "react-router-dom";
import { FontIcon } from "@fluentui/react";
import { FontIcon, Label, Spinner, SpinnerSize } from "@fluentui/react";
import ProjectForm from "./projectForm";
import { constants } from "../../../../common/constants";
import { strings, interpolate } from "../../../../common/strings";
Expand Down Expand Up @@ -43,6 +43,7 @@ export interface IProjectSettingsPageState {
project: IProject;
action: ProjectSettingAction;
isError: boolean;
isCommiting: boolean;
}

function mapStateToProps(state: IApplicationState) {
Expand Down Expand Up @@ -72,6 +73,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
project: this.props.project,
action: null,
isError: false,
isCommiting: false,
};

public async componentDidMount() {
Expand Down Expand Up @@ -107,6 +109,11 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
}
}

componentWillUnmount() {
if (this.state.project?.id) {
removeStorageItem(constants.projectFormTempKey);
}
}
// Hide ProjectMetrics for private-preview
public render() {
return (
Expand All @@ -132,6 +139,14 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
{this.state.isError &&
<Redirect to="/" />
}
{this.state.isCommiting &&
<div className="project-saving">
<div className="project-saving-spinner">
<Label className="p-0" ></Label>
<Spinner size={SpinnerSize.large} label="Saving Project..." ariaLive="assertive" labelPosition="right" />
</div>
</div>
}
</div>
);
}
Expand Down Expand Up @@ -171,34 +186,40 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
private onFormChange = (project: IProject) => {
if (this.isPartialProject(project)) {
setStorageItem(constants.projectFormTempKey, JSON.stringify(project));
this.setState({ project });
}
}

private onFormSubmit = async (project: IProject) => {
const isNew = !(!!project.id);
try {
this.setState({ isCommiting: true });
const projectService = new ProjectService();
if (!(await projectService.isValidProjectConnection(project))) {
return;
}

const projectService = new ProjectService();
if (!(await projectService.isValidProjectConnection(project))) {
return;
}

if (await this.isValidProjectName(project, isNew)) {
toast.error(interpolate(strings.projectSettings.messages.projectExisted, { project }));
return;
}
if (await this.isValidProjectName(project, isNew)) {
toast.error(interpolate(strings.projectSettings.messages.projectExisted, { project }));
return;
}

await this.deleteOldProjectWhenRenamed(project, isNew);
await this.props.applicationActions.ensureSecurityToken(project);
await this.props.projectActions.saveProject(project, false, true);
removeStorageItem(constants.projectFormTempKey);
await this.deleteOldProjectWhenRenamed(project, isNew);
await this.props.applicationActions.ensureSecurityToken(project);
await this.props.projectActions.saveProject(project, false, true);
// removeStorageItem(constants.projectFormTempKey);

toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));
toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));

if (isNew) {
this.props.history.push(`/projects/${this.props.project.id}/edit`);
} else {
this.props.history.goBack();
if (isNew) {
this.props.history.push(`/projects/${this.props.project.id}/edit`);
} else {
this.props.history.goBack();
}
} finally {
this.setState({ isCommiting: false });
}

}

private onFormCancel = () => {
Expand All @@ -210,7 +231,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
* Checks whether a project is partially populated
*/
private isPartialProject = (project: IProject): boolean => {
return project && !(!!project.id) &&
return project &&
(
!!project.name
|| !!project.description
Expand Down
9 changes: 1 addition & 8 deletions src/services/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,7 @@ export default class ProjectService implements IProjectService {

public async isProjectNameAlreadyUsed(project: IProject): Promise<boolean> {
const storageProvider = StorageProviderFactory.createFromConnection(project.sourceConnection);
const fileList = await storageProvider.listFiles("", constants.projectFileExtension/*ext*/);
for (const fileName of fileList) {
if (fileName === `${project.name}${constants.projectFileExtension}`) {
return true;
}
}

return false;
return await storageProvider.isFileExists(`${project.name}${constants.projectFileExtension}`);
}

public async isValidProjectConnection(project: IProject): Promise<boolean> {
Expand Down

0 comments on commit 355ca0b

Please sign in to comment.