diff --git a/core/gui/angular.json b/core/gui/angular.json index a76ee354362..70475bba4f5 100644 --- a/core/gui/angular.json +++ b/core/gui/angular.json @@ -29,10 +29,15 @@ "node_modules/jointjs/css/themes/default.css", "node_modules/ng-zorro-antd/ng-zorro-antd.min.css", "node_modules/ng-zorro-antd/resizable/style/index.min.css", + "node_modules/bootstrap/dist/css/bootstrap.css", + "node_modules/bootstrap-markdown/css/bootstrap-markdown.min.css", + "node_modules/font-awesome/css/font-awesome.css", "src/styles.scss" ], "scripts": [ - "./node_modules/marked/marked.min.js" + "./node_modules/marked/marked.min.js", + "node_modules/jquery/dist/jquery.js", + "node_modules/bootstrap-markdown/js/bootstrap-markdown.js" ], "customWebpackConfig": { "path": "./custom-webpack.config.js" diff --git a/core/gui/package.json b/core/gui/package.json index e8aefd32f05..c1839bdcb73 100644 --- a/core/gui/package.json +++ b/core/gui/package.json @@ -46,16 +46,21 @@ "@types/lodash-es": "4.17.4", "@types/plotly.js-basic-dist-min": "2.12.4", "ajv": "8.10.0", + "angular-markdown-editor": "^3.1.1", "backbone": "1.4.1", + "bootstrap": "^5.3.7", + "bootstrap-markdown": "^2.10.0", "content-disposition": "0.5.4", "dagre": "0.8.5", "deep-map": "2.0.0", "edit-distance": "1.0.4", "es6-weak-map": "2.0.3", "file-saver": "2.0.5", + "font-awesome": "^4.7.0", "fuse.js": "6.5.3", "html2canvas": "1.4.1", "jointjs": "3.5.4", + "jquery": "^3.7.1", "js-abbreviation-number": "1.4.0", "jszip": "3.10.1", "lodash-es": "4.17.21", @@ -111,11 +116,13 @@ "@nrwl/nx-cloud": "19.1.0", "@nx/angular": "20.0.3", "@types/backbone": "1.4.15", + "@types/bootstrap": "^5", "@types/content-disposition": "0", "@types/dagre": "0.7.47", "@types/file-saver": "2.0.5", "@types/graphlib": "2.1.8", "@types/jasmine": "4.6.4", + "@types/jquery": "^3", "@types/json-schema": "7.0.9", "@types/lodash": "4.14.179", "@types/node": "18.15.5", diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts index 13759e6e55a..e61ea791cde 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -172,6 +172,8 @@ import { AdminSettingsComponent } from "./dashboard/component/admin/settings/adm import { catchError, of } from "rxjs"; import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component"; import { NzInputNumberModule } from "ng-zorro-antd/input-number"; +import { UserDatasetFileEditorComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component"; +import { AngularMarkdownEditorModule } from "angular-markdown-editor"; registerLocaleData(en); @@ -264,6 +266,7 @@ registerLocaleData(en); HubSearchResultComponent, ComputingUnitSelectionComponent, AdminSettingsComponent, + UserDatasetFileEditorComponent, ], imports: [ BrowserModule, @@ -330,6 +333,7 @@ registerLocaleData(en); NzDividerModule, NzProgressModule, NzInputNumberModule, + AngularMarkdownEditorModule.forRoot(), ], providers: [ provideNzI18n(en_US), diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html index d4dddf94f6d..d30dcab51fc 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.html @@ -113,6 +113,18 @@

+
+ + + + + Choose a Version: + + +
+ +
+ { + if (this.versions.length > 0) { + // Select the latest version (newly created) + this.selectedVersion = this.versions[0]; + + // Refresh the file tree for the new version + if (this.did && this.selectedVersion.dvid) { + this.datasetService + .retrieveDatasetVersionFileTree(this.did, this.selectedVersion.dvid, this.isLogin) + .pipe(untilDestroyed(this)) + .subscribe(data => { + this.fileTreeNodeList = data.fileNodes; + this.currentDatasetVersionSize = data.size; + + // Try to find and re-select the same file we were editing + const fileNode = this.findFileInTree(currentFileName); + if (fileNode) { + this.loadFileContent(fileNode); + } else { + // Fallback to first file if our file isn't found + let currentNode = this.fileTreeNodeList[0]; + while (currentNode && currentNode.type === "directory" && currentNode.children) { + currentNode = currentNode.children[0]; + } + if (currentNode) { + this.loadFileContent(currentNode); + } + } + }); + } + } + }, 500); // Small delay to ensure backend has processed the new version + + this.exitEditMode(); + } + + private findFileInTree(fileName: string, nodes: DatasetFileNode[] = this.fileTreeNodeList): DatasetFileNode | null { + for (const node of nodes) { + if (node.name === fileName && node.type === "file") { + return node; + } + if (node.children) { + const found = this.findFileInTree(fileName, node.children); + if (found) { + return found; + } + } + } + return null; + } + + public onClickCreateReadme(): void { + this.modalService.confirm({ + nzTitle: "Create README.md", + nzContent: "Are you sure you want to create a README.md file for this dataset?", + nzOkText: "Yes, Create", + nzCancelText: "Cancel", + nzOnOk: () => { + this.createReadmeFile(); + }, + }); + } + + private createReadmeFile(): void { + if (!this.did) return; + + this.isCreatingReadme = true; + const defaultReadmeContent = "# Dataset README\n\nDescribe your dataset here..."; + + this.datasetService + .getDataset(this.did, this.isLogin) + .pipe( + switchMap(dashboardDataset => { + const datasetName = dashboardDataset.dataset.name; + const readmeBlob = new Blob([defaultReadmeContent], { type: "text/markdown" }); + const readmeFile = new File([readmeBlob], "README.md", { type: "text/markdown" }); + return this.datasetService.multipartUpload( + datasetName, + "README.md", + readmeFile, + this.chunkSizeMB * 1024 * 1024, + this.maxConcurrentChunks + ); + }), + switchMap(progress => { + if (progress.status === "finished") { + return this.datasetService.createDatasetVersion(this.did!, "Created README.md"); + } + return of(progress); + }), + untilDestroyed(this) + ) + .subscribe({ + next: result => { + if (result && typeof result === "object" && "dvid" in result) { + this.isCreatingReadme = false; + this.notificationService.success("README created successfully!"); + + this.currentDisplayedFileName = "README.md"; + this.onFileChanged(); + + setTimeout(() => { + this.isEditMode = true; + }, 1000); + } + }, + error: (error: unknown) => { + this.isCreatingReadme = false; + console.error("Error creating README:", error); + this.notificationService.error("Failed to create README"); + }, + }); + } + + public hasReadmeFile(): boolean { + return this.findFileInTree("README.md") !== null; + } + + public isEditableFile(fileName: string): boolean { + const extension = fileName.toLowerCase().split(".").pop(); + const editableExtensions = ["md", "markdown", "txt", "log", "yml", "yaml"]; + return editableExtensions.includes(extension || ""); + } + + public onClickEditFile(): void { + if (!this.selectedVersion || !this.currentDisplayedFileName) return; + + this.isEditMode = !this.isEditMode; + } + + public exitEditMode(): void { + this.isEditMode = false; + } + onPublicStatusChange(checked: boolean): void { // Handle the change in dataset public status if (this.did) { @@ -341,6 +488,8 @@ export class DatasetDetailComponent implements OnInit { } onVersionSelected(version: DatasetVersion): void { + this.exitEditMode(); + this.selectedVersion = version; if (this.did && this.selectedVersion.dvid) this.datasetService @@ -362,6 +511,7 @@ export class DatasetDetailComponent implements OnInit { } onVersionFileTreeNodeSelected(node: DatasetFileNode) { + this.exitEditMode(); this.loadFileContent(node); } diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.html b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.html new file mode 100644 index 00000000000..09d9d8cf0df --- /dev/null +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.html @@ -0,0 +1,120 @@ + + + +
+ + + +
+ + +
+
+
+
+

+ + {{ getFileName() }} +

+
+ +
+ + + + Unsaved changes + + + + + + + +
+
+ + +
+ +
+ + +
+ + +
+ +
+
+
+
diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.scss b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.scss new file mode 100644 index 00000000000..b8da4a680ea --- /dev/null +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.scss @@ -0,0 +1,99 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.file-loading { + padding: 16px; + text-align: center; +} + +.file-content { + /* Container for file editing interface */ +} + +.file-expanded { + border: 1px solid #e8e8e8; + border-radius: 6px; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.file-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + background: #fafafa; + border-radius: 6px 6px 0 0; +} + +.file-title { + margin: 0; + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #262626; +} + +.file-title i { + color: #1890ff; +} + +.file-controls { + display: flex; + gap: 8px; + align-items: center; +} + +.unsaved-indicator { + color: #fa8c16; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; +} + +.file-editor-container { + /* Container for different editor types */ +} + +.markdown-editor-wrapper { + /* Wrapper for angular-markdown-editor */ +} + +.simple-editor .file-textarea.text-editor { + width: 100%; + min-height: 300px; + padding: 16px; + border: 1px solid #d9d9d9; + border-radius: 0 0 6px 6px; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; + font-size: 14px; + line-height: 1.6; + resize: vertical; + background: white; +} + +.simple-editor .file-textarea.text-editor:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; +} diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts new file mode 100644 index 00000000000..6693998e938 --- /dev/null +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts @@ -0,0 +1,306 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, + ViewEncapsulation, +} from "@angular/core"; +import { DatasetService } from "../../../../../service/user/dataset/dataset.service"; +import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { NotificationService } from "../../../../../../common/service/notification/notification.service"; +import { switchMap } from "rxjs/operators"; +import { of } from "rxjs"; +import { EditorInstance, EditorOption } from "angular-markdown-editor"; +import { MarkdownService } from "ngx-markdown"; + +@UntilDestroy() +@Component({ + selector: "texera-user-dataset-file-editor", + templateUrl: "./user-dataset-file-editor.component.html", + styleUrls: ["./user-dataset-file-editor.component.scss"], + encapsulation: ViewEncapsulation.None, +}) +export class UserDatasetFileEditorComponent implements OnInit, OnChanges { + @Input() did: number | undefined; + @Input() dvid: number | undefined; + @Input() selectedVersion: any | undefined; + @Input() datasetName: string = ""; + @Input() filePath: string = ""; + @Input() isMaximized: boolean = false; + @Input() userHasWriteAccess: boolean = false; + @Input() isLogin: boolean = true; + @Input() chunkSizeMB!: number; + @Input() maxConcurrentChunks!: number; + @Input() isEditMode: boolean = true; + @Output() userMakeChanges = new EventEmitter(); + @Output() editCanceled = new EventEmitter(); + + @ViewChild("fileTextarea") fileTextarea!: ElementRef; + + public fileContent: string = ""; + public fileExists: boolean = false; + public isLoading: boolean = false; + public editingContent: string = ""; + public fileType: "markdown" | "text" | "unsupported" = "unsupported"; + public showFileContent: boolean = false; + + // Angular Markdown Editor properties + public bsEditorInstance!: EditorInstance; + public editorOptions!: EditorOption; + + constructor( + private datasetService: DatasetService, + private notificationService: NotificationService, + private markdownService: MarkdownService + ) {} + + ngOnInit(): void { + this.initializeEditorOptions(); + + if (this.dvid && this.datasetName && this.selectedVersion && this.filePath) { + this.determineFileType(); + this.loadFile(); + this.isEditMode = true; + } + } + + ngOnChanges(changes: SimpleChanges): void { + if ( + (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"] || changes["filePath"]) && + this.dvid && + this.datasetName && + this.selectedVersion && + this.filePath + ) { + this.isEditMode = false; + this.showFileContent = false; + this.isLoading = false; + this.fileExists = false; + this.fileContent = ""; + this.editingContent = ""; + + this.determineFileType(); + this.loadFile(); + } + } + + private initializeEditorOptions(): void { + this.editorOptions = { + autofocus: false, + iconlibrary: "fa", + savable: false, + onShow: (e: EditorInstance) => { + this.bsEditorInstance = e; + console.log("Markdown editor initialized"); + }, + onChange: (e: EditorInstance) => { + this.editingContent = e.getContent(); + }, + parser: (val: string) => this.parseMarkdown(val), + }; + } + + private determineFileType(): void { + const extension = this.filePath.toLowerCase().split(".").pop(); + switch (extension) { + case "md": + case "markdown": + this.fileType = "markdown"; + break; + case "txt": + case "log": + case "yml": + case "yaml": + this.fileType = "text"; + break; + default: + this.fileType = "unsupported"; + } + } + + private loadFile(): void { + if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion || !this.filePath) return; + + this.isLoading = true; + + this.datasetService + .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin) + .pipe( + switchMap(blob => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsText(blob); + }); + }), + untilDestroyed(this) + ) + .subscribe({ + next: content => { + this.isLoading = false; + this.fileExists = true; + this.fileContent = content; + this.editingContent = content; + }, + error: () => { + this.isLoading = false; + this.fileExists = false; + this.fileContent = ""; + this.editingContent = ""; + console.log("File not found or error loading"); + }, + }); + } + + public cancelEditing(): void { + this.editingContent = this.fileContent; + this.isEditMode = false; + this.editCanceled.emit(); + } + + public onMarkdownEditorChange(event: any): void { + if (event && event.detail && event.detail.eventData) { + this.editingContent = event.detail.eventData.getContent(); + } else { + // Handle direct content change + this.editingContent = event; + } + } + + public onEditorKeydown(event: KeyboardEvent): void { + if ((event.ctrlKey || event.metaKey) && event.key === "s") { + event.preventDefault(); + this.saveFile(); + } + + if (event.key === "Tab") { + event.preventDefault(); + const textarea = event.target as HTMLTextAreaElement; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + + const value = textarea.value; + textarea.value = value.substring(0, start) + " " + value.substring(end); + + textarea.selectionStart = textarea.selectionEnd = start + 2; + + this.editingContent = textarea.value; + } + } + + public saveFile(): void { + if (!this.did || !this.userHasWriteAccess) return; + + if (this.editingContent === this.fileContent) { + this.notificationService.warning("No changes detected in file content"); + return; + } + + this.uploadFileContent(this.editingContent, `${this.getFileName()} updated successfully`); + } + + private uploadFileContent(content: string, successMessage: string): void { + if (!this.did) return; + + this.datasetService + .getDataset(this.did, this.isLogin) + .pipe( + switchMap(dashboardDataset => { + const datasetName = dashboardDataset.dataset.name; + const fileName = this.getFileName(); + + const mimeType = this.getMimeType(); + const fileBlob = new Blob([content], { type: mimeType }); + const file = new File([fileBlob], fileName, { type: mimeType }); + + return this.datasetService.multipartUpload(datasetName, fileName, file, 50 * 1024 * 1024, 10); + }), + switchMap(progress => { + if (progress.status === "finished") { + const fileName = this.getFileName(); + const versionMessage = successMessage.includes("created") ? `Created ${fileName}` : `Updated ${fileName}`; + + return this.datasetService.createDatasetVersion(this.did!, versionMessage); + } + return of(progress); + }), + untilDestroyed(this) + ) + .subscribe({ + next: result => { + if (result && typeof result === "object" && "dvid" in result) { + this.fileExists = true; + this.fileContent = content; + this.isEditMode = false; + this.notificationService.success(successMessage); + this.userMakeChanges.emit(); + } + }, + error: (error: unknown) => { + console.error("Error uploading file:", error); + this.notificationService.error(`Failed to save ${this.getFileName()}`); + }, + }); + } + + private getMimeType(): string { + switch (this.fileType) { + case "markdown": + return "text/markdown"; + case "text": + return "text/plain"; + default: + return "text/plain"; + } + } + + public getFileName(): string { + if (!this.filePath) return ""; + return this.filePath.split("/").pop() || this.filePath; + } + + public isEditable(): boolean { + return this.fileType === "markdown" || this.fileType === "text"; + } + + public hasUnsavedChanges(): boolean { + return this.editingContent !== this.fileContent; + } + + private parseMarkdown(inputValue: string): string { + const markedOutput = this.markdownService.parse(inputValue.trim()); + this.highlightCode(); + return markedOutput; + } + + private highlightCode(): void { + setTimeout(() => { + this.markdownService.highlight(); + }); + } +} diff --git a/core/gui/yarn.lock b/core/gui/yarn.lock index 43ae178efaa..c96f33063e5 100644 --- a/core/gui/yarn.lock +++ b/core/gui/yarn.lock @@ -4775,6 +4775,13 @@ __metadata: languageName: node linkType: hard +"@popperjs/core@npm:^2.9.2": + version: 2.11.8 + resolution: "@popperjs/core@npm:2.11.8" + checksum: 10c0/4681e682abc006d25eb380d0cf3efc7557043f53b6aea7a5057d0d1e7df849a00e281cd8ea79c902a35a414d7919621fc2ba293ecec05f413598e0b23d5a1e63 + languageName: node + linkType: hard + "@prettier/eslint@npm:prettier-eslint@^16.1.0": version: 16.3.0 resolution: "prettier-eslint@npm:16.3.0" @@ -5076,6 +5083,15 @@ __metadata: languageName: node linkType: hard +"@types/bootstrap@npm:^5": + version: 5.2.10 + resolution: "@types/bootstrap@npm:5.2.10" + dependencies: + "@popperjs/core": "npm:^2.9.2" + checksum: 10c0/3e978855eb780df3907e8fe991371dc661c7a8c5b9852a10e33bcf6a909bc1481857aa8786d18b3aa828fb28660145fda0c8648265719e8a97a448b9f0158eae + languageName: node + linkType: hard + "@types/connect-history-api-fallback@npm:^1.3.5, @types/connect-history-api-fallback@npm:^1.5.4": version: 1.5.4 resolution: "@types/connect-history-api-fallback@npm:1.5.4" @@ -5271,6 +5287,15 @@ __metadata: languageName: node linkType: hard +"@types/jquery@npm:^3": + version: 3.5.33 + resolution: "@types/jquery@npm:3.5.33" + dependencies: + "@types/sizzle": "npm:*" + checksum: 10c0/d96c42762b7370ddf3b81cdad436a79d275e0ff09e2f4d7fbf2bbd8f97acef4110a11f1c3cb683195a1eba4fd9959e59d73f84d56ce2c010907a2bea696eb057 + languageName: node + linkType: hard + "@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -6407,6 +6432,19 @@ __metadata: languageName: node linkType: hard +"angular-markdown-editor@npm:^3.1.1": + version: 3.1.1 + resolution: "angular-markdown-editor@npm:3.1.1" + dependencies: + bootstrap: "npm:>=4.6.2" + bootstrap-markdown: "github:refactory-id/bootstrap-markdown" + font-awesome: "npm:^4.7.0" + jquery: "npm:^3.7.0" + tslib: "npm:^2.3.0" + checksum: 10c0/9109609e447fb9c91132d56d6893d08b779ff639fbe2c10def4cca1d035f34544fd8353266e7363c69cf05bc024fe7c18db8d6e6f328fc21255ca2e35926bc09 + languageName: node + linkType: hard + "ansi-colors@npm:4.1.3, ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -7084,6 +7122,29 @@ __metadata: languageName: node linkType: hard +"bootstrap-markdown@github:refactory-id/bootstrap-markdown": + version: 2.10.0 + resolution: "bootstrap-markdown@https://github.com/refactory-id/bootstrap-markdown.git#commit=a496d34b9bd34451c8315a850472f794c8df7d53" + checksum: 10c0/f314c390ad9c6317eaeeceaca38de363c2623bbc94167f72264a115f267bd37c838d75612e767173c7d528e2372a884bd8fbdb8e915bff84e79807adb16108ae + languageName: node + linkType: hard + +"bootstrap-markdown@npm:^2.10.0": + version: 2.10.0 + resolution: "bootstrap-markdown@npm:2.10.0" + checksum: 10c0/903c7cc043e58c742f08bd7e226afbfafba324036c237496387ea4267725c957453dbb5809dafdfced2c12f1b3d5759dd083f096ba69447e217a91a6922ce162 + languageName: node + linkType: hard + +"bootstrap@npm:>=4.6.2, bootstrap@npm:^5.3.7": + version: 5.3.7 + resolution: "bootstrap@npm:5.3.7" + peerDependencies: + "@popperjs/core": ^2.11.8 + checksum: 10c0/019f0d683aec843b9fc0592ae78560cfe286bc8e31e706d40d8c15d390dcca7ab2ffa193a489a74f65ed5596800b9b79da867545ce3bbafca945b630fe0055af + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -10623,6 +10684,13 @@ __metadata: languageName: node linkType: hard +"font-awesome@npm:^4.7.0": + version: 4.7.0 + resolution: "font-awesome@npm:4.7.0" + checksum: 10c0/1c456e2939c55192eed67db9c0efb8db3e92fd357ca189ca00030eb44acffa1e9f835288d2204c14b9a9c490a7b14b7090dfaff80ded6b2473f50a923dfb41e7 + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -11287,11 +11355,13 @@ __metadata: "@nx/angular": "npm:20.0.3" "@stoplight/json-ref-resolver": "npm:3.1.5" "@types/backbone": "npm:1.4.15" + "@types/bootstrap": "npm:^5" "@types/content-disposition": "npm:0" "@types/dagre": "npm:0.7.47" "@types/file-saver": "npm:2.0.5" "@types/graphlib": "npm:2.1.8" "@types/jasmine": "npm:4.6.4" + "@types/jquery": "npm:^3" "@types/json-schema": "npm:7.0.9" "@types/lodash": "npm:4.14.179" "@types/lodash-es": "npm:4.17.4" @@ -11304,8 +11374,11 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:7.0.2" "@typescript-eslint/parser": "npm:7.0.2" ajv: "npm:8.10.0" + angular-markdown-editor: "npm:^3.1.1" babel-plugin-dynamic-import-node: "npm:2.3.3" backbone: "npm:1.4.1" + bootstrap: "npm:^5.3.7" + bootstrap-markdown: "npm:^2.10.0" concurrently: "npm:7.4.0" content-disposition: "npm:0.5.4" dagre: "npm:0.8.5" @@ -11320,6 +11393,7 @@ __metadata: eslint-plugin-rxjs: "npm:5.0.3" eslint-plugin-rxjs-angular: "npm:2.0.1" file-saver: "npm:2.0.5" + font-awesome: "npm:^4.7.0" fs-extra: "npm:10.0.1" fuse.js: "npm:6.5.3" git-describe: "npm:4.1.0" @@ -11327,6 +11401,7 @@ __metadata: jasmine-core: "npm:5.4.0" jasmine-spec-reporter: "npm:7.0.0" jointjs: "npm:3.5.4" + jquery: "npm:^3.7.1" js-abbreviation-number: "npm:1.4.0" jszip: "npm:3.10.1" karma: "npm:6.4.4" @@ -12580,6 +12655,13 @@ __metadata: languageName: node linkType: hard +"jquery@npm:^3.7.0, jquery@npm:^3.7.1": + version: 3.7.1 + resolution: "jquery@npm:3.7.1" + checksum: 10c0/808cfbfb758438560224bf26e17fcd5afc7419170230c810dd11f5c1792e2263e2970cca8d659eb84fcd9acc301edb6d310096e450277d54be4f57071b0c82d9 + languageName: node + linkType: hard + "jquery@npm:~3.6.0": version: 3.6.4 resolution: "jquery@npm:3.6.4"