From ff28d58ded9182616a8244f21b27844f69a09c68 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Sat, 5 Jul 2025 14:19:42 -0700 Subject: [PATCH 01/12] implemented dataset readme frontend --- core/gui/src/app/app.module.ts | 2 + .../dataset-detail.component.html | 10 ++ .../user-dataset-readme.component.html | 116 ++++++++++++++ .../user-dataset-readme.component.scss | 145 ++++++++++++++++++ .../user-dataset-readme.component.spec.ts | 66 ++++++++ .../user-dataset-readme.component.ts | 124 +++++++++++++++ 6 files changed, 463 insertions(+) create mode 100644 core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html create mode 100644 core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss create mode 100644 core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts create mode 100644 core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts index 13759e6e55a..c2f78c384bf 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -172,6 +172,7 @@ 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 { UserDatasetReadmeComponent } from './dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component'; registerLocaleData(en); @@ -264,6 +265,7 @@ registerLocaleData(en); HubSearchResultComponent, ComputingUnitSelectionComponent, AdminSettingsComponent, + UserDatasetReadmeComponent, ], imports: [ BrowserModule, 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 df8e1b88f82..e304d6b2a81 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 @@ -147,6 +147,16 @@

+ + + + + + + + + + +

No README found

+

This dataset doesn't have a README yet.

+ +
+
+
+ + + +
+

+ + README.md +

+ +
+ + + + + + + + +
+
+ + +
+ +
+ + +
+ +
+ + + You can use Markdown syntax. Changes will create a new dataset version. + +
+
+
diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss new file mode 100644 index 00000000000..1ee445dc96d --- /dev/null +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss @@ -0,0 +1,145 @@ +/** + * 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. + */ + +.readme-empty-card, +.readme-content-card { + margin-bottom: 16px; +} + +.readme-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid #f0f0f0; + + .readme-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: #1f2937; + display: flex; + align-items: center; + gap: 8px; + + i { + color: #1890ff; + } + } + + .readme-controls { + display: flex; + gap: 4px; + } +} + +.readme-view { + ::ng-deep { + h1, h2, h3, h4, h5, h6 { + color: #1f2937; + margin-top: 16px; + margin-bottom: 8px; + font-weight: 600; + line-height: 1.25; + + &:first-child { + margin-top: 0; + } + } + + h1 { font-size: 1.5em; } + h2 { font-size: 1.3em; } + h3 { font-size: 1.1em; } + + p { + margin-bottom: 12px; + line-height: 1.6; + color: #374151; + } + + code { + background: #f3f4f6; + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; + } + + pre { + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 12px; + overflow-x: auto; + margin: 12px 0; + + code { + background: none; + padding: 0; + } + } + + ul, ol { + padding-left: 20px; + margin-bottom: 12px; + + li { + margin-bottom: 4px; + } + } + + blockquote { + border-left: 4px solid #e5e7eb; + padding-left: 12px; + margin: 12px 0; + color: #6b7280; + font-style: italic; + } + } +} + +.readme-editor { + .markdown-textarea { + width: 100%; + min-height: 200px; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-family: monospace; + font-size: 14px; + line-height: 1.5; + resize: vertical; + outline: none; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + } + + .editor-help { + margin-top: 8px; + color: #666; + + i { + margin-right: 4px; + } + } +} diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts new file mode 100644 index 00000000000..c67d336681b --- /dev/null +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts @@ -0,0 +1,66 @@ +/** + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UserDatasetReadmeComponent } from './user-dataset-readme.component'; +import { DatasetService } from '../../../../../service/user/dataset/dataset.service'; +import { NotificationService } from '../../../../../../common/service/notification/notification.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { MarkdownModule } from 'ngx-markdown'; +import { FormsModule } from '@angular/forms'; +import { NzEmptyModule } from 'ng-zorro-antd/empty'; +import { NzSpinModule } from 'ng-zorro-antd/spin'; +import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzAlertModule } from 'ng-zorro-antd/alert'; +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { commonTestProviders } from '../../../../../../common/testing/test-utils'; + +describe('UserDatasetReadmeComponent', () => { + let component: UserDatasetReadmeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserDatasetReadmeComponent], + imports: [ + HttpClientTestingModule, + MarkdownModule.forRoot(), + FormsModule, + NzEmptyModule, + NzSpinModule, + NzButtonModule, + NzAlertModule, + NzIconModule, + ], + providers: [ + DatasetService, + NotificationService, + ...commonTestProviders, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(UserDatasetReadmeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts new file mode 100644 index 00000000000..dd9654bde6e --- /dev/null +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts @@ -0,0 +1,124 @@ +/** + * 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, Input, Output, EventEmitter, OnInit } 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'; + +@UntilDestroy() +@Component({ + selector: 'texera-user-dataset-readme', + templateUrl: './user-dataset-readme.component.html', + styleUrls: ['./user-dataset-readme.component.scss'] +}) +export class UserDatasetReadmeComponent implements OnInit { + @Input() did: number | undefined; + @Input() isMaximized: boolean = false; + @Input() userHasWriteAccess: boolean = false; + @Output() userMakeChanges = new EventEmitter(); + + public readmeContent: string = ''; + public isEditing: boolean = false; + public readmeExists: boolean = false; + public isLoading: boolean = false; + public editingContent: string = ''; + + // CodeMirror options + public editorOptions = { + theme: 'default', + mode: 'markdown', + lineNumbers: true, + lineWrapping: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + autoCloseBrackets: true, + matchBrackets: true, + indentWithTabs: false, + indentUnit: 2, + tabSize: 2 + }; + + constructor( + private datasetService: DatasetService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.loadReadme(); + } + + private loadReadme(): void { + if (!this.did) return; + + this.isLoading = true; + // For now, simulate loading + setTimeout(() => { + this.readmeExists = false; + this.readmeContent = ''; + this.editingContent = this.readmeContent; + this.isLoading = false; + }, 1000); + } + + public createReadme(): void { + if (!this.did || !this.userHasWriteAccess) return; + + // Simulate creating README + this.readmeExists = true; + this.readmeContent = '# Dataset README\n\nDescribe your dataset here...'; + this.editingContent = this.readmeContent; + this.isEditing = true; + this.notificationService.success('README created successfully'); + this.userMakeChanges.emit(); + } + + public startEditing(): void { + if (!this.userHasWriteAccess) return; + this.editingContent = this.readmeContent; + this.isEditing = true; + } + + public cancelEditing(): void { + this.editingContent = this.readmeContent; + this.isEditing = false; + } + + public saveReadme(): void { + if (!this.did || !this.userHasWriteAccess) return; + + // Simulate saving + this.readmeContent = this.editingContent; + this.isEditing = false; + this.notificationService.success('README updated successfully'); + this.userMakeChanges.emit(); + } + + public deleteReadme(): void { + if (!this.did || !this.userHasWriteAccess) return; + + // Simulate deletion + this.readmeExists = false; + this.readmeContent = ''; + this.editingContent = ''; + this.isEditing = false; + this.notificationService.success('README deleted successfully'); + this.userMakeChanges.emit(); + } +} From 0576f672829fe22c8be3e8fe25511e99247f3c95 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Sat, 5 Jul 2025 16:08:11 -0700 Subject: [PATCH 02/12] implemented readme display, upload, and deletion. --- .../dataset-detail.component.html | 22 +- .../dataset-detail.component.ts | 17 ++ .../user-dataset-readme.component.html | 50 ++++- .../user-dataset-readme.component.scss | 145 +++++++++++-- .../user-dataset-readme.component.ts | 196 +++++++++++++++--- 5 files changed, 367 insertions(+), 63 deletions(-) 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 e304d6b2a81..69521cb8f57 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 @@ -149,13 +149,21 @@

- - + + + + + { + // Re-select the latest version (which should be the new one we just created) + if (this.versions.length > 0) { + this.selectedVersion = this.versions[0]; + this.onVersionSelected(this.selectedVersion); + } + }, 0); + } + } + onPublicStatusChange(checked: boolean): void { // Handle the change in dataset public status if (this.did) { diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html index 6cb245a4da9..998b72f3100 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html @@ -17,6 +17,15 @@ under the License. --> + + + + + + + - +
- -
- + +
+
+ +
+
+ + Editor +
+ + +
+ + +
+ + +
+
+ + Preview +
+
+ +
+
+
+
diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss index 1ee445dc96d..c0ed98c5689 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss @@ -18,7 +18,8 @@ */ .readme-empty-card, -.readme-content-card { +.readme-content-card, +.readme-loading-card { margin-bottom: 16px; } @@ -115,22 +116,136 @@ } } -.readme-editor { - .markdown-textarea { - width: 100%; - min-height: 200px; - padding: 12px; +.readme-editor-container { + .editor-split-view { + display: flex; + height: 500px; border: 1px solid #d9d9d9; border-radius: 4px; - font-family: monospace; - font-size: 14px; - line-height: 1.5; - resize: vertical; - outline: none; - - &:focus { - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + overflow: hidden; + + .editor-pane, + .preview-pane { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + } + + .pane-title { + margin: 0; + padding: 8px 12px; + background: #fafafa; + border-bottom: 1px solid #d9d9d9; + font-size: 14px; + font-weight: 500; + color: #595959; + display: flex; + align-items: center; + gap: 6px; + + i { + font-size: 12px; + } + } + + .editor-divider { + width: 1px; + background: #d9d9d9; + flex-shrink: 0; + } + } + + .markdown-editor { + flex: 1; + + ::ng-deep { + .CodeMirror { + height: 100%; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.5; + border: none; + } + + .CodeMirror-focused .CodeMirror-cursor { + border-left: 1px solid #1890ff; + } + + .CodeMirror-selected { + background: rgba(24, 144, 255, 0.1); + } + } + } + + .preview-content { + flex: 1; + padding: 12px; + overflow-y: auto; + background: #fff; + + // Apply same styles as readonly view + ::ng-deep { + h1, h2, h3, h4, h5, h6 { + color: #1f2937; + margin-top: 16px; + margin-bottom: 8px; + font-weight: 600; + line-height: 1.25; + + &:first-child { + margin-top: 0; + } + } + + h1 { font-size: 1.5em; } + h2 { font-size: 1.3em; } + h3 { font-size: 1.1em; } + + p { + margin-bottom: 12px; + line-height: 1.6; + color: #374151; + } + + code { + background: #f3f4f6; + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; + } + + pre { + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 12px; + overflow-x: auto; + margin: 12px 0; + + code { + background: none; + padding: 0; + } + } + + ul, ol { + padding-left: 20px; + margin-bottom: 12px; + + li { + margin-bottom: 4px; + } + } + + blockquote { + border-left: 4px solid #e5e7eb; + padding-left: 12px; + margin: 12px 0; + color: #6b7280; + font-style: italic; + } } } diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts index dd9654bde6e..63f6f984b6f 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts @@ -17,10 +17,13 @@ * under the License. */ -import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } 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, catchError } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { DatasetFileNode } from '../../../../../../common/type/datasetVersionFileTree'; @UntilDestroy() @Component({ @@ -28,10 +31,14 @@ import { NotificationService } from '../../../../../../common/service/notificati templateUrl: './user-dataset-readme.component.html', styleUrls: ['./user-dataset-readme.component.scss'] }) -export class UserDatasetReadmeComponent implements OnInit { +export class UserDatasetReadmeComponent implements OnInit, OnChanges { @Input() did: number | undefined; + @Input() dvid: number | undefined; + @Input() selectedVersion: any | undefined; + @Input() datasetName: string = ''; @Input() isMaximized: boolean = false; @Input() userHasWriteAccess: boolean = false; + @Input() isLogin: boolean = true; @Output() userMakeChanges = new EventEmitter(); public readmeContent: string = ''; @@ -40,7 +47,9 @@ export class UserDatasetReadmeComponent implements OnInit { public isLoading: boolean = false; public editingContent: string = ''; - // CodeMirror options + private readonly README_FILE_PATH = 'README.md'; + + // CodeMirror options for markdown editing public editorOptions = { theme: 'default', mode: 'markdown', @@ -57,36 +66,102 @@ export class UserDatasetReadmeComponent implements OnInit { constructor( private datasetService: DatasetService, - private notificationService: NotificationService + private notificationService: NotificationService, ) {} ngOnInit(): void { - this.loadReadme(); + if (this.dvid && this.datasetName && this.selectedVersion) { + this.loadReadme(); + } + } + + ngOnChanges(changes: SimpleChanges): void { + // Reload README when dataset version changes + if ((changes['dvid'] || changes['selectedVersion']) && + this.dvid && this.selectedVersion) { + this.loadReadme(); + } } private loadReadme(): void { - if (!this.did) return; + if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion) return; this.isLoading = true; - // For now, simulate loading - setTimeout(() => { - this.readmeExists = false; - this.readmeContent = ''; - this.editingContent = this.readmeContent; - this.isLoading = false; - }, 1000); + + this.datasetService + .retrieveDatasetVersionFileTree(this.did, this.dvid, this.isLogin) + .pipe( + switchMap(({ fileNodes }) => { + // Check if README.md exists in the file tree + const readmeExists = this.findReadmeInFileTree(fileNodes); + + if (readmeExists) { + const fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.README_FILE_PATH}`; + + return this.datasetService + .retrieveDatasetVersionSingleFile(fullPath, 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); + }); + }) + ); + } else { + // README doesn't exist + return of(''); + } + }), + catchError(error => { + console.log('README not found or error loading:', error); + return of(''); + }), + untilDestroyed(this) + ) + .subscribe({ + next: content => { + this.isLoading = false; + if (content) { + this.readmeExists = true; + this.readmeContent = content; + this.editingContent = content; + } else { + this.readmeExists = false; + this.readmeContent = ''; + this.editingContent = ''; + } + }, + error: () => { + this.isLoading = false; + this.readmeExists = false; + this.readmeContent = ''; + this.editingContent = ''; + } + }); + } + + private findReadmeInFileTree(fileNodes: DatasetFileNode[]): boolean { + for (const node of fileNodes) { + if (node.type === 'file' && node.name === 'README.md') { + return true; + } + if (node.type === 'directory' && node.children) { + if (this.findReadmeInFileTree(node.children)) { + return true; + } + } + } + return false; } public createReadme(): void { if (!this.did || !this.userHasWriteAccess) return; - // Simulate creating README - this.readmeExists = true; - this.readmeContent = '# Dataset README\n\nDescribe your dataset here...'; - this.editingContent = this.readmeContent; - this.isEditing = true; - this.notificationService.success('README created successfully'); - this.userMakeChanges.emit(); + const initialContent = '# Dataset README\n\nDescribe your dataset here...'; + this.uploadReadmeContent(initialContent, 'README created successfully'); } public startEditing(): void { @@ -103,22 +178,79 @@ export class UserDatasetReadmeComponent implements OnInit { public saveReadme(): void { if (!this.did || !this.userHasWriteAccess) return; - // Simulate saving - this.readmeContent = this.editingContent; - this.isEditing = false; - this.notificationService.success('README updated successfully'); - this.userMakeChanges.emit(); + this.uploadReadmeContent(this.editingContent, 'README updated successfully'); } public deleteReadme(): void { if (!this.did || !this.userHasWriteAccess) return; - // Simulate deletion - this.readmeExists = false; - this.readmeContent = ''; - this.editingContent = ''; - this.isEditing = false; - this.notificationService.success('README deleted successfully'); - this.userMakeChanges.emit(); + this.datasetService + .deleteDatasetFile(this.did, this.README_FILE_PATH) + .pipe( + // After deleting, create a new version to save changes. + switchMap(() => + this.datasetService.createDatasetVersion(this.did!, 'Deleted README.md') + ), + untilDestroyed(this) + ) + .subscribe({ + next: (newVersion) => { + this.readmeExists = false; + this.readmeContent = ''; + this.editingContent = ''; + this.isEditing = false; + this.notificationService.success('README deleted successfully'); + + // Emit the change to refresh file version screen + this.userMakeChanges.emit(); + }, + error: error => { + console.error('Error deleting README:', error); + this.notificationService.error('Failed to delete README'); + } + }); + } + + private uploadReadmeContent(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 readmeBlob = new Blob([content], { type: 'text/markdown' }); + const readmeFile = new File([readmeBlob], this.README_FILE_PATH, { type: 'text/markdown' }); + + return this.datasetService.multipartUpload(datasetName, this.README_FILE_PATH, readmeFile); + }), + // After upload completes, automatically create a new version + switchMap(progress => { + if (progress.status === 'finished') { + const versionMessage = successMessage.includes('created') ? 'Created README.md' : 'Updated README.md'; + return this.datasetService.createDatasetVersion(this.did!, versionMessage); + } + return of(progress); + }), + untilDestroyed(this) + ) + .subscribe({ + next: result => { + if (result && typeof result === 'object' && 'dvid' in result) { + this.readmeExists = true; + this.readmeContent = content; + this.isEditing = false; + this.notificationService.success(successMessage); + + // Emit the change to refresh file version screen + this.userMakeChanges.emit(); + } + }, + error: error => { + console.error('Error uploading README:', error); + this.notificationService.error('Failed to save README'); + } + }); } } From 6f850b06feb08d2a11c891156eb674164841e748 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Wed, 9 Jul 2025 15:19:17 -0700 Subject: [PATCH 03/12] readme editing functionality --- core/gui/src/app/app.module.ts | 1 + .../dataset-detail.component.html | 1 - .../user-dataset-readme.component.html | 162 +++++++- .../user-dataset-readme.component.scss | 147 ++++++-- .../user-dataset-readme.component.spec.ts | 66 ---- .../user-dataset-readme.component.ts | 345 +++++++++--------- 6 files changed, 451 insertions(+), 271 deletions(-) delete mode 100644 core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts index c2f78c384bf..f00d99e9a11 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -173,6 +173,7 @@ import { catchError, of } from "rxjs"; import { FormlyRepeatDndComponent } from "./common/formly/repeat-dnd/repeat-dnd.component"; import { NzInputNumberModule } from "ng-zorro-antd/input-number"; import { UserDatasetReadmeComponent } from './dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component'; +import { UserDatasetReadmeComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component"; registerLocaleData(en); 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 69521cb8f57..4f9e9e02c43 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 @@ -152,7 +152,6 @@

- - + - + Create README @@ -54,11 +58,15 @@ class="readme-content-card">

- + README.md

-
+
@@ -87,7 +99,9 @@

nzType="primary" nzSize="small" (click)="saveReadme()"> - + Save @@ -103,24 +119,132 @@

-
+
-
+
- + Editor
- +
+ + + + +
+ + + + + + +
+ + + + +
+ +
@@ -129,7 +253,9 @@
- + Preview
@@ -140,7 +266,9 @@
- + You can use Markdown syntax. Changes will create a new dataset version.
diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss index c0ed98c5689..2d9d8af137d 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss @@ -53,7 +53,12 @@ .readme-view { ::ng-deep { - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { color: #1f2937; margin-top: 16px; margin-bottom: 8px; @@ -65,9 +70,15 @@ } } - h1 { font-size: 1.5em; } - h2 { font-size: 1.3em; } - h3 { font-size: 1.1em; } + h1 { + font-size: 1.5em; + } + h2 { + font-size: 1.3em; + } + h3 { + font-size: 1.1em; + } p { margin-bottom: 12px; @@ -97,7 +108,8 @@ } } - ul, ol { + ul, + ol { padding-left: 20px; margin-bottom: 12px; @@ -129,7 +141,7 @@ flex: 1; display: flex; flex-direction: column; - min-width: 0; + min-width: 0; // Prevents flex items from overflowing } .pane-title { @@ -156,26 +168,101 @@ } } - .markdown-editor { - flex: 1; + .markdown-toolbar { + display: flex; + align-items: center; + padding: 8px 12px; + background: #fafafa; + border: 1px solid #d9d9d9; + border-bottom: none; + border-radius: 4px 4px 0 0; + gap: 4px; - ::ng-deep { - .CodeMirror { - height: 100%; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.5; - border: none; + .toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: transparent; + border-radius: 3px; + cursor: pointer; + color: #666; + transition: all 0.2s ease; + + &:hover { + background: #e6f7ff; + color: #1890ff; } - .CodeMirror-focused .CodeMirror-cursor { - border-left: 1px solid #1890ff; + &:active { + background: #bae7ff; } - .CodeMirror-selected { - background: rgba(24, 144, 255, 0.1); + .toolbar-text { + font-weight: bold; + font-size: 14px; } } + + .toolbar-divider { + width: 1px; + height: 20px; + background: #d9d9d9; + margin: 0 4px; + } + } + + .markdown-textarea { + flex: 1; + width: 100%; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 0 0 4px 4px; + font-family: "SF Mono", "Monaco", "Inconsolata", "Roboto Mono", "Source Code Pro", monospace; + font-size: 14px; + line-height: 1.6; + resize: none; + outline: none; + background: #fff; + color: #333; + transition: all 0.2s ease; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + } + + &::placeholder { + color: #999; + font-style: italic; + } + } + + .editor-toolbar { + padding: 8px 0; + border-top: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + + .editor-hint { + color: #666; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; + } + + .changes-indicator { + color: #fa8c16; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; + font-weight: 500; + } } .preview-content { @@ -186,7 +273,12 @@ // Apply same styles as readonly view ::ng-deep { - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { color: #1f2937; margin-top: 16px; margin-bottom: 8px; @@ -198,9 +290,15 @@ } } - h1 { font-size: 1.5em; } - h2 { font-size: 1.3em; } - h3 { font-size: 1.1em; } + h1 { + font-size: 1.5em; + } + h2 { + font-size: 1.3em; + } + h3 { + font-size: 1.1em; + } p { margin-bottom: 12px; @@ -230,7 +328,8 @@ } } - ul, ol { + ul, + ol { padding-left: 20px; margin-bottom: 12px; diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts deleted file mode 100644 index c67d336681b..00000000000 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserDatasetReadmeComponent } from './user-dataset-readme.component'; -import { DatasetService } from '../../../../../service/user/dataset/dataset.service'; -import { NotificationService } from '../../../../../../common/service/notification/notification.service'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { MarkdownModule } from 'ngx-markdown'; -import { FormsModule } from '@angular/forms'; -import { NzEmptyModule } from 'ng-zorro-antd/empty'; -import { NzSpinModule } from 'ng-zorro-antd/spin'; -import { NzButtonModule } from 'ng-zorro-antd/button'; -import { NzAlertModule } from 'ng-zorro-antd/alert'; -import { NzIconModule } from 'ng-zorro-antd/icon'; -import { commonTestProviders } from '../../../../../../common/testing/test-utils'; - -describe('UserDatasetReadmeComponent', () => { - let component: UserDatasetReadmeComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [UserDatasetReadmeComponent], - imports: [ - HttpClientTestingModule, - MarkdownModule.forRoot(), - FormsModule, - NzEmptyModule, - NzSpinModule, - NzButtonModule, - NzAlertModule, - NzIconModule, - ], - providers: [ - DatasetService, - NotificationService, - ...commonTestProviders, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(UserDatasetReadmeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts index 63f6f984b6f..8ebf7961c2a 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts @@ -17,56 +17,52 @@ * under the License. */ -import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } 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, catchError } from 'rxjs/operators'; -import { of } from 'rxjs'; -import { DatasetFileNode } from '../../../../../../common/type/datasetVersionFileTree'; +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, +} 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"; @UntilDestroy() @Component({ - selector: 'texera-user-dataset-readme', - templateUrl: './user-dataset-readme.component.html', - styleUrls: ['./user-dataset-readme.component.scss'] + selector: "texera-user-dataset-readme", + templateUrl: "./user-dataset-readme.component.html", + styleUrls: ["./user-dataset-readme.component.scss"], }) export class UserDatasetReadmeComponent implements OnInit, OnChanges { @Input() did: number | undefined; @Input() dvid: number | undefined; @Input() selectedVersion: any | undefined; - @Input() datasetName: string = ''; + @Input() datasetName: string = ""; @Input() isMaximized: boolean = false; @Input() userHasWriteAccess: boolean = false; @Input() isLogin: boolean = true; @Output() userMakeChanges = new EventEmitter(); - public readmeContent: string = ''; + @ViewChild("markdownTextarea") markdownTextarea!: ElementRef; + + public readmeContent: string = ""; public isEditing: boolean = false; public readmeExists: boolean = false; public isLoading: boolean = false; - public editingContent: string = ''; - - private readonly README_FILE_PATH = 'README.md'; - - // CodeMirror options for markdown editing - public editorOptions = { - theme: 'default', - mode: 'markdown', - lineNumbers: true, - lineWrapping: true, - foldGutter: true, - gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - autoCloseBrackets: true, - matchBrackets: true, - indentWithTabs: false, - indentUnit: 2, - tabSize: 2 - }; + public editingContent: string = ""; + + private readonly README_FILE_PATH = "README.md"; constructor( - private datasetService: DatasetService, - private notificationService: NotificationService, + private datasetService: DatasetService, + private notificationService: NotificationService ) {} ngOnInit(): void { @@ -76,9 +72,12 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { } ngOnChanges(changes: SimpleChanges): void { - // Reload README when dataset version changes - if ((changes['dvid'] || changes['selectedVersion']) && - this.dvid && this.selectedVersion) { + if ( + (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"]) && + this.dvid && + this.datasetName && + this.selectedVersion + ) { this.loadReadme(); } } @@ -88,80 +87,43 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { this.isLoading = true; - this.datasetService - .retrieveDatasetVersionFileTree(this.did, this.dvid, this.isLogin) - .pipe( - switchMap(({ fileNodes }) => { - // Check if README.md exists in the file tree - const readmeExists = this.findReadmeInFileTree(fileNodes); - - if (readmeExists) { - const fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.README_FILE_PATH}`; - - return this.datasetService - .retrieveDatasetVersionSingleFile(fullPath, 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); - }); - }) - ); - } else { - // README doesn't exist - return of(''); - } - }), - catchError(error => { - console.log('README not found or error loading:', error); - return of(''); - }), - untilDestroyed(this) - ) - .subscribe({ - next: content => { - this.isLoading = false; - if (content) { - this.readmeExists = true; - this.readmeContent = content; - this.editingContent = content; - } else { - this.readmeExists = false; - this.readmeContent = ''; - this.editingContent = ''; - } - }, - error: () => { - this.isLoading = false; - this.readmeExists = false; - this.readmeContent = ''; - this.editingContent = ''; - } - }); - } + const fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.README_FILE_PATH}`; - private findReadmeInFileTree(fileNodes: DatasetFileNode[]): boolean { - for (const node of fileNodes) { - if (node.type === 'file' && node.name === 'README.md') { - return true; - } - if (node.type === 'directory' && node.children) { - if (this.findReadmeInFileTree(node.children)) { - return true; - } - } - } - return false; + this.datasetService + .retrieveDatasetVersionSingleFile(fullPath, 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.readmeExists = true; + this.readmeContent = content; + this.editingContent = content; + }, + error: () => { + this.isLoading = false; + this.readmeExists = false; + this.readmeContent = ""; + this.editingContent = ""; + console.log("README not found or error loading"); + }, + }); } public createReadme(): void { if (!this.did || !this.userHasWriteAccess) return; - const initialContent = '# Dataset README\n\nDescribe your dataset here...'; - this.uploadReadmeContent(initialContent, 'README created successfully'); + const initialContent = "# Dataset README\n\nDescribe your dataset here..."; + this.uploadReadmeContent(initialContent, "README created successfully"); } public startEditing(): void { @@ -175,82 +137,139 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { this.isEditing = false; } + public onEditorKeydown(event: KeyboardEvent): void { + if ((event.ctrlKey || event.metaKey) && event.key === "s") { + event.preventDefault(); + this.saveReadme(); + } + + 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 insertMarkdown(before: string, after: string = "", placeholder: string = ""): void { + if (!this.markdownTextarea) return; + + const textarea = this.markdownTextarea.nativeElement; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + + let insertText: string; + if (selectedText) { + insertText = before + selectedText + after; + } else { + insertText = before + placeholder + after; + } + + // Trigger input event to preserve undo + textarea.focus(); + document.execCommand("insertText", false, insertText); + + this.editingContent = textarea.value; + + // Update cursor position + setTimeout(() => { + if (selectedText) { + textarea.selectionStart = start + before.length; + textarea.selectionEnd = start + before.length + selectedText.length; + } else { + textarea.selectionStart = textarea.selectionEnd = start + before.length; + } + textarea.focus(); + }); + } + public saveReadme(): void { if (!this.did || !this.userHasWriteAccess) return; - this.uploadReadmeContent(this.editingContent, 'README updated successfully'); + if (this.editingContent === this.readmeContent) { + this.notificationService.warning("No changes detected in README content"); + return; + } + + this.uploadReadmeContent(this.editingContent, "README updated successfully"); } public deleteReadme(): void { if (!this.did || !this.userHasWriteAccess) return; this.datasetService - .deleteDatasetFile(this.did, this.README_FILE_PATH) - .pipe( - // After deleting, create a new version to save changes. - switchMap(() => - this.datasetService.createDatasetVersion(this.did!, 'Deleted README.md') - ), - untilDestroyed(this) - ) - .subscribe({ - next: (newVersion) => { - this.readmeExists = false; - this.readmeContent = ''; - this.editingContent = ''; - this.isEditing = false; - this.notificationService.success('README deleted successfully'); + .deleteDatasetFile(this.did, this.README_FILE_PATH) + .pipe( + // After deleting, create a new version to save changes. + switchMap(() => this.datasetService.createDatasetVersion(this.did!, "Deleted README.md")), + untilDestroyed(this) + ) + .subscribe({ + next: () => { + this.readmeExists = false; + this.readmeContent = ""; + this.editingContent = ""; + this.isEditing = false; + this.notificationService.success("README deleted successfully"); - // Emit the change to refresh file version screen - this.userMakeChanges.emit(); - }, - error: error => { - console.error('Error deleting README:', error); - this.notificationService.error('Failed to delete README'); - } - }); + // Emit the change to refresh file version screen + this.userMakeChanges.emit(); + }, + error: (error: unknown) => { + console.error("Error deleting README:", error); + this.notificationService.error("Failed to delete README"); + }, + }); } private uploadReadmeContent(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 readmeBlob = new Blob([content], { type: 'text/markdown' }); - const readmeFile = new File([readmeBlob], this.README_FILE_PATH, { type: 'text/markdown' }); - - return this.datasetService.multipartUpload(datasetName, this.README_FILE_PATH, readmeFile); - }), - // After upload completes, automatically create a new version - switchMap(progress => { - if (progress.status === 'finished') { - const versionMessage = successMessage.includes('created') ? 'Created README.md' : 'Updated README.md'; - return this.datasetService.createDatasetVersion(this.did!, versionMessage); - } - return of(progress); - }), - untilDestroyed(this) - ) - .subscribe({ - next: result => { - if (result && typeof result === 'object' && 'dvid' in result) { - this.readmeExists = true; - this.readmeContent = content; - this.isEditing = false; - this.notificationService.success(successMessage); - - // Emit the change to refresh file version screen - this.userMakeChanges.emit(); - } - }, - error: error => { - console.error('Error uploading README:', error); - this.notificationService.error('Failed to save README'); + .getDataset(this.did, this.isLogin) + .pipe( + switchMap(dashboardDataset => { + const datasetName = dashboardDataset.dataset.name; + + const readmeBlob = new Blob([content], { type: "text/markdown" }); + const readmeFile = new File([readmeBlob], this.README_FILE_PATH, { type: "text/markdown" }); + + return this.datasetService.multipartUpload(datasetName, this.README_FILE_PATH, readmeFile); + }), + // After upload completes, automatically create a new version + switchMap(progress => { + if (progress.status === "finished") { + const versionMessage = successMessage.includes("created") ? "Created README.md" : "Updated README.md"; + return this.datasetService.createDatasetVersion(this.did!, versionMessage); + } + return of(progress); + }), + untilDestroyed(this) + ) + .subscribe({ + next: result => { + if (result && typeof result === "object" && "dvid" in result) { + this.readmeExists = true; + this.readmeContent = content; + this.isEditing = false; + this.notificationService.success(successMessage); + + // Emit the change to refresh file version screen + this.userMakeChanges.emit(); } - }); + }, + error: (error: unknown) => { + console.error("Error uploading README:", error); + this.notificationService.error("Failed to save README"); + }, + }); } } From ec3d83d110d861e3eeeceba192c51537f0d74c6d Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Wed, 9 Jul 2025 15:27:42 -0700 Subject: [PATCH 04/12] simplified css --- .../user-dataset-readme.component.html | 7 +- .../user-dataset-readme.component.scss | 176 ++---------------- 2 files changed, 17 insertions(+), 166 deletions(-) diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html index 3addf4e8f87..0f021632c7b 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html @@ -28,7 +28,7 @@ - + @@ -130,7 +130,7 @@

*ngIf="isEditing" class="readme-editor-container">
- +
-
- +
Date: Mon, 11 Aug 2025 16:12:45 -0700 Subject: [PATCH 05/12] make file editor more general --- core/gui/src/app/app.module.ts | 5 +- .../dataset-detail.component.html | 80 ++++++-- .../dataset-detail.component.ts | 86 ++++++++- .../user-dataset-file-editor.component.html} | 180 ++++++++---------- .../user-dataset-file-editor.component.scss} | 0 .../user-dataset-file-editor.component.ts} | 163 ++++++++++------ 6 files changed, 342 insertions(+), 172 deletions(-) rename core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/{user-dataset-readme/user-dataset-readme.component.html => user-dataset-file-editor/user-dataset-file-editor.component.html} (59%) rename core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/{user-dataset-readme/user-dataset-readme.component.scss => user-dataset-file-editor/user-dataset-file-editor.component.scss} (100%) rename core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/{user-dataset-readme/user-dataset-readme.component.ts => user-dataset-file-editor/user-dataset-file-editor.component.ts} (60%) diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts index f00d99e9a11..d597b9b502e 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -172,8 +172,7 @@ 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 { UserDatasetReadmeComponent } from './dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component'; -import { UserDatasetReadmeComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component"; +import { UserDatasetFileEditorComponent } from "./dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component"; registerLocaleData(en); @@ -266,7 +265,7 @@ registerLocaleData(en); HubSearchResultComponent, ComputingUnitSelectionComponent, AdminSettingsComponent, - UserDatasetReadmeComponent, + UserDatasetFileEditorComponent, ], imports: [ BrowserModule, 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 4f9e9e02c43..f2c8eca1659 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 @@ -148,22 +148,6 @@

- - - - - - [isLogin]="isLogin" class="file-renderer"> + + + + Choose a Version:

Version Size: {{ formatSize(currentDatasetVersionSize) }}
+ + +
+ + + +
+ +
+ +
+
+
+ + +
+ +
+ { + const datasetName = dashboardDataset.dataset.name; + const readmeBlob = new Blob([this.quickReadmeContent], { 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.showQuickReadmeForm = false; + this.notificationService.success("README created successfully!"); + this.onFileChanged(); + } + }, + 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; + } + + 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 isEditableFile(fileName: string): boolean { + const extension = fileName.toLowerCase().split('.').pop(); + const editableExtensions = ['md', 'markdown', 'txt', 'log', 'json', 'xml', 'csv', 'yml', 'yaml']; + return editableExtensions.includes(extension || ''); + } + onPublicStatusChange(checked: boolean): void { // Handle the change in dataset public status if (this.did) { diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.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 similarity index 59% rename from core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.html rename to core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.html index 0f021632c7b..1a6aaaf0867 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.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 @@ -18,65 +18,61 @@ --> - - +
+ + nzMessage="Loading file content"> - +
- - + +
+ + +
+ + +
-

No README found

-

This dataset doesn't have a README yet.

+

No {{ getFileName() }} found

+

This file doesn't exist yet.

- +
- - -
-

- - README.md + +
+
+

+ + {{ getFileName() }}

-
+
@@ -98,10 +92,8 @@

nz-button nzType="primary" nzSize="small" - (click)="saveReadme()"> - + (click)="saveFile()"> + Save

- -
- + +
+ + +
{{ fileContent }}
- -
-
+ +
+ +
- + Editor
@@ -146,27 +137,21 @@
class="toolbar-btn" nz-tooltip="Bold" (click)="insertMarkdown('**', '**', 'bold text')"> - +
@@ -183,27 +168,21 @@
class="toolbar-btn" nz-tooltip="Quote" (click)="insertMarkdown('> ', '', 'Quote')"> - +
@@ -213,35 +192,29 @@
class="toolbar-btn" nz-tooltip="Link" (click)="insertMarkdown('[text](', ')', 'linkUrl')"> - +
@@ -252,9 +225,7 @@
- + Preview
@@ -263,13 +234,24 @@
+ +
+ +
+
- - You can use Markdown syntax. Changes will create a new dataset version. + + You can use Markdown syntax. + Changes will create a new dataset version. Press Ctrl+S to save.
- +
diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.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 similarity index 100% rename from core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.scss rename to core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.scss diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.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 similarity index 60% rename from core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.component.ts rename to core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-file-editor/user-dataset-file-editor.component.ts index 8ebf7961c2a..2df06442d81 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/user-dataset-readme/user-dataset-readme.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 @@ -36,29 +36,31 @@ import { of } from "rxjs"; @UntilDestroy() @Component({ - selector: "texera-user-dataset-readme", - templateUrl: "./user-dataset-readme.component.html", - styleUrls: ["./user-dataset-readme.component.scss"], + selector: "texera-user-dataset-file-editor", + templateUrl: "./user-dataset-file-editor.component.html", + styleUrls: ["./user-dataset-file-editor.component.scss"], }) -export class UserDatasetReadmeComponent implements OnInit, OnChanges { +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; @Output() userMakeChanges = new EventEmitter(); - @ViewChild("markdownTextarea") markdownTextarea!: ElementRef; + @ViewChild("fileTextarea") fileTextarea!: ElementRef; - public readmeContent: string = ""; + public fileContent: string = ""; public isEditing: boolean = false; - public readmeExists: boolean = false; + public fileExists: boolean = false; public isLoading: boolean = false; public editingContent: string = ""; - - private readonly README_FILE_PATH = "README.md"; + public fileType: 'markdown' | 'text' | 'unsupported' = 'unsupported'; constructor( private datasetService: DatasetService, @@ -66,28 +68,51 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { ) {} ngOnInit(): void { - if (this.dvid && this.datasetName && this.selectedVersion) { - this.loadReadme(); + if (this.dvid && this.datasetName && this.selectedVersion && this.filePath) { + this.determineFileType(); + this.loadFile(); } } ngOnChanges(changes: SimpleChanges): void { if ( - (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"]) && + (changes["dvid"] || changes["datasetName"] || changes["selectedVersion"] || changes["filePath"]) && this.dvid && this.datasetName && - this.selectedVersion + this.selectedVersion && + this.filePath ) { - this.loadReadme(); + this.determineFileType(); + this.loadFile(); + } + } + + 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 'json': + case 'xml': + case 'yml': + case 'yaml': + this.fileType = 'text'; + break; + default: + this.fileType = 'unsupported'; } } - private loadReadme(): void { - if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion) return; + private loadFile(): void { + if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion || !this.filePath) return; this.isLoading = true; - const fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.README_FILE_PATH}`; + const fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.filePath}`; this.datasetService .retrieveDatasetVersionSingleFile(fullPath, this.isLogin) @@ -105,42 +130,48 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { .subscribe({ next: content => { this.isLoading = false; - this.readmeExists = true; - this.readmeContent = content; + this.fileExists = true; + this.fileContent = content; this.editingContent = content; }, error: () => { this.isLoading = false; - this.readmeExists = false; - this.readmeContent = ""; + this.fileExists = false; + this.fileContent = ""; this.editingContent = ""; - console.log("README not found or error loading"); + console.log("File not found or error loading"); }, }); } - public createReadme(): void { + public createFile(): void { if (!this.did || !this.userHasWriteAccess) return; - const initialContent = "# Dataset README\n\nDescribe your dataset here..."; - this.uploadReadmeContent(initialContent, "README created successfully"); + let initialContent = ""; + if (this.fileType === 'markdown') { + initialContent = `# ${this.getFileName()}\n\nAdd your content here...`; + } else { + initialContent = "Add your content here..."; + } + + this.uploadFileContent(initialContent, `${this.getFileName()} created successfully`); } public startEditing(): void { - if (!this.userHasWriteAccess) return; - this.editingContent = this.readmeContent; + if (!this.userHasWriteAccess || this.fileType === 'unsupported') return; + this.editingContent = this.fileContent; this.isEditing = true; } public cancelEditing(): void { - this.editingContent = this.readmeContent; + this.editingContent = this.fileContent; this.isEditing = false; } public onEditorKeydown(event: KeyboardEvent): void { if ((event.ctrlKey || event.metaKey) && event.key === "s") { event.preventDefault(); - this.saveReadme(); + this.saveFile(); } if (event.key === "Tab") { @@ -159,9 +190,9 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { } public insertMarkdown(before: string, after: string = "", placeholder: string = ""): void { - if (!this.markdownTextarea) return; + if (!this.fileTextarea || this.fileType !== 'markdown') return; - const textarea = this.markdownTextarea.nativeElement; + const textarea = this.fileTextarea.nativeElement; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); @@ -191,46 +222,46 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { }); } - public saveReadme(): void { + public saveFile(): void { if (!this.did || !this.userHasWriteAccess) return; - if (this.editingContent === this.readmeContent) { - this.notificationService.warning("No changes detected in README content"); + if (this.editingContent === this.fileContent) { + this.notificationService.warning("No changes detected in file content"); return; } - this.uploadReadmeContent(this.editingContent, "README updated successfully"); + this.uploadFileContent(this.editingContent, `${this.getFileName()} updated successfully`); } - public deleteReadme(): void { + public deleteFile(): void { if (!this.did || !this.userHasWriteAccess) return; this.datasetService - .deleteDatasetFile(this.did, this.README_FILE_PATH) + .deleteDatasetFile(this.did, this.filePath) .pipe( // After deleting, create a new version to save changes. - switchMap(() => this.datasetService.createDatasetVersion(this.did!, "Deleted README.md")), + switchMap(() => this.datasetService.createDatasetVersion(this.did!, `Deleted ${this.filePath}`)), untilDestroyed(this) ) .subscribe({ next: () => { - this.readmeExists = false; - this.readmeContent = ""; + this.fileExists = false; + this.fileContent = ""; this.editingContent = ""; this.isEditing = false; - this.notificationService.success("README deleted successfully"); + this.notificationService.success(`${this.getFileName()} deleted successfully`); // Emit the change to refresh file version screen this.userMakeChanges.emit(); }, error: (error: unknown) => { - console.error("Error deleting README:", error); - this.notificationService.error("Failed to delete README"); + console.error("Error deleting file:", error); + this.notificationService.error(`Failed to delete ${this.getFileName()}`); }, }); } - private uploadReadmeContent(content: string, successMessage: string): void { + private uploadFileContent(content: string, successMessage: string): void { if (!this.did) return; this.datasetService @@ -239,15 +270,22 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { switchMap(dashboardDataset => { const datasetName = dashboardDataset.dataset.name; - const readmeBlob = new Blob([content], { type: "text/markdown" }); - const readmeFile = new File([readmeBlob], this.README_FILE_PATH, { type: "text/markdown" }); - - return this.datasetService.multipartUpload(datasetName, this.README_FILE_PATH, readmeFile); + const mimeType = this.getMimeType(); + const fileBlob = new Blob([content], { type: mimeType }); + const file = new File([fileBlob], this.filePath, { type: mimeType }); + + return this.datasetService.multipartUpload( + datasetName, + this.filePath, + file, + this.chunkSizeMB * 1024 * 1024, + this.maxConcurrentChunks + ); }), // After upload completes, automatically create a new version switchMap(progress => { if (progress.status === "finished") { - const versionMessage = successMessage.includes("created") ? "Created README.md" : "Updated README.md"; + const versionMessage = successMessage.includes("created") ? `Created ${this.filePath}` : `Updated ${this.filePath}`; return this.datasetService.createDatasetVersion(this.did!, versionMessage); } return of(progress); @@ -257,8 +295,8 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { .subscribe({ next: result => { if (result && typeof result === "object" && "dvid" in result) { - this.readmeExists = true; - this.readmeContent = content; + this.fileExists = true; + this.fileContent = content; this.isEditing = false; this.notificationService.success(successMessage); @@ -267,9 +305,28 @@ export class UserDatasetReadmeComponent implements OnInit, OnChanges { } }, error: (error: unknown) => { - console.error("Error uploading README:", error); - this.notificationService.error("Failed to save README"); + 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 { + return this.filePath.split('/').pop() || this.filePath; + } + + public isEditable(): boolean { + return this.fileType === 'markdown' || this.fileType === 'text'; + } } From a5ca8331e93be8bfd7b356d25974723e5e057f93 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Mon, 11 Aug 2025 16:56:33 -0700 Subject: [PATCH 06/12] fix file upload/load errors --- .../dataset-detail.component.html | 22 ++--- .../dataset-detail.component.ts | 81 +++++++++++++------ .../user-dataset-file-editor.component.ts | 42 +++++++--- 3 files changed, 101 insertions(+), 44 deletions(-) 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 f2c8eca1659..824783753e4 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 @@ -153,17 +153,6 @@

*ngIf="!selectedVersion" nzNotFoundContent="No version is selected"> - - - [maxConcurrentChunks]="maxConcurrentChunks" (userMakeChanges)="onFileChanged()"> + + + { - // Re-select the latest version (which should be the new one we just created) - if (this.versions.length > 0) { - this.selectedVersion = this.versions[0]; - this.onVersionSelected(this.selectedVersion); + // Wait a bit for the version list to update, then refresh the current version + setTimeout(() => { + 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 + } + + // Helper method to extract filename from full path + private getFileName(fullPath: string): string { + if (!fullPath) return ''; + return fullPath.split('/').pop() || fullPath; + } + + // Update your existing findFileInTree to be more robust + 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; } - }, 0); + } } + return null; } public onClickOpenReadmeEditor(): void { @@ -285,24 +333,9 @@ export class DatasetDetailComponent implements OnInit { return this.findFileInTree("README.md") !== null; } - 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 isEditableFile(fileName: string): boolean { const extension = fileName.toLowerCase().split('.').pop(); - const editableExtensions = ['md', 'markdown', 'txt', 'log', 'json', 'xml', 'csv', 'yml', 'yaml']; + const editableExtensions = ['md', 'markdown', 'txt', 'log', 'yml', 'yaml']; return editableExtensions.includes(extension || ''); } 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 index 2df06442d81..9cbe6d2af95 100644 --- 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 @@ -82,6 +82,12 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.selectedVersion && this.filePath ) { + // Reset loading states when inputs change + this.isLoading = false; + this.fileExists = false; + this.fileContent = ""; + this.editingContent = ""; + this.determineFileType(); this.loadFile(); } @@ -107,12 +113,25 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { } } + private loadFile(): void { if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion || !this.filePath) return; this.isLoading = true; - const fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.filePath}`; + // The filePath is already the relative path from the dataset version root + // We should NOT construct a new path, just use it directly + let fullPath: string; + + if (this.filePath.startsWith('/texera/')) { + // If filePath already contains the full path, use it as-is + fullPath = this.filePath; + } else { + // If it's just a relative path, construct the full path + fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.filePath}`; + } + + console.log('Loading file with path:', fullPath); // Debug log this.datasetService .retrieveDatasetVersionSingleFile(fullPath, this.isLogin) @@ -269,23 +288,28 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { .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], this.filePath, { type: mimeType }); + const file = new File([fileBlob], fileName, { type: mimeType }); return this.datasetService.multipartUpload( datasetName, - this.filePath, + fileName, file, - this.chunkSizeMB * 1024 * 1024, - this.maxConcurrentChunks + 50 * 1024 * 1024, + 10 ); }), - // After upload completes, automatically create a new version switchMap(progress => { if (progress.status === "finished") { - const versionMessage = successMessage.includes("created") ? `Created ${this.filePath}` : `Updated ${this.filePath}`; + // Fix: Use only the filename, not the full path + const fileName = this.getFileName(); + const versionMessage = successMessage.includes("created") + ? `Created ${fileName}` + : `Updated ${fileName}`; + return this.datasetService.createDatasetVersion(this.did!, versionMessage); } return of(progress); @@ -299,8 +323,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.fileContent = content; this.isEditing = false; this.notificationService.success(successMessage); - - // Emit the change to refresh file version screen this.userMakeChanges.emit(); } }, @@ -323,6 +345,8 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { } public getFileName(): string { + if (!this.filePath) return ''; + return this.filePath.split('/').pop() || this.filePath; } From ff6da19bcf0fd8a16e55d5e558142637d724661d Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Tue, 12 Aug 2025 02:32:14 -0700 Subject: [PATCH 07/12] refine appearance --- .../user-dataset-file-editor.component.html | 333 ++++++------ .../user-dataset-file-editor.component.scss | 478 ++++++++++++------ .../user-dataset-file-editor.component.ts | 12 +- 3 files changed, 509 insertions(+), 314 deletions(-) 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 index 1a6aaaf0867..fd87a9101ba 100644 --- 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 @@ -58,200 +58,203 @@
-
-

- - {{ getFileName() }} -

-
- - - - - + +
+
+ + + {{ getFileName() }} + +
- - +
- -
- - -
{{ fileContent }}
-
- - -
- -
- -
-
- - Editor -
+ +
+
+

+ + {{ getFileName() }} +

- -
- +
+ + +
+
-
+ +
+ + +
{{ fileContent }}
+
- - - - + +
+ +
+ +
+
+ + Editor +
-
+ +
+ + + - - - +
+ + + + + + +
+ + + + +
+ + +
+ +
+ + +
+
+ + Preview +
+
+ +
+
+ +
-
- - -
-
- - Preview -
-
- -
+
+ + + You can use Markdown syntax. + Changes will create a new dataset version. Press Ctrl+S to save. +
- - -
- -
- -
- - - You can use Markdown syntax. - Changes will create a new dataset version. Press Ctrl+S to save. - -
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 index 226bc667d3a..b1c1fbe904d 100644 --- 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 @@ -17,195 +17,381 @@ * under the License. */ -.readme-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - padding-bottom: 8px; - border-bottom: 1px solid #f0f0f0; - - .readme-title { - margin: 0; - font-size: 16px; - font-weight: 600; - display: flex; - align-items: center; - gap: 8px; +.file-loading, +.file-unsupported, +.file-empty { + padding: 16px; + text-align: center; +} - i { - color: #1890ff; +.file-content { + // Collapsed state - minimal appearance + .file-collapsed { + .file-header-collapsed { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: #f8f9fa; + border: 1px solid #e8e8e8; + border-radius: 6px; + transition: all 0.3s ease; + + &:hover { + background: #f0f0f0; + border-color: #d9d9d9; + } + + .file-title-small { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 500; + color: #595959; + + i { + color: #1890ff; + } + } + + .file-actions { + display: flex; + gap: 8px; + } } } - .readme-controls { - display: flex; - gap: 4px; + // Expanded state - full editor + .file-expanded { + border: 1px solid #e8e8e8; + border-radius: 6px; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } -} -@mixin markdown-content { - ::ng-deep { - h1, - h2, - h3, - h4, - h5, - h6 { - margin-top: 16px; - margin-bottom: 8px; + .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; - line-height: 1.25; + color: #262626; - &:first-child { - margin-top: 0; + i { + color: #1890ff; } } - h1 { - font-size: 1.5em; - } - h2 { - font-size: 1.3em; - } - h3 { - font-size: 1.1em; + .file-controls { + display: flex; + gap: 8px; + align-items: center; } + } + + .file-view { + padding: 16px; - p { - margin-bottom: 12px; + .text-content { + white-space: pre-wrap; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; line-height: 1.6; + background: #f8f8f8; + padding: 16px; + border-radius: 4px; + border: 1px solid #e8e8e8; + overflow-x: auto; } - code { - background: #f3f4f6; - padding: 2px 4px; - border-radius: 3px; - font-family: monospace; - font-size: 0.9em; - } + // Markdown content styles for view mode + ::ng-deep { + h1, h2, h3, h4, h5, h6 { + margin-top: 16px; + margin-bottom: 8px; + font-weight: 600; + line-height: 1.25; - pre { - background: #f8fafc; - border: 1px solid #e5e7eb; - border-radius: 4px; - padding: 12px; - overflow-x: auto; - margin: 12px 0; + &:first-child { + margin-top: 0; + } + } + + h1 { font-size: 1.5em; } + h2 { font-size: 1.3em; } + h3 { font-size: 1.1em; } + + p { + margin-bottom: 12px; + line-height: 1.6; + } code { - background: none; - padding: 0; + background: #f3f4f6; + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; } - } - ul, - ol { - padding-left: 20px; - margin-bottom: 12px; - } + pre { + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 12px; + overflow-x: auto; + margin: 12px 0; + + code { + background: none; + padding: 0; + } + } - blockquote { - border-left: 4px solid #e5e7eb; - padding-left: 12px; - margin: 12px 0; - font-style: italic; + ul, ol { + padding-left: 20px; + margin-bottom: 12px; + } + + blockquote { + border-left: 4px solid #e5e7eb; + padding-left: 12px; + margin: 12px 0; + font-style: italic; + } } } -} -.readme-view { - @include markdown-content; -} + .file-editor-container { + .editor-split-view { + display: flex; + height: 500px; -.readme-editor-container { - .editor-split-view { - display: flex; - height: 500px; - border: 1px solid #d9d9d9; - border-radius: 4px; - overflow: hidden; + .editor-pane, + .preview-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } - .editor-pane, - .preview-pane { - flex: 1; - display: flex; - flex-direction: column; + .editor-divider { + width: 1px; + background: #d9d9d9; + margin: 0; + } + + .pane-title { + margin: 0; + padding: 8px 12px; + background: #f0f0f0; + border-bottom: 1px solid #d9d9d9; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + color: #595959; + } + + .markdown-toolbar { + display: flex; + gap: 4px; + padding: 8px 12px; + background: #fafafa; + border-bottom: 1px solid #e8e8e8; + flex-wrap: wrap; + + .toolbar-btn { + border: none; + background: transparent; + padding: 6px 8px; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; + transition: all 0.2s ease; + + &:hover { + background: #e6f7ff; + color: #1890ff; + } + + .toolbar-text { + font-weight: bold; + font-size: 12px; + } + } + + .toolbar-divider { + width: 1px; + background: #d9d9d9; + margin: 0 6px; + align-self: stretch; + } + } + + .file-textarea { + flex: 1; + resize: none; + border: none; + outline: none; + padding: 16px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 14px; + line-height: 1.6; + background: white; + + &:focus { + box-shadow: none; + } + } + + .preview-content { + flex: 1; + padding: 16px; + overflow-y: auto; + background: white; + border-left: 1px solid #e8e8e8; + + // Markdown content styles for preview + ::ng-deep { + h1, h2, h3, h4, h5, h6 { + margin-top: 16px; + margin-bottom: 8px; + font-weight: 600; + line-height: 1.25; + + &:first-child { + margin-top: 0; + } + } + + h1 { font-size: 1.5em; } + h2 { font-size: 1.3em; } + h3 { font-size: 1.1em; } + + p { + margin-bottom: 12px; + line-height: 1.6; + } + + code { + background: #f3f4f6; + padding: 2px 4px; + border-radius: 3px; + font-family: monospace; + font-size: 0.9em; + } + + pre { + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 12px; + overflow-x: auto; + margin: 12px 0; + + code { + background: none; + padding: 0; + } + } + + ul, ol { + padding-left: 20px; + margin-bottom: 12px; + } + + blockquote { + border-left: 4px solid #e5e7eb; + padding-left: 12px; + margin: 12px 0; + font-style: italic; + } + } + } } - .pane-title { - margin: 0; - padding: 8px 12px; - background: #fafafa; - border-bottom: 1px solid #d9d9d9; - font-size: 14px; - font-weight: 500; + .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; + + &:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); + outline: none; + } + } + } + + .editor-help { + padding: 12px 16px; + background: #f8f9fa; + border-top: 1px solid #e8e8e8; + border-radius: 0 0 6px 6px; + font-size: 12px; + color: #666; display: flex; align-items: center; gap: 6px; } - - .editor-divider { - width: 1px; - background: #d9d9d9; - } } +} - .markdown-toolbar { - display: flex; - align-items: center; - padding: 8px 12px; - background: #fafafa; - border-bottom: 1px solid #d9d9d9; - gap: 4px; - - .toolbar-btn { - width: 28px; - height: 28px; - border: none; - background: transparent; - border-radius: 3px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: background 0.2s; +// Responsive design +@media (max-width: 768px) { + .file-content .file-editor-container .editor-split-view { + flex-direction: column; + height: auto; - &:hover { - background: #e6f7ff; - } + .editor-pane, + .preview-pane { + min-height: 250px; } - .toolbar-divider { - width: 1px; - height: 20px; - background: #d9d9d9; - margin: 0 4px; + .editor-divider { + width: 100%; + height: 1px; + margin: 0; } - } - .markdown-textarea { - flex: 1; - width: 100%; - padding: 12px; - border: none; - font-family: monospace; - font-size: 14px; - line-height: 1.6; - resize: none; - outline: none; - - &:focus { - box-shadow: inset 0 0 0 1px #1890ff; + .preview-content { + border-left: none; + border-top: 1px solid #e8e8e8; } } - .preview-content { - flex: 1; - padding: 12px; - overflow-y: auto; - background: #fff; + .file-content .file-collapsed .file-header-collapsed { + flex-direction: column; + gap: 12px; + align-items: stretch; - @include markdown-content; + .file-actions { + justify-content: center; + } } } 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 index 9cbe6d2af95..3d9eea29651 100644 --- 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 @@ -1,3 +1,4 @@ + /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -61,6 +62,7 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { public isLoading: boolean = false; public editingContent: string = ""; public fileType: 'markdown' | 'text' | 'unsupported' = 'unsupported'; + public showFileContent: boolean = false; // Add this for expand/collapse constructor( private datasetService: DatasetService, @@ -82,7 +84,9 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.selectedVersion && this.filePath ) { - // Reset loading states when inputs change + // Reset editing states when switching files + this.isEditing = false; + this.showFileContent = false; this.isLoading = false; this.fileExists = false; this.fileContent = ""; @@ -102,8 +106,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { break; case 'txt': case 'log': - case 'json': - case 'xml': case 'yml': case 'yaml': this.fileType = 'text'; @@ -187,6 +189,10 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.isEditing = false; } + public expandFile(): void { + this.startEditing(); + } + public onEditorKeydown(event: KeyboardEvent): void { if ((event.ctrlKey || event.metaKey) && event.key === "s") { event.preventDefault(); From a1446e1788f87da52cc1103f34cf7c4a5b3a27b3 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Tue, 12 Aug 2025 02:48:36 -0700 Subject: [PATCH 08/12] clean up code --- .../user-dataset-file-editor.component.html | 30 --------- .../user-dataset-file-editor.component.scss | 34 ----------- .../user-dataset-file-editor.component.ts | 61 +------------------ 3 files changed, 2 insertions(+), 123 deletions(-) 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 index fd87a9101ba..fbfb747dc81 100644 --- 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 @@ -26,36 +26,6 @@
- -
- - -
- - -
- - -

No {{ getFileName() }} found

-

This file doesn't exist yet.

- -
-
-
-
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 index b1c1fbe904d..a8fcc85cbfe 100644 --- 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 @@ -361,37 +361,3 @@ } } } - -// Responsive design -@media (max-width: 768px) { - .file-content .file-editor-container .editor-split-view { - flex-direction: column; - height: auto; - - .editor-pane, - .preview-pane { - min-height: 250px; - } - - .editor-divider { - width: 100%; - height: 1px; - margin: 0; - } - - .preview-content { - border-left: none; - border-top: 1px solid #e8e8e8; - } - } - - .file-content .file-collapsed .file-header-collapsed { - flex-direction: column; - gap: 12px; - align-items: stretch; - - .file-actions { - justify-content: center; - } - } -} 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 index 3d9eea29651..eb3e31dbafa 100644 --- 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 @@ -62,8 +62,7 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { public isLoading: boolean = false; public editingContent: string = ""; public fileType: 'markdown' | 'text' | 'unsupported' = 'unsupported'; - public showFileContent: boolean = false; // Add this for expand/collapse - + public showFileContent: boolean = false; constructor( private datasetService: DatasetService, private notificationService: NotificationService @@ -84,7 +83,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.selectedVersion && this.filePath ) { - // Reset editing states when switching files this.isEditing = false; this.showFileContent = false; this.isLoading = false; @@ -121,22 +119,8 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.isLoading = true; - // The filePath is already the relative path from the dataset version root - // We should NOT construct a new path, just use it directly - let fullPath: string; - - if (this.filePath.startsWith('/texera/')) { - // If filePath already contains the full path, use it as-is - fullPath = this.filePath; - } else { - // If it's just a relative path, construct the full path - fullPath = `/texera/${this.datasetName}/${this.selectedVersion.name}/${this.filePath}`; - } - - console.log('Loading file with path:', fullPath); // Debug log - this.datasetService - .retrieveDatasetVersionSingleFile(fullPath, this.isLogin) + .retrieveDatasetVersionSingleFile(this.filePath, this.isLogin) .pipe( switchMap(blob => { return new Promise((resolve, reject) => { @@ -165,19 +149,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { }); } - public createFile(): void { - if (!this.did || !this.userHasWriteAccess) return; - - let initialContent = ""; - if (this.fileType === 'markdown') { - initialContent = `# ${this.getFileName()}\n\nAdd your content here...`; - } else { - initialContent = "Add your content here..."; - } - - this.uploadFileContent(initialContent, `${this.getFileName()} created successfully`); - } - public startEditing(): void { if (!this.userHasWriteAccess || this.fileType === 'unsupported') return; this.editingContent = this.fileContent; @@ -258,34 +229,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.uploadFileContent(this.editingContent, `${this.getFileName()} updated successfully`); } - public deleteFile(): void { - if (!this.did || !this.userHasWriteAccess) return; - - this.datasetService - .deleteDatasetFile(this.did, this.filePath) - .pipe( - // After deleting, create a new version to save changes. - switchMap(() => this.datasetService.createDatasetVersion(this.did!, `Deleted ${this.filePath}`)), - untilDestroyed(this) - ) - .subscribe({ - next: () => { - this.fileExists = false; - this.fileContent = ""; - this.editingContent = ""; - this.isEditing = false; - this.notificationService.success(`${this.getFileName()} deleted successfully`); - - // Emit the change to refresh file version screen - this.userMakeChanges.emit(); - }, - error: (error: unknown) => { - console.error("Error deleting file:", error); - this.notificationService.error(`Failed to delete ${this.getFileName()}`); - }, - }); - } - private uploadFileContent(content: string, successMessage: string): void { if (!this.did) return; From a55fca7ff569b003c3ac635076c02892937afb53 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Tue, 12 Aug 2025 03:17:41 -0700 Subject: [PATCH 09/12] change quickReadme to Readme --- .../dataset-detail.component.html | 27 +++------ .../dataset-detail.component.scss | 57 +++++++++++++++---- .../dataset-detail.component.ts | 22 +++---- 3 files changed, 61 insertions(+), 45 deletions(-) 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 824783753e4..a32a065bc46 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 @@ -249,18 +249,18 @@
Choose a Version:
nzType="dashed" nzBlock nzSize="small" - (click)="showQuickReadmeForm = !showQuickReadmeForm"> + (click)="showReadmeForm = !showReadmeForm"> - {{ showQuickReadmeForm ? 'Cancel' : 'Quick README' }} + {{ showReadmeForm ? 'Cancel' : 'Create README.md' }} - -
+ +

nzSize="small" (click)="onClickCreateReadme()" [nzLoading]="isCreatingReadme" - [disabled]="!quickReadmeContent.trim()"> + [disabled]="!readmeContent.trim()"> Create
- -
- -
- { const datasetName = dashboardDataset.dataset.name; - const readmeBlob = new Blob([this.quickReadmeContent], { type: "text/markdown" }); + const readmeBlob = new Blob([this.readmeContent], { type: "text/markdown" }); const readmeFile = new File([readmeBlob], "README.md", { type: "text/markdown" }); return this.datasetService.multipartUpload( datasetName, @@ -316,7 +308,7 @@ export class DatasetDetailComponent implements OnInit { next: result => { if (result && typeof result === "object" && "dvid" in result) { this.isCreatingReadme = false; - this.showQuickReadmeForm = false; + this.showReadmeForm = false; this.notificationService.success("README created successfully!"); this.onFileChanged(); } From 5939a06e45a3fabc900fcd35e31a88f1d30683d8 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Tue, 12 Aug 2025 03:21:35 -0700 Subject: [PATCH 10/12] add back comments --- .../dataset-detail.component.scss | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss index 5571b6ef856..447a7aabafc 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.scss @@ -18,19 +18,19 @@ */ .create-dataset-version-button { - display: flex; - align-items: center; - justify-content: center; + display: flex; /* Use flexbox for centering */ + align-items: center; /* Center vertically */ + justify-content: center; /* Center horizontally */ color: white; border: none; - padding: 12px 40px; + padding: 12px 40px; /* Increase padding for a wider button */ border-radius: 25px; cursor: pointer; transition: background-color 0.3s; - margin: 50px auto 0 auto; - width: 200px; - font-size: 18px; - font-weight: bold; + margin: 50px auto 0 auto; /* Auto margins for horizontal centering */ + width: 200px; /* Adjust width as needed */ + font-size: 18px; /* Make text slightly bigger */ + font-weight: bold; /* Optional: Make text bold */ } .version-storage { @@ -42,7 +42,7 @@ } .version-storage nz-select { - width: 100%; + width: 100%; /* Make the select element take the full width of its parent */ } .right-panel-title { @@ -90,7 +90,7 @@ body { .grayed-out { filter: grayscale(1) opacity(0.7); - background-color: #f0f0f0; + background-color: #f0f0f0; /* Adjust the color as needed */ } .disabled-click { From 7fde5000d2371c3821a9fa74dbec1e2c91041224 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Tue, 26 Aug 2025 10:49:14 -0700 Subject: [PATCH 11/12] addressed comments --- core/gui/angular.json | 7 +- core/gui/package.json | 7 + core/gui/src/app/app.module.ts | 2 + .../dataset-detail.component.html | 48 +-- .../dataset-detail.component.scss | 21 +- .../dataset-detail.component.ts | 50 ++- .../user-dataset-file-editor.component.html | 188 ++------- .../user-dataset-file-editor.component.scss | 392 +++--------------- .../user-dataset-file-editor.component.ts | 109 ++--- core/gui/yarn.lock | 82 ++++ 10 files changed, 309 insertions(+), 597 deletions(-) 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 d597b9b502e..eac6565b47b 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -173,6 +173,7 @@ 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); @@ -332,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 a32a065bc46..5ff858a570d 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 @@ -88,6 +88,18 @@

+

- - -
- -
- -
-
{ + this.createReadmeFile(); + } + }); } - public onClickCreateReadme(): void { - if (!this.did || !this.readmeContent.trim()) return; + 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([this.readmeContent], { type: "text/markdown" }); + const readmeBlob = new Blob([defaultReadmeContent], { type: "text/markdown" }); const readmeFile = new File([readmeBlob], "README.md", { type: "text/markdown" }); return this.datasetService.multipartUpload( datasetName, @@ -308,9 +314,14 @@ export class DatasetDetailComponent implements OnInit { next: result => { if (result && typeof result === "object" && "dvid" in result) { this.isCreatingReadme = false; - this.showReadmeForm = false; this.notificationService.success("README created successfully!"); + + this.currentDisplayedFileName = "README.md"; this.onFileChanged(); + + setTimeout(() => { + this.isEditMode = true; + }, 1000); } }, error: (error: unknown) => { @@ -331,6 +342,16 @@ export class DatasetDetailComponent implements OnInit { 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) { @@ -411,6 +432,8 @@ export class DatasetDetailComponent implements OnInit { } onVersionSelected(version: DatasetVersion): void { + this.exitEditMode(); + this.selectedVersion = version; if (this.did && this.selectedVersion.dvid) this.datasetService @@ -428,6 +451,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 index fbfb747dc81..c38399008a5 100644 --- 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 @@ -28,42 +28,29 @@
- - -
-
- - - {{ getFileName() }} - -
- -
-
-
- - -
+
-

- - {{ getFileName() }} -

+
+

+ + {{ getFileName() }} +

+
+ + + + Unsaved changes + + +
- -
- - -
{{ fileContent }}
-
- -
- -
- -
-
- - Editor -
- - -
- - - - -
- - - - - - -
- - - - -
- - -
- -
- - -
-
- - Preview -
-
- -
-
+
+ + +
+ +
- +
- -
- - - You can use Markdown syntax. - Changes will create a new dataset version. Press Ctrl+S to save. - -
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 index a8fcc85cbfe..b13545ea786 100644 --- 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 @@ -17,347 +17,83 @@ * under the License. */ -.file-loading, -.file-unsupported, -.file-empty { +.file-loading { padding: 16px; text-align: center; } .file-content { - // Collapsed state - minimal appearance - .file-collapsed { - .file-header-collapsed { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; - background: #f8f9fa; - border: 1px solid #e8e8e8; - border-radius: 6px; - transition: all 0.3s ease; - - &:hover { - background: #f0f0f0; - border-color: #d9d9d9; - } - - .file-title-small { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 500; - color: #595959; - - i { - color: #1890ff; - } - } - - .file-actions { - display: flex; - gap: 8px; - } - } - } - - // Expanded state - full editor - .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; - - i { - color: #1890ff; - } - } - - .file-controls { - display: flex; - gap: 8px; - align-items: center; - } - } - - .file-view { - padding: 16px; - - .text-content { - white-space: pre-wrap; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.6; - background: #f8f8f8; - padding: 16px; - border-radius: 4px; - border: 1px solid #e8e8e8; - overflow-x: auto; - } - - // Markdown content styles for view mode - ::ng-deep { - h1, h2, h3, h4, h5, h6 { - margin-top: 16px; - margin-bottom: 8px; - font-weight: 600; - line-height: 1.25; - - &:first-child { - margin-top: 0; - } - } - - h1 { font-size: 1.5em; } - h2 { font-size: 1.3em; } - h3 { font-size: 1.1em; } - - p { - margin-bottom: 12px; - line-height: 1.6; - } - - code { - background: #f3f4f6; - padding: 2px 4px; - border-radius: 3px; - font-family: monospace; - font-size: 0.9em; - } - - pre { - background: #f8fafc; - border: 1px solid #e5e7eb; - border-radius: 4px; - padding: 12px; - overflow-x: auto; - margin: 12px 0; - - code { - background: none; - padding: 0; - } - } - - ul, ol { - padding-left: 20px; - margin-bottom: 12px; - } - - blockquote { - border-left: 4px solid #e5e7eb; - padding-left: 12px; - margin: 12px 0; - font-style: italic; - } - } - } - - .file-editor-container { - .editor-split-view { - display: flex; - height: 500px; - - .editor-pane, - .preview-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - } - - .editor-divider { - width: 1px; - background: #d9d9d9; - margin: 0; - } - - .pane-title { - margin: 0; - padding: 8px 12px; - background: #f0f0f0; - border-bottom: 1px solid #d9d9d9; - font-size: 14px; - font-weight: 600; - display: flex; - align-items: center; - gap: 6px; - color: #595959; - } - - .markdown-toolbar { - display: flex; - gap: 4px; - padding: 8px 12px; - background: #fafafa; - border-bottom: 1px solid #e8e8e8; - flex-wrap: wrap; - - .toolbar-btn { - border: none; - background: transparent; - padding: 6px 8px; - cursor: pointer; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - min-width: 32px; - height: 32px; - transition: all 0.2s ease; - - &:hover { - background: #e6f7ff; - color: #1890ff; - } - - .toolbar-text { - font-weight: bold; - font-size: 12px; - } - } - - .toolbar-divider { - width: 1px; - background: #d9d9d9; - margin: 0 6px; - align-self: stretch; - } - } - - .file-textarea { - flex: 1; - resize: none; - border: none; - outline: none; - padding: 16px; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 14px; - line-height: 1.6; - background: white; - - &:focus { - box-shadow: none; - } - } - - .preview-content { - flex: 1; - padding: 16px; - overflow-y: auto; - background: white; - border-left: 1px solid #e8e8e8; - - // Markdown content styles for preview - ::ng-deep { - h1, h2, h3, h4, h5, h6 { - margin-top: 16px; - margin-bottom: 8px; - font-weight: 600; - line-height: 1.25; - - &:first-child { - margin-top: 0; - } - } + /* Container for file editing interface */ +} - h1 { font-size: 1.5em; } - h2 { font-size: 1.3em; } - h3 { font-size: 1.1em; } +.file-expanded { + border: 1px solid #e8e8e8; + border-radius: 6px; + background: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} - p { - margin-bottom: 12px; - line-height: 1.6; - } +.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; +} - code { - background: #f3f4f6; - padding: 2px 4px; - border-radius: 3px; - font-family: monospace; - font-size: 0.9em; - } +.file-title { + margin: 0; + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #262626; +} - pre { - background: #f8fafc; - border: 1px solid #e5e7eb; - border-radius: 4px; - padding: 12px; - overflow-x: auto; - margin: 12px 0; +.file-title i { + color: #1890ff; +} - code { - background: none; - padding: 0; - } - } +.file-controls { + display: flex; + gap: 8px; + align-items: center; +} - ul, ol { - padding-left: 20px; - margin-bottom: 12px; - } +.unsaved-indicator { + color: #fa8c16; + font-size: 12px; + display: flex; + align-items: center; + gap: 4px; +} - blockquote { - border-left: 4px solid #e5e7eb; - padding-left: 12px; - margin: 12px 0; - font-style: italic; - } - } - } - } +.file-editor-container { + /* Container for different editor types */ +} - .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; +.markdown-editor-wrapper { + /* Wrapper for angular-markdown-editor */ +} - &:focus { - border-color: #1890ff; - box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); - outline: none; - } - } - } +.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; +} - .editor-help { - padding: 12px 16px; - background: #f8f9fa; - border-top: 1px solid #e8e8e8; - border-radius: 0 0 6px 6px; - font-size: 12px; - color: #666; - display: flex; - align-items: center; - gap: 6px; - } - } +.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 index eb3e31dbafa..f73c6c14f37 100644 --- 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 @@ -1,4 +1,3 @@ - /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -28,18 +27,22 @@ import { 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; @@ -52,26 +55,36 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { @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 isEditing: boolean = false; 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 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; } } @@ -83,7 +96,7 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { this.selectedVersion && this.filePath ) { - this.isEditing = false; + this.isEditMode = false; this.showFileContent = false; this.isLoading = false; this.fileExists = false; @@ -95,6 +108,22 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { } } + 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) { @@ -113,7 +142,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { } } - private loadFile(): void { if (!this.did || !this.dvid || !this.datasetName || !this.selectedVersion || !this.filePath) return; @@ -149,19 +177,19 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { }); } - public startEditing(): void { - if (!this.userHasWriteAccess || this.fileType === 'unsupported') return; - this.editingContent = this.fileContent; - this.isEditing = true; - } - public cancelEditing(): void { this.editingContent = this.fileContent; - this.isEditing = false; + this.isEditMode = false; + this.editCanceled.emit(); } - public expandFile(): void { - this.startEditing(); + 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 { @@ -185,39 +213,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { } } - public insertMarkdown(before: string, after: string = "", placeholder: string = ""): void { - if (!this.fileTextarea || this.fileType !== 'markdown') return; - - const textarea = this.fileTextarea.nativeElement; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const selectedText = textarea.value.substring(start, end); - - let insertText: string; - if (selectedText) { - insertText = before + selectedText + after; - } else { - insertText = before + placeholder + after; - } - - // Trigger input event to preserve undo - textarea.focus(); - document.execCommand("insertText", false, insertText); - - this.editingContent = textarea.value; - - // Update cursor position - setTimeout(() => { - if (selectedText) { - textarea.selectionStart = start + before.length; - textarea.selectionEnd = start + before.length + selectedText.length; - } else { - textarea.selectionStart = textarea.selectionEnd = start + before.length; - } - textarea.focus(); - }); - } - public saveFile(): void { if (!this.did || !this.userHasWriteAccess) return; @@ -253,7 +248,6 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { }), switchMap(progress => { if (progress.status === "finished") { - // Fix: Use only the filename, not the full path const fileName = this.getFileName(); const versionMessage = successMessage.includes("created") ? `Created ${fileName}` @@ -270,7 +264,7 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { if (result && typeof result === "object" && "dvid" in result) { this.fileExists = true; this.fileContent = content; - this.isEditing = false; + this.isEditMode = false; this.notificationService.success(successMessage); this.userMakeChanges.emit(); } @@ -295,11 +289,26 @@ export class UserDatasetFileEditorComponent implements OnInit, OnChanges { 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 0fe16ba21cd..8a0b34ae962 100644 --- a/core/gui/yarn.lock +++ b/core/gui/yarn.lock @@ -4718,6 +4718,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" @@ -5019,6 +5026,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" @@ -5214,6 +5230,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" @@ -6350,6 +6375,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" @@ -7013,6 +7051,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" @@ -10503,6 +10564,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" @@ -11118,11 +11186,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" @@ -11135,8 +11205,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" @@ -11151,6 +11224,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" @@ -11158,6 +11232,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" @@ -12404,6 +12479,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" From 73adc7b4a7f74ca89aed3363bb8cd98d761d9cc7 Mon Sep 17 00:00:00 2001 From: Colin Harrison Date: Tue, 26 Aug 2025 11:07:37 -0700 Subject: [PATCH 12/12] ran formatter --- core/gui/src/app/app.module.ts | 2 +- .../dataset-detail.component.html | 8 ++- .../dataset-detail.component.ts | 20 +++---- .../user-dataset-file-editor.component.html | 46 ++++++++++---- .../user-dataset-file-editor.component.scss | 2 +- .../user-dataset-file-editor.component.ts | 60 ++++++++----------- 6 files changed, 78 insertions(+), 60 deletions(-) diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts index eac6565b47b..e61ea791cde 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -173,7 +173,7 @@ 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'; +import { AngularMarkdownEditorModule } from "angular-markdown-editor"; registerLocaleData(en); 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 5ff858a570d..c0b1682bf3c 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 @@ -257,7 +257,9 @@
Choose a Version:
-
+
class="compact-readme-btn" [nzLoading]="isCreatingReadme" (click)="onClickCreateReadme()"> - + Create README.md
diff --git a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts index e77ba132f64..14328c5f190 100644 --- a/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts +++ b/core/gui/src/app/dashboard/component/user/user-dataset/user-dataset-explorer/dataset-detail.component.ts @@ -40,7 +40,7 @@ import { DatasetStagedObject } from "../../../../../common/type/dataset-staged-o import { NzModalService } from "ng-zorro-antd/modal"; import { UserDatasetVersionCreatorComponent } from "./user-dataset-version-creator/user-dataset-version-creator.component"; import { AdminSettingsService } from "../../../../service/admin/settings/admin-settings.service"; -import {of} from "rxjs"; +import { of } from "rxjs"; export const THROTTLE_TIME_MS = 1000; @@ -251,7 +251,7 @@ export class DatasetDetailComponent implements OnInit { } }, 500); // Small delay to ensure backend has processed the new version - this.exitEditMode() + this.exitEditMode(); } private findFileInTree(fileName: string, nodes: DatasetFileNode[] = this.fileTreeNodeList): DatasetFileNode | null { @@ -271,13 +271,13 @@ export class DatasetDetailComponent implements OnInit { 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', + 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(); - } + }, }); } @@ -337,9 +337,9 @@ export class DatasetDetailComponent implements OnInit { } public isEditableFile(fileName: string): boolean { - const extension = fileName.toLowerCase().split('.').pop(); - const editableExtensions = ['md', 'markdown', 'txt', 'log', 'yml', 'yaml']; - return editableExtensions.includes(extension || ''); + const extension = fileName.toLowerCase().split(".").pop(); + const editableExtensions = ["md", "markdown", "txt", "log", "yml", "yaml"]; + return editableExtensions.includes(extension || ""); } public onClickEditFile(): void { 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 index c38399008a5..09d9d8cf0df 100644 --- 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 @@ -18,7 +18,9 @@ --> -
+
-
-
+
+

- + {{ getFileName() }}

- - + + Unsaved changes @@ -52,7 +65,9 @@

nzSize="small" [disabled]="!hasUnsavedChanges()" (click)="saveFile()"> - + Save @@ -68,10 +85,13 @@

-
- +
-
+
-
+