Skip to content

Commit c9d5796

Browse files
authored
Update label files while project is loaded. (microsoft#880)
* Match supported schema versions. * Read $schema from file system into state asset. * Check and update schema state and write to storage file system. * Write updated state to file system.
1 parent 83e49ab commit c9d5796

File tree

10 files changed

+81
-29
lines changed

10 files changed

+81
-29
lines changed

src/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const appVersion = appVersionArr.join(".");
1111
const enableAPIVersionSelection = appInfo.enableAPIVersionSelection;
1212
const enablePredictionResultUpload = appInfo.enablePredictionResultUpload;
1313
const apiVersion = "v2.1-preview.3";
14+
const supportedLabelsSchemas = new Set(["http://www.azure.com/schema/formrecognizer/labels.json", "https://schema.cognitiveservices.azure.com/formrecognizer/2021-03-01/labels.json"]);
1415

1516
/**
1617
* Constants used throughout application
@@ -47,6 +48,7 @@ export const constants = {
4748
showOriginLabelsByDefault: true,
4849
fieldsSchema: "https://schema.cognitiveservices.azure.com/formrecognizer/2021-03-01/fields.json",
4950
labelsSchema: "https://schema.cognitiveservices.azure.com/formrecognizer/2021-03-01/labels.json",
51+
supportedLabelsSchemas,
5052
enableMultiPageField: false,
5153
pdfjsWorkerSrc(version: string) {
5254
return `https://fotts.azureedge.net/npm/pdfjs-dist/${version}/pdf.worker.js`;

src/electron/providers/storage/localFileSystem.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export default class LocalFileSystem implements IStorageProvider {
184184
const labelData = JSON.parse(json) as ILabelData;
185185
if (labelData) {
186186
asset.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled;
187+
asset.schema = labelData.$schema;
187188
}
188189
} else if (files.find((str) => str === ocrFileName)) {
189190
asset.state = AssetState.Visited;
@@ -213,6 +214,7 @@ export default class LocalFileSystem implements IStorageProvider {
213214
const labelData = JSON.parse(json) as ILabelData;
214215
if (labelData) {
215216
asset.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled;
217+
asset.schema = labelData.$schema;
216218
}
217219
} else if (files.find((str) => str === ocrFileName)) {
218220
asset.state = AssetState.Visited;

src/models/applicationState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ export interface IAsset {
186186
isRunningAutoLabeling?: boolean,
187187
cachedImage?: string,
188188
mimeType?: string,
189+
schema?: string
189190
}
190191

191192
export interface IPrebuiltSettings{

src/providers/storage/azureBlobStorage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export class AzureBlobStorage implements IStorageProvider {
219219
const labelData = JSON.parse(json) as ILabelData;
220220
if (labelData) {
221221
asset.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled;
222+
asset.schema = labelData.$schema;
222223
}
223224
} else if (files.find((str) => str === ocrFileName)) {
224225
asset.state = AssetState.Visited;
@@ -247,6 +248,7 @@ export class AzureBlobStorage implements IStorageProvider {
247248
const labelData = JSON.parse(json) as ILabelData;
248249
if (labelData) {
249250
asset.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled;
251+
asset.schema = labelData.$schema;
250252
}
251253
} else if (files.find((str) => str === ocrFileName)) {
252254
asset.state = AssetState.Visited;

src/react/components/pages/editorPage/canvas.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
489489
}
490490
}
491491
} else {
492-
if (labelsData.$schema === constants.labelsSchema) {
492+
if (constants.supportedLabelsSchemas.has(labelsData.$schema)) {
493493
relatedLabel = labelsData.labels.find((label) => label.label === this.encodeLabelString(tag));
494494
} else {
495495
relatedLabel = labelsData.labels.find((label) => label.label === tag);
@@ -498,7 +498,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
498498
if (relatedLabel &&
499499
(((relatedLabel.labelType === null || relatedLabel.labelType === undefined) && (selectedRegions[0].category === FeatureCategory.DrawnRegion))
500500
|| (relatedLabel.labelType !== null && relatedLabel.labelType !== undefined && relatedLabel.labelType !== selectedRegions[0].category))) {
501-
regions = this.convertLabelToRegion(relatedLabel, labelsData?.$schema === constants.labelsSchema);
501+
regions = this.convertLabelToRegion(relatedLabel, constants.supportedLabelsSchemas.has(labelsData?.$schema));
502502
regions.forEach((region) => {
503503
region.tags = [];
504504
if (region.isTableRegion) {
@@ -1556,7 +1556,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
15561556

15571557
public convertLabelDataToRegions = (labelData: ILabelData): IRegion[] => {
15581558
let regions = [];
1559-
const encodedSchema = labelData?.$schema === constants.labelsSchema;
1559+
const encodedSchema = constants.supportedLabelsSchemas.has(labelData?.$schema);
15601560

15611561
labelData?.labels?.forEach((label) => {
15621562
const newRegions = this.convertLabelToRegion(label, encodedSchema);
@@ -1643,7 +1643,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
16431643
};
16441644

16451645
const labels = (this.props?.selectedAsset?.labelData?.labels?.map(label => {
1646-
if (this.props.selectedAsset.labelData.$schema === constants.labelsSchema) {
1646+
if (constants.supportedLabelsSchemas.has(this.props.selectedAsset.labelData.$schema)) {
16471647
return ({
16481648
...label,
16491649
value: []
@@ -1701,7 +1701,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
17011701
if (!tableTag) return
17021702
label = labels.find(label => label?.label === this.getTableLabelFromRegion(tableTag, tableRegion));
17031703
} else {
1704-
if (this.props.selectedAsset.labelData?.$schema === constants.labelsSchema) {
1704+
if (constants.supportedLabelsSchemas.has(this.props.selectedAsset.labelData?.$schema)) {
17051705
label = labels.find(label => this.decodeLabelString(label?.label) === tag);
17061706
} else {
17071707
label = labels.find(label => label?.label === tag);

src/react/components/pages/editorPage/editorPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
341341
lockedTags={this.state.lockedTags}
342342
selectedRegions={this.state.selectedRegions}
343343
labels={labels}
344-
encoded={selectedAsset?.labelData?.$schema === constants.labelsSchema}
344+
encoded={constants.supportedLabelsSchemas.has(selectedAsset?.labelData?.$schema)}
345345
tableLabels={tableLabels}
346346
pageNumber={this.state.pageNumber}
347347
onChange={this.onTagsChanged}

src/redux/actions/projectActions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ describe("Project Redux Actions", () => {
127127
mockAssetService.prototype.getAssets = jest.fn(() => Promise.resolve(testAssets));
128128

129129
const project = MockFactory.createTestProject("TestProject");
130-
const results = await projectActions.loadAssets(project)(store.dispatch);
130+
const results = await projectActions.loadAssets(project)(store.dispatch, store.getState);
131131
const actions = store.getActions();
132132

133133
expect(actions.length).toEqual(1);

src/redux/actions/projectActions.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import { toast } from 'react-toastify';
2828
import { strings, interpolate } from "../../common/strings";
2929
import clone from "rfdc";
3030
import _ from "lodash";
31+
import { decryptProject } from "../../common/utils";
32+
import { StorageProviderFactory } from "../../providers/storage/storageProviderFactory";
33+
import { constants } from "../../common/constants";
3134

3235
/**
3336
* Actions to be performed in relation to projects
@@ -88,9 +91,9 @@ export function loadProject(project: IProject, sharedToken?: ISecurityToken):
8891
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
8992
}
9093
const loadedProject = await projectService.load(project, projectToken);
91-
92-
dispatch(loadProjectAction(loadedProject));
93-
return loadedProject;
94+
const schemaUpdatedProject = await AssetService.checkAndUpdateSchema(loadedProject);
95+
dispatch(loadProjectAction(schemaUpdatedProject));
96+
return schemaUpdatedProject;
9497
};
9598
}
9699

@@ -104,25 +107,22 @@ export function saveProject(project: IProject, saveTags?: boolean, updateTagsFro
104107
project = Object.assign({}, project);
105108
const appState = getState();
106109
const projectService = new ProjectService();
107-
108110
if (projectService.isDuplicate(project, appState.recentProjects)) {
109111
throw new AppError(ErrorCode.ProjectDuplicateName, `Project with name '${project.name}
110112
already exists with the same target connection '${project.sourceConnection.name}'`);
111113
}
112-
113-
const projectToken = appState.appSettings.securityTokens
114-
.find((securityToken) => securityToken.name === project.securityToken);
115-
116-
if (!projectToken) {
117-
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
114+
const findMatchToken = (tokens, project) => {
115+
const tokenFinded = tokens.find((securityToken) => securityToken.name === project.securityToken);
116+
if (!tokenFinded) {
117+
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
118+
}
119+
return tokenFinded;
118120
}
119121

122+
const projectToken = findMatchToken(appState.appSettings.securityTokens, project);
120123
const savedProject = await projectService.save(project, projectToken, saveTags, updateTagsFromFiles);
121124
dispatch(saveProjectAction(savedProject));
122-
123-
// Reload project after save actions
124-
await loadProject(savedProject)(dispatch, getState);
125-
125+
dispatch(loadProjectAction(await decryptProject(savedProject, projectToken)));
126126
return savedProject;
127127
};
128128
}
@@ -215,14 +215,24 @@ export function deleteAsset(project: IProject, assetMetadata: IAssetMetadata): (
215215
* Gets assets from project, dispatches load assets action and returns assets
216216
* @param project - Project from which to load assets
217217
*/
218-
export function loadAssets(project: IProject): (dispatch: Dispatch) => Promise<IAsset[]> {
219-
return async (dispatch: Dispatch) => {
218+
export function loadAssets(project: IProject): (dispatch: Dispatch, getState: () => IApplicationState) => Promise<IAsset[]> {
219+
return async (dispatch: Dispatch, getState: () => IApplicationState) => {
220220
const assetService = new AssetService(project);
221221
const assets = await assetService.getAssets();
222+
let shouldAssetsUpdate = false;
223+
for (const asset of assets) {
224+
if (AssetService.shouldSchemaUpdate(asset.schema)) {
225+
shouldAssetsUpdate = true;
226+
asset.schema = constants.labelsSchema;
227+
}
228+
}
222229
if (!areAssetsEqual(assets, project.assets)) {
223230
dispatch(loadProjectAssetsAction(assets));
224231
}
225-
232+
if (shouldAssetsUpdate) {
233+
const {currentProject} = getState();
234+
await AssetService.checkAndUpdateSchema(currentProject);
235+
}
226236
return assets;
227237
};
228238
}

src/services/assetService.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ export class AssetService {
444444
if (tagType === FieldType.Object || tagType === FieldType.Array) {
445445
labelData.labels = labelData.labels.filter((label) => label.label.split("/")[0].replace(/~1/g, "/").replace(/~0/g, "~") !== tagName);
446446
} else {
447-
if (labelData?.$schema === constants.labelsSchema) {
447+
if (constants.supportedLabelsSchemas.has(labelData?.$schema)) {
448448
labelData.labels = labelData.labels.filter((label) => label.label.replace(/~1/g, "/").replace(/~0/g, "~") !== tagName);
449449
} else {
450450
labelData.labels = labelData.labels.filter((label) => label.label !== tagName);
@@ -739,4 +739,39 @@ export class AssetService {
739739
};
740740
return new Promise(checkSucceeded);
741741
}
742+
743+
/**
744+
* Chech and update schema version through label.json files relate to project assets.
745+
* @param project to get assets and connect to file system.
746+
* @returns updated project
747+
*/
748+
public static checkAndUpdateSchema = async(project: IProject): Promise<IProject> => {
749+
let shouldAssetsUpdate = false;
750+
let updatedProject;
751+
const { assets } = project;
752+
const shouldSchemaUpdate = schema => constants.supportedLabelsSchemas.has(schema) && schema !== constants.labelsSchema;
753+
if (_.isPlainObject(assets)) {
754+
const assetService = new AssetService(project);
755+
const assetMetadatas: IAssetMetadata[] = await Promise.all(Object.values(assets).map(async (asset) => await assetService.getAssetMetadata(asset)));
756+
await Promise.all(assetMetadatas.map(async (assetMetadata) => {
757+
if (_.isPlainObject(assetMetadata.labelData) && AssetService.shouldSchemaUpdate(assetMetadata.labelData?.$schema)) {
758+
shouldAssetsUpdate = true;
759+
assetMetadata.labelData = { ...assetMetadata.labelData, "$schema": constants.labelsSchema };
760+
await assetService.save(assetMetadata);
761+
}
762+
}))
763+
const updatedAssets = { ...assets };
764+
for (const [assetID, asset] of Object.entries(assets)) {
765+
if (shouldSchemaUpdate(asset.schema)) {
766+
updatedAssets[assetID] = { ...assets[assetID], schema: constants.labelsSchema };
767+
}
768+
}
769+
updatedProject = { ...project, assets: updatedAssets };
770+
}
771+
return shouldAssetsUpdate ? updatedProject : project;
772+
}
773+
774+
public static shouldSchemaUpdate = (schema: string): boolean => {
775+
return constants.supportedLabelsSchemas.has(schema) && schema !== constants.labelsSchema;
776+
}
742777
}

src/services/projectService.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,11 @@ export default class ProjectService implements IProjectService {
249249
if (!assetLabel || assetLabel === blob) {
250250
const content = JSON.parse(await storageProvider.readText(blob)) as ILabelData;
251251
content.labels.forEach((label) => {
252-
if (content?.$schema === constants.labelsSchema && label.label.split("/").length > 1) {
252+
if (constants.supportedLabelsSchemas.has(content?.$schema) && label.label.split("/").length > 1) {
253253
return;
254254
}
255255
let labelName;
256-
if (content?.$schema === constants.labelsSchema) {
256+
if (constants.supportedLabelsSchemas.has(content?.$schema)) {
257257
labelName = label.label.replace(/~1/g, "/").replace(/~0/g, "~");
258258
} else {
259259
labelName = label.label
@@ -411,13 +411,13 @@ export default class ProjectService implements IProjectService {
411411
project.tags = [...project.tags, ...missingTags];
412412
}
413413

414-
// private async getAllTagsInProjectCount(project: IProject, tags: ITag[]) {}
414+
// public async getAllTagsInProjectCount(project: IProject, tags: ITag[]) {}
415415
/**
416416
* Save fields.json
417417
* @param project the project we're trying to create
418418
* @param storageProvider the storage we're trying to save the project
419419
*/
420-
private async saveFieldsFile(project: IProject, storageProvider: IStorageProvider) {
420+
public async saveFieldsFile(project: IProject, storageProvider: IStorageProvider) {
421421
Guard.null(project);
422422
Guard.null(project.tags);
423423

0 commit comments

Comments
 (0)