feat: support electron for on premise solution (#333)
* feat: support electron for on premise solution * refactor: remove trailing whitespaces and empty lines * feat: add app icon for os taskbar * refactor: fix indent, variable naming, and brackets * docs: update test runbook and third party notices
This commit is contained in:
Родитель
8297b18a08
Коммит
ca0bd0c2ab
3
Procfile
3
Procfile
|
@ -1 +1,2 @@
|
|||
react: npm run react-start
|
||||
react: yarn react-start
|
||||
electron: yarn electron-start
|
||||
|
|
|
@ -330,3 +330,81 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|||
THE SOFTWARE.
|
||||
|
||||
==========
|
||||
|
||||
foreman
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) IBM Corp. 2012,2016. All Rights Reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
==========
|
||||
|
||||
file-type
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
==========
|
||||
|
||||
electron
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2013-2020 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
==========
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.8 KiB |
|
@ -1,5 +1,49 @@
|
|||
# Test Runbook
|
||||
|
||||
## **Feat: support Electron for on premise solution**
|
||||
|
||||
> ### Feature description ###
|
||||
- Support FoTT's existing features in Electon
|
||||
- Support local file system provider in Electron
|
||||
|
||||
> ### Use Case ###
|
||||
|
||||
**`As`** a user
|
||||
**`I want`** to use FoTT's existing features through a desktop app
|
||||
**`So`** I don't have to use a browser to use FoTT
|
||||
|
||||
**`As`** a user
|
||||
**`I want`** to use files in my local file system
|
||||
**`So`** I can keep all files on premise
|
||||
|
||||
> ### Acceptance criteria ###
|
||||
|
||||
#### Scenario One ####
|
||||
|
||||
**`Given`** I've installed new dependencies and started FoTT in Electron.\
|
||||
**`When`** I click a command item in the title bar.\
|
||||
**`Then`** FoTT should perform the command as expected.\
|
||||
|
||||
#### **Scenario Two** ####
|
||||
|
||||
**`Given`** I've installed new dependencies and started FoTT in Electron.\
|
||||
**`When`** I perform an action for any existing feature.\
|
||||
**`Then`** FoTT should perform as expected (the same as through a browser).\
|
||||
|
||||
#### ***Scenario Three*** ####
|
||||
|
||||
**`Given`** I've installed new dependencies and started FoTT in Electron.\
|
||||
**`When`** I create a new connection with local file system as the provider.\
|
||||
**`Then`** I should be able to create a project with the created connection.\
|
||||
|
||||
#### ***Scenario Four*** ####
|
||||
|
||||
**`Given`** I've installed new dependencies and started FoTT in Electron. And, I have an existing project in my local file system.\
|
||||
**`When`** I click "Open local project" on the home page and select the existing project.\
|
||||
**`Then`** FoTT should load the project as expected.\
|
||||
|
||||
___
|
||||
|
||||
## **Fix: enable to reorder tags quickly**
|
||||
|
||||
> ### Feature description ###
|
||||
|
@ -20,7 +64,6 @@ Enable reordering tags quickly
|
|||
**`When`** I clicking fast on tags buttons `'Move tag up'` or `'Move tag down'`\
|
||||
**`Then`** it moves without visible jittering.
|
||||
|
||||
___
|
||||
___
|
||||
|
||||
## **Enable rerun OCR for current or all documents**
|
||||
|
@ -51,8 +94,6 @@ Adding the following buttons to the canvas command bar:
|
|||
**`When`** I click "Run OCR on all documents" in the canvas command bar\
|
||||
**`Then`** I should see "Running OCR..." for all documents. When running OCR finishes for each document, I should be ale to view each document's updated OCR JSON file.
|
||||
|
||||
|
||||
___
|
||||
___
|
||||
|
||||
## **Feat: enable compose model and add model name when training a new model**
|
||||
|
@ -87,7 +128,6 @@ ___
|
|||
**`When`** I type customerized model name in input field and click compose button on modal\
|
||||
**`Then`** I should see "Model is composing, please wait...". After that the list shows up again, new composed model with given name will be on the top of the list. The new composed model also has a "combine" icon.
|
||||
|
||||
|
||||
#### ***Scenario Three*** ####
|
||||
|
||||
**`Given`** I've opened a project containing documents and I'm on the Model Compose page.\
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"private": true,
|
||||
"main": "build/main.js",
|
||||
"dependencies": {
|
||||
"file-type": "^14.6.2",
|
||||
"@azure/storage-blob": "10.3.0",
|
||||
"@fluentui/react": "^7.117.2",
|
||||
"axios": "^0.19.0",
|
||||
|
@ -55,9 +56,9 @@
|
|||
"webpack:prod": "webpack --config ./config/webpack.prod.js",
|
||||
"electron:run:dev": "yarn webpack:dev && electron . --remote-debugging-port=9223",
|
||||
"electron:run:prod": "yarn webpack:prod && electron . --remote-debugging-port=9223",
|
||||
"electron:start:dev": "yarn webpack:dev && yarn electron-start",
|
||||
"electron:start:dev": "yarn electron-start",
|
||||
"electron:start:prod": "yarn webpack:prod && yarn electron-start",
|
||||
"electron-start": "node src/electron/start",
|
||||
"electron-start": "yarn webpack:dev && node src/electron/start",
|
||||
"tslint": "./node_modules/.bin/tslint 'src/**/*.ts*'",
|
||||
"tslintfix": "./node_modules/.bin/tslint 'src/**/*.ts*' --fix"
|
||||
},
|
||||
|
@ -92,6 +93,7 @@
|
|||
"@types/reactstrap": "^8.2.0",
|
||||
"@types/redux-logger": "^3.0.7",
|
||||
"acorn": "^7.1.1",
|
||||
"foreman": "^3.0.1",
|
||||
"electron": "^8.3.0",
|
||||
"electron-builder": "^22.6.1",
|
||||
"enzyme": "^3.10.0",
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -96,9 +96,14 @@ export async function decryptObject<T = any>(encodedMessage: string, secret: str
|
|||
return JSON.parse(json) as T;
|
||||
}
|
||||
|
||||
export async function sha256Hash(message: string) {
|
||||
const buffer = await crypto.subtle.digest("SHA-256", encodeUtf8(message));
|
||||
return encodeHex(buffer);
|
||||
export async function sha256Hash(message: string, nodejsMode?: boolean) {
|
||||
if (nodejsMode) {
|
||||
const nodejsCrypto = await require('crypto');
|
||||
return await nodejsCrypto.createHash('sha256').update(message).digest("hex");
|
||||
} else {
|
||||
const buffer = await crypto.subtle.digest("SHA-256", encodeUtf8(message));
|
||||
return encodeHex(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
async function importKey(secretBytes: ArrayBuffer) {
|
||||
|
|
|
@ -117,12 +117,15 @@ export const english: IAppStrings = {
|
|||
},
|
||||
},
|
||||
train: {
|
||||
modelNameTitle: "Model name",
|
||||
labelFolderTitle: "Label folder URI",
|
||||
defaultLabelFolderURL: "https://example.com/folder",
|
||||
title: "Train",
|
||||
training: "Training",
|
||||
pleaseWait: "Please wait",
|
||||
notTrainedYet: "Not trained yet",
|
||||
backEndNotAvailable: "Checkbox feature will work in future version of Form Recognizer service, please stay tuned.",
|
||||
addName: "Add model name...",
|
||||
addName: "Add a model name...",
|
||||
},
|
||||
modelCompose: {
|
||||
title: "Model compose",
|
||||
|
@ -287,10 +290,10 @@ export const english: IAppStrings = {
|
|||
},
|
||||
},
|
||||
local: {
|
||||
title: "Local File System",
|
||||
folderPath: "Folder Path",
|
||||
selectFolder: "Select Folder",
|
||||
chooseFolder: "Choose Folder",
|
||||
title: "Local file system",
|
||||
folderPath: "Browse",
|
||||
selectFolder: "Select folder",
|
||||
chooseFolder: "Choose folder",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -118,6 +118,9 @@ export const spanish: IAppStrings = {
|
|||
},
|
||||
},
|
||||
train: {
|
||||
modelNameTitle: "Nombre del modelo",
|
||||
labelFolderTitle: "URI de carpeta de etiquetas",
|
||||
defaultLabelFolderURL: "https://example.com/folder",
|
||||
title: "Entrenar",
|
||||
training: "Entrenamiento",
|
||||
pleaseWait: "Por favor espera",
|
||||
|
|
|
@ -117,6 +117,9 @@ export interface IAppStrings {
|
|||
},
|
||||
};
|
||||
train: {
|
||||
modelNameTitle: string;
|
||||
labelFolderTitle: string;
|
||||
defaultLabelFolderURL: string;
|
||||
title: string;
|
||||
training: string;
|
||||
pleaseWait: string;
|
||||
|
|
|
@ -226,7 +226,58 @@ const DarkDefaultPalette: Partial<IPalette> = {
|
|||
neutralLighterAlt: "#201f1e",
|
||||
white: "#1b1a19",
|
||||
redDark: "#F1707B",
|
||||
};
|
||||
};
|
||||
|
||||
const lightGreyPalette = {
|
||||
themePrimary: "#B8B8B9",
|
||||
themeLighterAlt: "#070707",
|
||||
themeLighter: "#1d1d1e",
|
||||
themeLight: "#373738",
|
||||
themeTertiary: "#6f6f70",
|
||||
themeSecondary: "#a2a2a4",
|
||||
themeDarkAlt: "#bfbfc1",
|
||||
themeDark: "#c9c9cb",
|
||||
themeDarker: "#d7d7d8",
|
||||
neutralLighterAlt: "#4e5257",
|
||||
neutralLighter: "#55595d",
|
||||
neutralLight: "#606469",
|
||||
neutralQuaternaryAlt: "#666b6f",
|
||||
neutralQuaternary: "#6c7075",
|
||||
neutralTertiaryAlt: "#83888c",
|
||||
neutralTertiary: "#373738",
|
||||
neutralSecondary: "#6f6f70",
|
||||
neutralPrimaryAlt: "#a2a2a4",
|
||||
neutralPrimary: "#B8B8B9",
|
||||
neutralDark: "#c9c9cb",
|
||||
black: "#d7d7d8",
|
||||
white: "#474B4F"
|
||||
}
|
||||
|
||||
const subMenuPalette = {
|
||||
themePrimary: "#f5f5f5",
|
||||
themeLighterAlt: "#dadada",
|
||||
themeLighter: "#bfbfbf",
|
||||
themeLight: "#a4a4a4",
|
||||
themeTertiary: "#898989",
|
||||
themeSecondary: "#6e6e6e",
|
||||
themeDarkAlt: "#535353",
|
||||
themeDark: "#383838",
|
||||
themeDarker: "#1d1d1d",
|
||||
neutralLighterAlt: "#3f4246",
|
||||
neutralLighter: "#464a4d",
|
||||
neutralLight: "#525559",
|
||||
neutralQuaternaryAlt: "#595d61",
|
||||
neutralQuaternary: "#5f6367",
|
||||
neutralTertiaryAlt: "#787d81",
|
||||
neutralTertiary: "#e9e9e9",
|
||||
neutralSecondary: "#ececec",
|
||||
neutralPrimaryAlt: "#f0f0f0",
|
||||
neutralPrimary: "#dedede",
|
||||
neutralDark: "#f7f7f7",
|
||||
black: "#fbfbfb",
|
||||
white: "#373a3d"
|
||||
}
|
||||
|
||||
|
||||
const defaultDarkTheme = createTheme({palette: DarkDefaultPalette});
|
||||
const whiteTheme = createTheme({palette: whiteButtonPalette});
|
||||
|
@ -237,6 +288,8 @@ const blueTheme = createTheme({palette: blueButtonPalette});
|
|||
const darkTheme = createTheme({palette: darkThemePalette});
|
||||
const darkGreyTheme = createTheme({palette: darkGreyPalette});
|
||||
const greenWithWhiteBackgroundTheme = createTheme({palette: greenWithWhiteBackgroundPalette});
|
||||
const lightGreyTheme = createTheme({palette: lightGreyPalette});
|
||||
const subMenuTheme = createTheme({palette: subMenuPalette})
|
||||
|
||||
export function getPrimaryWhiteTheme() {
|
||||
return whiteTheme;
|
||||
|
@ -273,3 +326,11 @@ export function getGreenWithWhiteBackgroundTheme() {
|
|||
export function getDefaultDarkTheme() {
|
||||
return defaultDarkTheme;
|
||||
}
|
||||
|
||||
export function getSubMenuTheme() {
|
||||
return subMenuTheme;
|
||||
}
|
||||
|
||||
export function getLightGreyTheme() {
|
||||
return lightGreyTheme;
|
||||
}
|
||||
|
|
|
@ -225,6 +225,22 @@
|
|||
{
|
||||
"name": "BookAnswers",
|
||||
"unicode": "F8A4"
|
||||
},
|
||||
{
|
||||
"name": "ChromeRestore",
|
||||
"unicode": "E923"
|
||||
},
|
||||
{
|
||||
"name": "ChromeMinimize",
|
||||
"unicode": "E921"
|
||||
},
|
||||
{
|
||||
"name": "System",
|
||||
"unicode": "E770"
|
||||
},
|
||||
{
|
||||
"name": "SquareShape",
|
||||
"unicode": "F1A6"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { app, ipcMain, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import {
|
||||
app, ipcMain, BrowserWindow, BrowserWindowConstructorOptions,
|
||||
Menu, MenuItemConstructorOptions,
|
||||
} from "electron";
|
||||
import { IpcMainProxy } from "./common/ipcMainProxy";
|
||||
import LocalFileSystem from "./providers/storage/localFileSystem";
|
||||
|
||||
|
@ -14,10 +17,13 @@ async function createWindow() {
|
|||
const windowOptions: BrowserWindowConstructorOptions = {
|
||||
width: 1024,
|
||||
height: 768,
|
||||
minWidth: 450,
|
||||
minHeight: 100,
|
||||
frame: process.platform === "linux",
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#272B30",
|
||||
show: false,
|
||||
icon: "app-icons/Tag.png"
|
||||
};
|
||||
|
||||
const staticUrl = process.env.ELECTRON_START_URL || `file:///${__dirname}/index.html`;
|
||||
|
@ -37,19 +43,82 @@ async function createWindow() {
|
|||
ipcMainProxy.unregisterAll();
|
||||
});
|
||||
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow!.show();
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
if (!ipcMainProxy) {
|
||||
ipcMainProxy = new IpcMainProxy(ipcMain, mainWindow);
|
||||
|
||||
}
|
||||
ipcMainProxy = new IpcMainProxy(ipcMain, mainWindow);
|
||||
registerContextMenu(mainWindow);
|
||||
|
||||
const localFileSystem = new LocalFileSystem(mainWindow);
|
||||
ipcMainProxy.registerProxy("LocalFileSystem", localFileSystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds standard cut/copy/paste/etc context menu comments when right clicking input elements
|
||||
* @param browserWindow The browser window to apply the context-menu items
|
||||
*/
|
||||
function registerContextMenu(browserWindow: BrowserWindow): void {
|
||||
const selectionMenu = Menu.buildFromTemplate([
|
||||
{ role: "copy", accelerator: "CmdOrCtrl+C" },
|
||||
{ type: "separator" },
|
||||
{ role: "selectAll", accelerator: "CmdOrCtrl+A" },
|
||||
]);
|
||||
|
||||
const inputMenu = Menu.buildFromTemplate([
|
||||
{ role: "undo", accelerator: "CmdOrCtrl+Z" },
|
||||
{ role: "redo", accelerator: "CmdOrCtrl+Shift+Z" },
|
||||
{ type: "separator", label: "separator1"},
|
||||
{ role: "cut", accelerator: "CmdOrCtrl+X" },
|
||||
{ role: "copy", accelerator: "CmdOrCtrl+C" },
|
||||
{ role: "paste", accelerator: "CmdOrCtrl+V" },
|
||||
{ type: "separator", label: "separator2"},
|
||||
{ role: "selectAll", accelerator: "CmdOrCtrl+A" },
|
||||
]);
|
||||
|
||||
browserWindow.webContents.on("context-menu", (e, props) => {
|
||||
const { selectionText, isEditable } = props;
|
||||
if (isEditable) {
|
||||
inputMenu.popup({
|
||||
window: browserWindow,
|
||||
});
|
||||
} else if (selectionText && selectionText.trim() !== "") {
|
||||
selectionMenu.popup({
|
||||
window: browserWindow,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const menuItems: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: "File", submenu: [
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
// { role: "editMenu" },
|
||||
{
|
||||
label: "View", submenu: [
|
||||
{ role: "reload" },
|
||||
{ type: "separator", label: "separator1" },
|
||||
{ role: "toggleDevTools" },
|
||||
{ role: "togglefullscreen" },
|
||||
{ type: "separator", label: "separator2" },
|
||||
{ role: "resetZoom", label: "Reset Zoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Window", submenu: [
|
||||
{ role: "minimize" },
|
||||
{ role: "close" },
|
||||
]
|
||||
},
|
||||
];
|
||||
const menu = Menu.buildFromTemplate(menuItems);
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
|
|
|
@ -11,6 +11,9 @@ import { AssetService } from "../../../services/assetService";
|
|||
import { constants } from "../../../common/constants";
|
||||
import { strings } from "../../../common/strings";
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const FileType = require('file-type');
|
||||
|
||||
export default class LocalFileSystem implements IStorageProvider {
|
||||
|
||||
public storageType: StorageType.Local;
|
||||
|
@ -46,6 +49,10 @@ export default class LocalFileSystem implements IStorageProvider {
|
|||
});
|
||||
}
|
||||
|
||||
public getFileType(filePath: string): Promise<Buffer> {
|
||||
return FileType.fromFile(filePath);
|
||||
}
|
||||
|
||||
public readBinary(filePath: string): Promise<Buffer> {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
fs.readFile(path.normalize(filePath), (err: NodeJS.ErrnoException, data: Buffer) => {
|
||||
|
@ -146,11 +153,10 @@ export default class LocalFileSystem implements IStorageProvider {
|
|||
const result: IAsset[] = [];
|
||||
const files = await this.listFiles(path.normalize(folderPath));
|
||||
for (const file of files) {
|
||||
const asset = await AssetService.createAssetFromFilePath(file);
|
||||
const asset = await AssetService.createAssetFromFilePath(file, undefined, true);
|
||||
if (this.isSupportedAssetType(asset.type)) {
|
||||
const labelFileName = decodeURIComponent(`${asset.name}${constants.labelFileExtension}`);
|
||||
const ocrFileName = decodeURIComponent(`${asset.name}${constants.ocrFileExtension}`);
|
||||
|
||||
const labelFileName = decodeURIComponent(`${file}${constants.labelFileExtension}`);
|
||||
const ocrFileName = decodeURIComponent(`${file}${constants.ocrFileExtension}`);
|
||||
if (files.find((str) => str === labelFileName)) {
|
||||
asset.state = AssetState.Tagged;
|
||||
} else if (files.find((str) => str === ocrFileName)) {
|
||||
|
|
|
@ -303,6 +303,7 @@ export enum ErrorCode {
|
|||
HttpStatusNotFound = "notFound",
|
||||
HttpStatusTooManyRequests = "tooManyRequests",
|
||||
RequestSendError = "requestSendError",
|
||||
ProjectUploadError = "ProjectUploadError",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"title": "${strings.connections.providers.local.title}",
|
||||
"required": [
|
||||
"folderPath"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"folderPath": {
|
||||
"title": "${strings.connections.providers.local.folderPath}",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,6 +49,11 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
|
|||
return IpcRendererProxy.send(`${PROXY_NAME}:readText`, [filePath]);
|
||||
}
|
||||
|
||||
public getFileType(fileName: string): Promise<any> {
|
||||
const filePath = [this.options.folderPath, fileName].join("/");
|
||||
return IpcRendererProxy.send(`${PROXY_NAME}:getFileType`, [filePath]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read buffer from file
|
||||
* @param fileName Name of file from which to read buffer
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"folderPath": {
|
||||
"ui:widget": "localFolderPicker"
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ export interface IStorageProvider extends IAssetProvider {
|
|||
storageType: StorageType;
|
||||
|
||||
readText(filePath: string, ignoreNotFound?: boolean): Promise<string>;
|
||||
getFileType?(filePath: string): Promise<any>;
|
||||
readBinary(filePath: string): Promise<Buffer>;
|
||||
deleteFile(filePath: string, ignoreNotFound?: boolean, ignoreForbidden?: boolean): Promise<void>;
|
||||
|
||||
|
|
|
@ -176,3 +176,9 @@
|
|||
.align-self-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.flex-textbox {
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import React, { SyntheticEvent } from "react";
|
||||
import shortid from "shortid";
|
||||
import HtmlFileReader from "../../../../common/htmlFileReader";
|
||||
import { IFileInfo } from "../../../../models/applicationState";
|
||||
|
||||
/**
|
||||
* Properties for File Picker
|
||||
* @member onChange - Function to call on change of file selection
|
||||
* @member onError - Function to call on file picking error
|
||||
*/
|
||||
export interface IFilePickerProps {
|
||||
onChange: (sender: SyntheticEvent, fileText: IFileInfo) => void;
|
||||
onError: (sender: SyntheticEvent, error: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name - File Picker
|
||||
* @description - Pick file from local file system
|
||||
*/
|
||||
export default class FilePicker extends React.Component<IFilePickerProps> {
|
||||
private fileInput;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.fileInput = React.createRef();
|
||||
this.onFileUploaded = this.onFileUploaded.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call click on current file input
|
||||
*/
|
||||
public upload = () => {
|
||||
this.fileInput.current.click();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<input id={shortid.generate()} ref={this.fileInput} type="file" onChange={this.onFileUploaded} />
|
||||
);
|
||||
}
|
||||
|
||||
private onFileUploaded = (e) => {
|
||||
if (e.target.files.length === 0) {
|
||||
this.props.onError(e, "No files were selected");
|
||||
}
|
||||
|
||||
HtmlFileReader.readAsText(e.target.files[0])
|
||||
.then((fileInfo) => this.props.onChange(e, fileInfo))
|
||||
.catch((err) => this.props.onError(e, err));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import React from "react";
|
||||
import { LocalFileSystemProxy } from "../../../../providers/storage/localFileSystemProxy";
|
||||
import { strings } from "../../../../common/strings";
|
||||
import { TextField, PrimaryButton } from "@fluentui/react";
|
||||
import { getPrimaryGreenTheme, getGreenWithWhiteBackgroundTheme } from "../../../../common/themes";
|
||||
|
||||
/**
|
||||
* Properties for Local Folder Picker
|
||||
* @member id - ID for HTML form control element
|
||||
* @member value - Initial value for picker
|
||||
* @member onChange - Function to call on change to selected value
|
||||
*/
|
||||
interface ILocalFolderPickerProps {
|
||||
id?: string;
|
||||
value: string;
|
||||
onChange: (value) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* State for Local Folder Picker
|
||||
* @member value - Selected folder
|
||||
*/
|
||||
interface ILocalFolderPickerState {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name - Local Folder Picker
|
||||
* @description - Select folder from local file system
|
||||
*/
|
||||
export default class LocalFolderPicker extends React.Component<ILocalFolderPickerProps, ILocalFolderPickerState> {
|
||||
private localFileSystem: LocalFileSystemProxy;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
value: this.props.value || "",
|
||||
};
|
||||
|
||||
this.localFileSystem = new LocalFileSystemProxy();
|
||||
this.selectLocalFolder = this.selectLocalFolder.bind(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { value } = this.state;
|
||||
|
||||
return (
|
||||
<div className="input-group">
|
||||
<TextField
|
||||
className="mr-2 flex-textbox"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
style={{cursor: "pointer"}}
|
||||
onClick={this.selectLocalFolder}
|
||||
readOnly={true}
|
||||
value={value}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="keep-button-80px"
|
||||
theme={getPrimaryGreenTheme()}
|
||||
text={strings.connections.providers.local.folderPath}
|
||||
autoFocus={true}
|
||||
onClick={this.selectLocalFolder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
if (prevProps.value !== this.props.value) {
|
||||
this.setState({
|
||||
value: this.props.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private selectLocalFolder = async () => {
|
||||
const filePath = await this.localFileSystem.selectContainer();
|
||||
if (filePath) {
|
||||
this.setState({
|
||||
value: filePath,
|
||||
}, () => this.props.onChange(filePath));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ import { strings } from "../../../../common/strings";
|
|||
import { AppSettingsForm } from "./appSettingsForm";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { SkipButton } from "../../shell/skipButton";
|
||||
|
||||
/**
|
||||
* Props for App Settings Page
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
"description": {
|
||||
"title": "${strings.common.description}",
|
||||
"type": "string"
|
||||
},
|
||||
"providerType": {
|
||||
"title": "${strings.common.provider}",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import Form, { Widget, IChangeEvent, FormValidation } from "react-jsonschema-for
|
|||
import { FontIcon, PrimaryButton} from "@fluentui/react";
|
||||
import { IConnection } from "../../../../models/applicationState";
|
||||
import { strings, addLocValues } from "../../../../common/strings";
|
||||
import LocalFolderPicker from "../../common/localFolderPicker/localFolderPicker";
|
||||
import CustomFieldTemplate from "../../common/customField/customFieldTemplate";
|
||||
import ConnectionProviderPicker from "../../common/connectionProviderPicker/connectionProviderPicker";
|
||||
import { ProtectedInput } from "../../common/protectedInput/protectedInput";
|
||||
|
@ -53,6 +54,7 @@ export interface IConnectionFormState {
|
|||
*/
|
||||
export default class ConnectionForm extends React.Component<IConnectionFormProps, IConnectionFormState> {
|
||||
private widgets = {
|
||||
localFolderPicker: (LocalFolderPicker as any) as Widget,
|
||||
connectionProviderPicker: (ConnectionProviderPicker as any) as Widget,
|
||||
protectedInput: (ProtectedInput as any) as Widget,
|
||||
checkbox: CustomWidget(Checkbox, (props) => ({
|
||||
|
|
|
@ -16,7 +16,6 @@ import ConnectionForm from "./connectionForm";
|
|||
import ConnectionItem from "./connectionItem";
|
||||
import "./connectionsPage.scss";
|
||||
import { toast } from "react-toastify";
|
||||
import { SkipButton } from "../../shell/skipButton";
|
||||
|
||||
/**
|
||||
* Properties for Connection Page
|
||||
|
@ -149,7 +148,9 @@ export default class ConnectionPage extends React.Component<IConnectionPageProps
|
|||
|
||||
private onFormSubmit = async (connection: IConnection) => {
|
||||
try {
|
||||
connection.providerOptions["sas"] = connection.providerOptions["sas"].trim();
|
||||
if (connection.providerType === "azureBlobStorage") {
|
||||
connection.providerOptions["sas"] = connection.providerOptions["sas"].trim();
|
||||
}
|
||||
await this.props.actions.saveConnection(connection);
|
||||
toast.success(interpolate(strings.connections.messages.saveSuccess, { connection }));
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import IProjectActions, * as projectActions from "../../../../redux/actions/proj
|
|||
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
|
||||
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
|
||||
import { CloudFilePicker } from "../../common/cloudFilePicker/cloudFilePicker";
|
||||
import FilePicker from "../../common/filePicker/filePicker";
|
||||
import CondensedList from "../../common/condensedList/condensedList";
|
||||
import Confirm from "../../common/confirm/confirm";
|
||||
import "./homePage.scss";
|
||||
|
@ -24,7 +25,7 @@ import {
|
|||
import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory";
|
||||
import { decryptProject } from "../../../../common/utils";
|
||||
import { toast } from "react-toastify";
|
||||
import { SkipButton } from "../../shell/skipButton";
|
||||
import { isElectron } from "../../../../common/hostProcess";
|
||||
|
||||
export interface IHomePageProps extends RouteComponentProps, React.Props<HomePage> {
|
||||
recentProjects: IProject[];
|
||||
|
@ -64,9 +65,11 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
cloudPickerOpen: false,
|
||||
};
|
||||
|
||||
private filePicker: React.RefObject<FilePicker> = React.createRef();
|
||||
private newProjectRef = React.createRef<HTMLAnchorElement>();
|
||||
private deleteConfirmRef = React.createRef<Confirm>();
|
||||
private cloudFilePickerRef = React.createRef<CloudFilePicker>();
|
||||
private importConfirmRef: React.RefObject<Confirm> = React.createRef();
|
||||
|
||||
public async componentDidMount() {
|
||||
this.props.appTitleActions.setTitle("Welcome");
|
||||
|
@ -92,6 +95,18 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
<div>{strings.homePage.newProject}</div>
|
||||
</a>
|
||||
</li>
|
||||
{isElectron() &&
|
||||
<li>
|
||||
<a href="#" className="p-5 file-upload"
|
||||
onClick={() => this.filePicker.current.upload()} >
|
||||
<FontIcon iconName="System" className="icon-9x" />
|
||||
<h6>{strings.homePage.openLocalProject.title}</h6>
|
||||
</a>
|
||||
<FilePicker ref={this.filePicker}
|
||||
onChange={this.onProjectFileUpload}
|
||||
onError={this.onProjectFileUploadError} />
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
{/*Open Cloud Project*/}
|
||||
{/* eslint-disable-next-line */}
|
||||
|
@ -185,4 +200,30 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
private deleteProject = async (project: IProject) => {
|
||||
await this.props.actions.deleteProject(project);
|
||||
}
|
||||
|
||||
private onProjectFileUpload = async (e, project) => {
|
||||
let projectJson: IProject;
|
||||
|
||||
try {
|
||||
projectJson = JSON.parse(project.content);
|
||||
} catch (error) {
|
||||
throw new AppError(ErrorCode.ProjectInvalidJson, "Error parsing JSON");
|
||||
}
|
||||
|
||||
if (projectJson.name === null || projectJson.name === undefined) {
|
||||
try {
|
||||
await this.importConfirmRef.current.open(project);
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
} else {
|
||||
await this.loadSelectedProject(projectJson);
|
||||
}
|
||||
}
|
||||
|
||||
private onProjectFileUploadError = (e, error: any) => {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@ import { parseTiffData, renderTiffToCanvas, loadImageToCanvas } from "../../../.
|
|||
import { constants } from "../../../../common/constants";
|
||||
import { getPrimaryGreenTheme, getPrimaryWhiteTheme,
|
||||
getGreenWithWhiteBackgroundTheme } from "../../../../common/themes";
|
||||
import { SkipButton } from "../../shell/skipButton";
|
||||
import axios from "axios";
|
||||
|
||||
const cMapUrl = constants.pdfjsCMapUrl(pdfjsLib.version);
|
||||
|
@ -220,7 +219,6 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
|
|||
<div className="container-space-between">
|
||||
<Dropdown
|
||||
className="sourceDropdown"
|
||||
defaultSelectedKey={this.state.sourceOption}
|
||||
selectedKey={this.state.sourceOption}
|
||||
options={sourceOptions}
|
||||
disabled={this.state.isPredicting || this.state.isFetching}
|
||||
|
|
|
@ -19,7 +19,6 @@ import "./projectSettingsPage.scss";
|
|||
import { ProjectSettingAction } from "./projectSettingAction";
|
||||
import ProjectService from "../../../../services/projectService";
|
||||
import { getStorageItem, setStorageItem, removeStorageItem } from "../../../../redux/middleware/localStorage";
|
||||
import { SkipButton } from "../../shell/skipButton";
|
||||
|
||||
/**
|
||||
* Properties for Project Settings Page
|
||||
|
|
|
@ -74,4 +74,8 @@
|
|||
&-text {
|
||||
padding-left:4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label-folder-url-input {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ import Alert from "../../common/alert/alert";
|
|||
import url from "url";
|
||||
import PreventLeaving from "../../common/preventLeaving/preventLeaving";
|
||||
import ServiceHelper from "../../../../services/serviceHelper";
|
||||
import { getPrimaryGreenTheme } from "../../../../common/themes";
|
||||
import { getPrimaryGreenTheme, getGreenWithWhiteBackgroundTheme } from "../../../../common/themes";
|
||||
|
||||
export interface ITrainPageProps extends RouteComponentProps, React.Props<TrainPage> {
|
||||
connections: IConnection[];
|
||||
|
@ -37,6 +37,7 @@ export interface ITrainPageProps extends RouteComponentProps, React.Props<TrainP
|
|||
}
|
||||
|
||||
export interface ITrainPageState {
|
||||
inputedLabelFolderURL: string;
|
||||
trainMessage: string;
|
||||
isTraining: boolean;
|
||||
currTrainRecord: ITrainRecordProps;
|
||||
|
@ -79,6 +80,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputedLabelFolderURL: "",
|
||||
trainMessage: strings.train.notTrainedYet,
|
||||
isTraining: false,
|
||||
currTrainRecord: null,
|
||||
|
@ -105,6 +107,10 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
|
||||
public render() {
|
||||
const currTrainRecord = this.state.currTrainRecord;
|
||||
const localFileSystemProvider: boolean = this.props.project && this.props.project.sourceConnection &&
|
||||
this.props.project.sourceConnection.providerType === "localFileSystemProxy";
|
||||
const trainDisabled: boolean = localFileSystemProvider && (this.state.inputedLabelFolderURL.length === 0 ||
|
||||
this.state.inputedLabelFolderURL === strings.train.defaultLabelFolderURL);
|
||||
|
||||
return (
|
||||
<div className="train-page skipToMainContent" id="pageTrain">
|
||||
|
@ -132,24 +138,42 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
<div className="condensed-list-body">
|
||||
<div className="m-3">
|
||||
<h4 className="text-shadow-none"> Train a new model </h4>
|
||||
{!this.state.isTraining ? (
|
||||
{!this.state.isTraining && localFileSystemProvider &&
|
||||
<div>
|
||||
<span>
|
||||
Model Name
|
||||
{strings.train.labelFolderTitle}
|
||||
</span>
|
||||
<TextField
|
||||
placeholder={strings.train.addName}
|
||||
autoComplete="off"
|
||||
onChange={this.onTextChanged}
|
||||
>
|
||||
</TextField>
|
||||
className="label-folder-url-input"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
onFocus={this.removeDefaultInputedLabelFolderURL}
|
||||
onChange={this.setInputedLabelFolderURL}
|
||||
placeholder={strings.train.defaultLabelFolderURL}
|
||||
value={this.state.inputedLabelFolderURL}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<span>
|
||||
{strings.train.modelNameTitle}
|
||||
</span>
|
||||
<TextField
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
placeholder={strings.train.addName}
|
||||
autoComplete="off"
|
||||
onChange={this.onTextChanged}
|
||||
disabled={this.state.isTraining}
|
||||
>
|
||||
</TextField>
|
||||
{!this.state.isTraining ? (
|
||||
<div className="container-items-end">
|
||||
<PrimaryButton
|
||||
style={{"margin": "10px 0px"}}
|
||||
style={{"margin": "15px 0px"}}
|
||||
id="train_trainButton"
|
||||
theme={getPrimaryGreenTheme()}
|
||||
autoFocus={true}
|
||||
className="flex-center"
|
||||
onClick={this.handleTrainClick}>
|
||||
onClick={this.handleTrainClick}
|
||||
disabled={trainDisabled}>
|
||||
<FontIcon iconName="MachineLearning" />
|
||||
<h6 className="d-inline text-shadow-none ml-2 mb-0">
|
||||
{strings.train.title} </h6>
|
||||
|
@ -193,6 +217,16 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
);
|
||||
}
|
||||
|
||||
private removeDefaultInputedLabelFolderURL = () => {
|
||||
if (this.state.inputedLabelFolderURL === strings.train.defaultLabelFolderURL) {
|
||||
this.setState({inputedLabelFolderURL: ""});
|
||||
}
|
||||
}
|
||||
|
||||
private setInputedLabelFolderURL = (event) => {
|
||||
this.setState({inputedLabelFolderURL: event.target.value});
|
||||
}
|
||||
|
||||
private onTextChanged = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text: string) => {
|
||||
this.modelName = text;
|
||||
}
|
||||
|
@ -247,12 +281,20 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
constants.apiModelsPath,
|
||||
);
|
||||
const provider = this.props.project.sourceConnection.providerOptions as any;
|
||||
const trainSourceURL = provider.sas;
|
||||
let trainSourceURL;
|
||||
let trainPrefix;
|
||||
|
||||
if (this.props.project.sourceConnection.providerType === "localFileSystemProxy") {
|
||||
trainSourceURL = this.state.inputedLabelFolderURL;
|
||||
trainPrefix = ""
|
||||
} else {
|
||||
trainSourceURL = provider.sas;
|
||||
trainPrefix = this.props.project.folderPath ? this.props.project.folderPath : "";
|
||||
}
|
||||
const payload = {
|
||||
source: trainSourceURL,
|
||||
sourceFilter: {
|
||||
prefix: this.props.project.folderPath ? this.props.project.folderPath : "",
|
||||
prefix: trainPrefix,
|
||||
includeSubFolders: false,
|
||||
},
|
||||
useLabelFile: true,
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class TrainTable
|
|||
{!this.props.accuracies && <table className="accuracytable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th> Train Message </th>
|
||||
<th> Train message </th>
|
||||
<td> {this.props.trainMessage} </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -378,3 +378,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-close-icon {
|
||||
padding: 6px 18px;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: red;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.end-icons {
|
||||
padding: 6px 18px;
|
||||
color: #ccc;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: $lighter-2;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { FontIcon } from "@fluentui/react";
|
||||
import { FontIcon, CommandBarButton, IButtonStyles, IIconProps, IOverflowSetItemProps, OverflowSet, Customizer, ICustomizations, Separator, ContextualMenuItemType } from "@fluentui/react";
|
||||
import { IApplicationState } from "../../../models/applicationState";
|
||||
import { PlatformType } from "../../../common/hostProcess";
|
||||
import { PlatformType, isElectron } from "../../../common/hostProcess";
|
||||
import { getLightGreyTheme, getSubMenuTheme } from "../../../common/themes";
|
||||
import "./titleBar.scss";
|
||||
|
||||
export interface ITitleBarProps extends React.Props<TitleBar> {
|
||||
|
@ -14,9 +15,11 @@ export interface ITitleBarProps extends React.Props<TitleBar> {
|
|||
}
|
||||
|
||||
export interface ITitleBarState {
|
||||
isElectron: boolean;
|
||||
platform: string;
|
||||
maximized: boolean;
|
||||
fullscreen: boolean;
|
||||
menu: Electron.Menu;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: IApplicationState) {
|
||||
|
@ -28,29 +31,202 @@ function mapStateToProps(state: IApplicationState) {
|
|||
@connect(mapStateToProps, null)
|
||||
export class TitleBar extends React.Component<ITitleBarProps, ITitleBarState> {
|
||||
public state: ITitleBarState = {
|
||||
isElectron: false,
|
||||
platform: global && global.process && global.process.platform ? global.process.platform : PlatformType.Web,
|
||||
maximized: false,
|
||||
fullscreen: false,
|
||||
menu: null,
|
||||
};
|
||||
|
||||
private isElectron: boolean;
|
||||
private remote: Electron.Remote;
|
||||
private currentWindow: Electron.BrowserWindow;
|
||||
|
||||
public componentDidMount() {
|
||||
this.isElectron = isElectron();
|
||||
|
||||
if (this.isElectron) {
|
||||
this.remote = window.require("electron").remote as Electron.Remote;
|
||||
this.currentWindow = this.remote.getCurrentWindow();
|
||||
this.currentWindow.on("maximize", () => this.onMaximize(true));
|
||||
this.currentWindow.on("unmaximize", () => this.onMaximize(false));
|
||||
this.currentWindow.on("enter-full-screen", () => this.onFullScreen(true));
|
||||
this.currentWindow.on("leave-full-screen", () => this.onFullScreen(false));
|
||||
|
||||
this.setState({
|
||||
maximized: this.currentWindow.isMaximized(),
|
||||
fullscreen: this.currentWindow.isFullScreen(),
|
||||
menu: this.remote.Menu.getApplicationMenu(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (this.state.fullscreen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const titleBarTheme: ICustomizations = {
|
||||
settings: {
|
||||
theme: getLightGreyTheme(),
|
||||
},
|
||||
scopedSettings: {},
|
||||
};
|
||||
|
||||
const onRenderItem = (item: IOverflowSetItemProps): JSX.Element => {
|
||||
const buttonStyles: Partial<IButtonStyles> = {
|
||||
root: {
|
||||
padding: '0 5px',
|
||||
alignSelf: 'stretch',
|
||||
}
|
||||
};
|
||||
const iconStyles: Partial<IIconProps> = {
|
||||
style: {display: "none"}
|
||||
};
|
||||
|
||||
return (
|
||||
<CommandBarButton
|
||||
menuProps={item.subMenuProps}
|
||||
styles={buttonStyles}
|
||||
text={item.name}
|
||||
menuIconProps={iconStyles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const onRenderOverflowButton = (overflowItems: any[] | undefined): JSX.Element => {
|
||||
return (
|
||||
<CommandBarButton
|
||||
ariaLabel="More items"
|
||||
role="menuitem"
|
||||
menuProps={{ items: overflowItems! }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="title-bar bg-lighter-3">
|
||||
{(this.state.platform === PlatformType.Web) &&
|
||||
<div className="title-bar-icon">
|
||||
{typeof (this.props.icon) === "string" && <FontIcon iconName={this.props.icon} />}
|
||||
{typeof (this.props.icon) !== "string" && this.props.icon}
|
||||
</div>
|
||||
<div className="title-bar-icon">
|
||||
{typeof (this.props.icon) === "string" && <FontIcon iconName={this.props.icon} />}
|
||||
{typeof (this.props.icon) !== "string" && this.props.icon}
|
||||
</div>
|
||||
{this.isElectron &&
|
||||
<Customizer {...titleBarTheme}>
|
||||
<OverflowSet
|
||||
role="menubar"
|
||||
items={this.addDefaultMenuItems(this.state.menu)}
|
||||
onRenderOverflowButton={onRenderOverflowButton}
|
||||
onRenderItem={onRenderItem}
|
||||
/>
|
||||
</Customizer>
|
||||
}
|
||||
<div className="title-bar-main">{this.props.title || "Welcome"}</div>
|
||||
<div className="title-bar-controls">
|
||||
{this.props.children}
|
||||
{this.isElectron &&
|
||||
[
|
||||
<Separator vertical key="seperator" className="mr-2 ml-2"/>,
|
||||
<div key="minimizeDiv">
|
||||
<FontIcon className="end-icons" iconName="ChromeMinimize" onClick={this.minimizeWindow}/>
|
||||
</div>,
|
||||
<div key="resizeDiv">
|
||||
{this.state.maximized
|
||||
? <FontIcon className="end-icons" iconName="ChromeRestore" onClick={this.unmaximizeWindow}/>
|
||||
: <FontIcon className="end-icons" iconName="SquareShape" onClick={this.maximizeWindow}/>
|
||||
}
|
||||
</div>,
|
||||
<div key="closeDiv">
|
||||
<FontIcon className="app-close-icon" iconName="Cancel" onClick={this.closeWindow}/>
|
||||
</div>
|
||||
]
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
private addDefaultMenuItems = (menu: Electron.Menu) => {
|
||||
if (!menu) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return menu.items.reduce(this.renderMenuItem, []);
|
||||
}
|
||||
|
||||
private renderMenuItem = (results, menuItem: Electron.MenuItem) => {
|
||||
if (!menuItem.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemType: string = menuItem["type"];
|
||||
|
||||
switch (itemType) {
|
||||
case "separator":
|
||||
results.push({
|
||||
key: menuItem.label,
|
||||
itemType: ContextualMenuItemType.Divider,
|
||||
})
|
||||
break;
|
||||
case "submenu":
|
||||
results.push({
|
||||
key: menuItem.label,
|
||||
name: menuItem.label,
|
||||
subMenuProps: {
|
||||
theme: getSubMenuTheme(),
|
||||
items: this.addDefaultMenuItems(menuItem["submenu"]),
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "normal":
|
||||
results.push({
|
||||
key: menuItem.label,
|
||||
name: menuItem.label,
|
||||
onClick: (e) => this.onMenuItemClick(e, menuItem)
|
||||
});
|
||||
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private onMenuItemClick(e: any, menuItem: Electron.MenuItem) {
|
||||
if (menuItem.label === "Zoom In") {
|
||||
this.currentWindow.webContents.setZoomLevel(this.currentWindow.webContents.zoomLevel + .3);
|
||||
} else if (menuItem.label === "Zoom Out") {
|
||||
this.currentWindow.webContents.setZoomLevel(this.currentWindow.webContents.zoomLevel - .3);
|
||||
} else if (menuItem.label === "Reset Zoom") {
|
||||
this.currentWindow.webContents.setZoomLevel(-3);
|
||||
} else if (menuItem.click) {
|
||||
menuItem.click.call(menuItem, menuItem, this.currentWindow);
|
||||
}
|
||||
}
|
||||
|
||||
private onMaximize = (isMaximized: boolean) => {
|
||||
this.setState({
|
||||
maximized: isMaximized,
|
||||
});
|
||||
}
|
||||
|
||||
private onFullScreen = (isFullScreen: boolean) => {
|
||||
this.setState({
|
||||
fullscreen: isFullScreen,
|
||||
});
|
||||
}
|
||||
|
||||
private minimizeWindow = () => {
|
||||
this.currentWindow.minimize();
|
||||
}
|
||||
|
||||
private maximizeWindow = () => {
|
||||
this.currentWindow.maximize();
|
||||
}
|
||||
|
||||
private unmaximizeWindow = () => {
|
||||
this.currentWindow.unmaximize();
|
||||
}
|
||||
|
||||
private closeWindow = () => {
|
||||
this.currentWindow.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -65,6 +65,10 @@ export function registerIcons() {
|
|||
More: "\uE712",
|
||||
ReceiptProcessing: "\uE496",
|
||||
KeyPhraseExtraction: "\uE395",
|
||||
ChromeRestore: "\uE923",
|
||||
ChromeMinimize: "\uE921",
|
||||
System: "\uE770",
|
||||
SquareShape: "\uF1A6",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,12 +6,20 @@ import { AzureBlobStorage } from "./providers/storage/azureBlobStorage";
|
|||
import { StorageProviderFactory } from "./providers/storage/storageProviderFactory";
|
||||
import registerToolbar from "./registerToolbar";
|
||||
import { strings } from "./common/strings";
|
||||
import { HostProcessType } from "./common/hostProcess";
|
||||
import { LocalFileSystemProxy } from "./providers/storage/localFileSystemProxy";
|
||||
|
||||
/**
|
||||
* Registers storage, asset and export providers, as well as all toolbar items
|
||||
*/
|
||||
export default function registerProviders() {
|
||||
// Storage Providers
|
||||
StorageProviderFactory.register({
|
||||
name: "localFileSystemProxy",
|
||||
displayName: strings.connections.providers.local.title,
|
||||
platformSupport: HostProcessType.Electron,
|
||||
factory: (options) => new LocalFileSystemProxy(options),
|
||||
});
|
||||
StorageProviderFactory.register({
|
||||
name: "azureBlobStorage",
|
||||
displayName: strings.connections.providers.azureBlob.title,
|
||||
|
@ -19,6 +27,12 @@ export default function registerProviders() {
|
|||
});
|
||||
|
||||
// Asset Providers
|
||||
AssetProviderFactory.register({
|
||||
name: "localFileSystemProxy",
|
||||
displayName: strings.connections.providers.local.title,
|
||||
platformSupport: HostProcessType.Electron,
|
||||
factory: (options) => new LocalFileSystemProxy(options),
|
||||
});
|
||||
AssetProviderFactory.register({
|
||||
name: "azureBlobStorage",
|
||||
displayName: strings.connections.providers.azureBlob.title,
|
||||
|
|
|
@ -62,7 +62,7 @@ export class AssetService {
|
|||
* @param filePath - filepath of asset
|
||||
* @param fileName - name of asset
|
||||
*/
|
||||
public static async createAssetFromFilePath(filePath: string, fileName?: string): Promise<IAsset> {
|
||||
public static async createAssetFromFilePath(filePath: string, fileName?: string, nodejsMode?: boolean): Promise<IAsset> {
|
||||
Guard.empty(filePath);
|
||||
|
||||
const normalizedPath = filePath.toLowerCase();
|
||||
|
@ -76,7 +76,7 @@ export class AssetService {
|
|||
filePath = encodeFileURI(filePath, true);
|
||||
}
|
||||
|
||||
const hash = await sha256Hash(filePath);
|
||||
const hash = await sha256Hash(filePath, nodejsMode);
|
||||
// eslint-disable-next-line
|
||||
const pathParts = filePath.split(/[\\\/]/);
|
||||
fileName = fileName || pathParts[pathParts.length - 1];
|
||||
|
@ -87,7 +87,14 @@ export class AssetService {
|
|||
let assetFormat = extensionParts[0].toLowerCase();
|
||||
|
||||
if (supportedImageFormats.hasOwnProperty(assetFormat)) {
|
||||
const types = await this.getMimeType(filePath);
|
||||
let types;
|
||||
if (nodejsMode) {
|
||||
const FileType = require('file-type');
|
||||
const fileType = await FileType.fromFile(normalizedPath);
|
||||
types = [fileType.ext];
|
||||
} else {
|
||||
types = await this.getMimeType(filePath);
|
||||
}
|
||||
|
||||
// If file was renamed/spoofed - fix file extension to true MIME type and show message
|
||||
if (!types.includes(assetFormat)) {
|
||||
|
|
|
@ -82,10 +82,28 @@ export class OCRService {
|
|||
|
||||
private fetchOcrUriResult = async (filePath: string, ocrFileName: string) => {
|
||||
try {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
let body;
|
||||
let headers;
|
||||
if (filePath.startsWith("file:")) {
|
||||
const splitFilePath = filePath.split("/")
|
||||
const fileName = splitFilePath[splitFilePath.length - 1];
|
||||
const bodyAndType = await Promise.all(
|
||||
[
|
||||
this.storageProvider.readBinary(decodeURI(fileName)),
|
||||
this.storageProvider.getFileType(decodeURI(fileName))
|
||||
]
|
||||
);
|
||||
body = bodyAndType[0];
|
||||
const fileType = bodyAndType[1].mime;
|
||||
headers = { "Content-Type": fileType, "cache-control": "no-cache" };
|
||||
}
|
||||
else {
|
||||
body = { url: filePath };
|
||||
headers = { "Content-Type": "application/json" };
|
||||
}
|
||||
const response = await ServiceHelper.postWithAutoRetry(
|
||||
this.project.apiUriBase + "/formrecognizer/v2.0-preview/layout/analyze",
|
||||
{ url: filePath },
|
||||
body,
|
||||
{ headers },
|
||||
this.project.apiKey as string,
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче