From 685655bc9cd16eb725baf9e5e3278dc8b94dc3ed Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 14 Feb 2026 14:01:52 -0800 Subject: [PATCH 1/8] added card-item component to platform --- frontend/src/app/app.module.ts | 2 + .../card-item/card-item.component.html | 183 ++++++++ .../card-item/card-item.component.scss | 165 ++++++++ .../card-item/card-item.component.ts | 400 ++++++++++++++++++ 4 files changed, 750 insertions(+) create mode 100644 frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html create mode 100644 frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss create mode 100644 frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index a591207f605..0bfe9c0b6a9 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -157,6 +157,7 @@ import { ResultExportationComponent } from "./workspace/component/result-exporta import { ReportGenerationService } from "./workspace/service/report-generation/report-generation.service"; import { SearchBarComponent } from "./dashboard/component/user/search-bar/search-bar.component"; import { ListItemComponent } from "./dashboard/component/user/list-item/list-item.component"; +import { CardItemComponent } from "./dashboard/component/user/list-item/card-item/card-item.component"; import { HubComponent } from "./hub/component/hub.component"; import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component"; import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component"; @@ -270,6 +271,7 @@ registerLocaleData(en); HighlightSearchTermsPipe, SearchBarComponent, ListItemComponent, + CardItemComponent, HubComponent, HubWorkflowDetailComponent, LandingPageComponent, diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html new file mode 100644 index 00000000000..199bf7e22c9 --- /dev/null +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html @@ -0,0 +1,183 @@ + + +
+ +
+ +
+
+ + +
+ +
+
+ + +
+
+ {{ entry.name }} +
+ +
+
+ + + +
+ + +
+
+ {{ entry.description ? entry.description : (hovering ? 'Write a description...' : '') }} +
+ +
+ + +
+
+ + {{ entry.ownerName || 'User' }} +
+
{{ formatTime(entry.lastModifiedTime) }}
+
+ + +
+ + + + +
+ + + + + +
+
+
+
diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss new file mode 100644 index 00000000000..bad6d27c588 --- /dev/null +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss @@ -0,0 +1,165 @@ +.card-item { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + transition: box-shadow 0.2s; + overflow: hidden; + + &:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &.selected { + border-color: #1e90ff; + background-color: #e6f7ff; + } +} + +.card-preview { + height: 150px; + background-color: #f5f5f5; + position: relative; + cursor: pointer; + border-bottom: 1px solid #f0f0f0; + + &:hover { + background-color: #e8e8e8; + } +} + +.card-checkbox { + position: absolute; + top: 8px; + left: 8px; + z-index: 10; +} + +.card-content { + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; +} + +.title-container { + display: flex; + align-items: center; + gap: 8px; + overflow: hidden; + flex: 1; +} + +.type-icon { + font-size: 20px; + color: #1890ff; +} + +.name-container { + flex: 1; + overflow: hidden; +} + +.resource-name { + font-size: 16px; + font-weight: 500; + color: rgba(0, 0, 0, 0.85); +} + +.resource-name-edit-input { + width: 100%; + font-size: 16px; + padding: 2px 4px; +} + +.edit-btn { + opacity: 0; + transition: opacity 0.2s; +} + +.card-item:hover .edit-btn { + opacity: 1; +} + +.card-description { + flex: 1; + min-height: 40px; /* Ensure 2 lines roughly */ + cursor: pointer; +} + +.resource-description { + font-size: 13px; + color: rgba(0, 0, 0, 0.45); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.resource-description-edit-textarea { + width: 100%; + resize: none; + font-size: 13px; +} + +.card-meta { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 12px; + color: rgba(0, 0, 0, 0.45); + margin-top: auto; +} + +.owner-info { + display: flex; + align-items: center; + gap: 6px; + max-width: 60%; +} + +.owner-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.card-actions { + display: flex; + justify-content: space-between; + align-items: center; + border-top: 1px solid #f0f0f0; + padding-top: 8px; + margin-top: 8px; +} + +.action-btn { + padding: 4px 8px; + color: rgba(0, 0, 0, 0.45); + + &:hover { + color: #1890ff; + } +} + +.liked { + color: #ff4d4f; +} + +.delete-btn:hover { + color: #ff4d4f; +} + +.truncate-single-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts new file mode 100644 index 00000000000..fecdd59236d --- /dev/null +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts @@ -0,0 +1,400 @@ +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewChild, +} from "@angular/core"; +// import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { untilDestroyed } from "@ngneat/until-destroy"; +import { NzModalRef, NzModalService } from "ng-zorro-antd/modal"; +import { DashboardEntry } from "src/app/dashboard/type/dashboard-entry"; +import { ShareAccessComponent } from "../../share-access/share-access.component"; +import { + DEFAULT_WORKFLOW_NAME, + WorkflowPersistService, +} from "src/app/common/service/workflow-persist/workflow-persist.service"; +import { firstValueFrom } from "rxjs"; +import { HubWorkflowDetailComponent } from "../../../../../hub/component/workflow/detail/hub-workflow-detail.component"; +import { ActionType, HubService } from "../../../../../hub/service/hub.service"; +import { DownloadService } from "src/app/dashboard/service/user/download/download.service"; +import { formatSize } from "src/app/common/util/size-formatter.util"; +import { DatasetService, DEFAULT_DATASET_NAME } from "../../../../service/user/dataset/dataset.service"; +import { NotificationService } from "../../../../../common/service/notification/notification.service"; +import { + DASHBOARD_HUB_DATASET_RESULT_DETAIL, + DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, + DASHBOARD_USER_DATASET, + DASHBOARD_USER_PROJECT, + DASHBOARD_USER_WORKSPACE, +} from "../../../../../app-routing.constant"; +import { isDefined } from "../../../../../common/util/predicate"; + +// @UntilDestroy() +@Component({ + selector: "texera-card-item", + templateUrl: "./card-item.component.html", + styleUrls: ["./card-item.component.scss"], +}) +export class CardItemComponent implements OnChanges { + private owners: number[] = []; + public originalName: string = ""; + public originalDescription: string | undefined = undefined; + public disableDelete: boolean = false; + @Input() currentUid: number | undefined; + @ViewChild("nameInput") nameInput!: ElementRef; + @ViewChild("descriptionInput") descriptionInput!: ElementRef; + editingName = false; + editingDescription = false; + likeCount: number = 0; + viewCount = 0; + entryLink: string[] = []; + size: number | undefined = 0; + public iconType: string = ""; + isLiked: boolean = false; + @Input() isPrivateSearch = false; + @Input() editable = false; + private _entry?: DashboardEntry; + hovering: boolean = false; + + @Input() + get entry(): DashboardEntry { + if (!this._entry) { + throw new Error("entry property must be provided."); + } + return this._entry; + } + + set entry(value: DashboardEntry) { + this._entry = value; + } + + @Output() checkboxChanged = new EventEmitter(); + @Output() deleted = new EventEmitter(); + @Output() duplicated = new EventEmitter(); + @Output() refresh = new EventEmitter(); + + constructor( + private modalService: NzModalService, + private workflowPersistService: WorkflowPersistService, + private datasetService: DatasetService, + private modal: NzModalService, + private hubService: HubService, + private downloadService: DownloadService, + private cdr: ChangeDetectorRef, + private notificationService: NotificationService + ) {} + + initializeEntry() { + if (this.entry.type === "workflow") { + if (typeof this.entry.id === "number") { + this.disableDelete = !this.entry.workflow.isOwner; + this.owners = this.entry.accessibleUserIds; + if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) { + this.entryLink = [DASHBOARD_USER_WORKSPACE, String(this.entry.id)]; + } else { + this.entryLink = [DASHBOARD_HUB_WORKFLOW_RESULT_DETAIL, String(this.entry.id)]; + } + this.size = this.entry.size; + } + this.iconType = "project"; + } else if (this.entry.type === "project") { + this.entryLink = [DASHBOARD_USER_PROJECT, String(this.entry.id)]; + this.iconType = "container"; + } else if (this.entry.type === "dataset") { + if (typeof this.entry.id === "number") { + this.disableDelete = !this.entry.dataset.isOwner; + this.owners = this.entry.accessibleUserIds; + if (this.currentUid !== undefined && this.owners.includes(this.currentUid)) { + this.entryLink = [DASHBOARD_USER_DATASET, String(this.entry.id)]; + } else { + this.entryLink = [DASHBOARD_HUB_DATASET_RESULT_DETAIL, String(this.entry.id)]; + } + this.iconType = "database"; + this.size = this.entry.size; + } + } else if (this.entry.type === "file") { + // not sure where to redirect + this.iconType = "folder-open"; + } else { + throw new Error("Unexpected type in DashboardEntry."); + } + this.likeCount = this.entry.likeCount; + this.viewCount = this.entry.viewCount; + this.isLiked = this.entry.isLiked; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes["entry"]) { + this.initializeEntry(); + } + } + + onCheckboxChange(entry: DashboardEntry): void { + entry.checked = !entry.checked; + this.cdr.markForCheck(); + this.checkboxChanged.emit(); + } + + public async onClickOpenShareAccess(): Promise { + let modal: NzModalRef | undefined; + + if (this.entry.type === "workflow") { + modal = this.modalService.create({ + nzContent: ShareAccessComponent, + nzData: { + writeAccess: this.entry.workflow.accessLevel === "WRITE", + type: this.entry.type, + id: this.entry.id, + allOwners: await firstValueFrom(this.workflowPersistService.retrieveOwners()), + inWorkspace: false, + }, + nzFooter: null, + nzTitle: "Share this workflow with others", + nzCentered: true, + nzWidth: "700px", + }); + } else if (this.entry.type === "dataset") { + modal = this.modalService.create({ + nzContent: ShareAccessComponent, + nzData: { + writeAccess: this.entry.accessLevel === "WRITE", + type: "dataset", + id: this.entry.id, + allOwners: await firstValueFrom(this.datasetService.retrieveOwners()), + }, + nzFooter: null, + nzTitle: "Share this dataset with others", + nzCentered: true, + nzWidth: "700px", + }); + } + if (modal) { + modal.componentInstance?.refresh.pipe(untilDestroyed(this)).subscribe(() => { + this.refresh.emit(); + }); + } + } + + public onClickDownload = (): void => { + if (!this.entry.id) return; + + if (this.entry.type === "workflow") { + this.downloadService + .downloadWorkflow(this.entry.id, this.entry.workflow.workflow.name) + .pipe(untilDestroyed(this)) + .subscribe(); + } else if (this.entry.type === "dataset") { + this.downloadService.downloadDataset(this.entry.id, this.entry.name).pipe(untilDestroyed(this)).subscribe(); + } + }; + + onEditName(): void { + this.originalName = this.entry.name; + this.editingName = true; + setTimeout(() => { + if (this.nameInput) { + const inputElement = this.nameInput.nativeElement; + const valueLength = inputElement.value.length; + inputElement.focus(); + inputElement.setSelectionRange(valueLength, valueLength); + } + }, 0); + } + + onEditDescription(): void { + this.originalDescription = this.entry.description; + this.editingDescription = true; + setTimeout(() => { + if (this.descriptionInput) { + const textareaElement = this.descriptionInput.nativeElement; + const valueLength = textareaElement.value.length; + textareaElement.focus(); + textareaElement.setSelectionRange(valueLength, valueLength); + } + }, 0); + } + + private updateProperty( + updateMethod: (id: number, value: string) => any, + propertyName: "name" | "description", + newValue: string, + originalValue: string | undefined + ): void { + if (!this.entry.id) { + this.notificationService.error("Id is missing"); + return; + } + + updateMethod(this.entry.id, newValue) + .pipe(untilDestroyed(this)) + .subscribe({ + next: () => { + this.entry[propertyName] = newValue; // Dynamic property assignment + }, + error: () => { + this.notificationService.error("Update failed"); + (this.entry as any)[propertyName] = originalValue ?? ""; // Fallback to original value + this.setEditingState(propertyName, false); + }, + complete: () => { + this.setEditingState(propertyName, false); + }, + }); + } + + private setEditingState(propertyName: "name" | "description", state: boolean): void { + if (propertyName === "name") { + this.editingName = state; + } else if (propertyName === "description") { + this.editingDescription = state; + } + } + + public confirmUpdateCustomName(name: string): void { + const newName = this.entry.type === "workflow" ? name || DEFAULT_WORKFLOW_NAME : name || DEFAULT_DATASET_NAME; + + if (this.entry.type === "workflow") { + this.updateProperty( + this.workflowPersistService.updateWorkflowName.bind(this.workflowPersistService), + "name", + newName, + this.originalName + ); + } else if (this.entry.type === "dataset") { + this.updateProperty( + this.datasetService.updateDatasetName.bind(this.datasetService), + "name", + newName, + this.originalName + ); + } + } + + public confirmUpdateCustomDescription(description: string | undefined): void { + const updatedDescription = description ?? ""; + + if (this.entry.type === "workflow") { + this.updateProperty( + this.workflowPersistService.updateWorkflowDescription.bind(this.workflowPersistService), + "description", + updatedDescription, + this.originalDescription + ); + } else if (this.entry.type === "dataset") { + this.updateProperty( + this.datasetService.updateDatasetDescription.bind(this.datasetService), + "description", + updatedDescription, + this.originalDescription + ); + } + } + + formatTime(timestamp: number | undefined): string { + if (timestamp === undefined) { + return "Unknown"; // Return "Unknown" if the timestamp is undefined + } + + const currentTime = new Date().getTime(); + const timeDifference = currentTime - timestamp; + + const minutesAgo = Math.floor(timeDifference / (1000 * 60)); + const hoursAgo = Math.floor(timeDifference / (1000 * 60 * 60)); + const daysAgo = Math.floor(timeDifference / (1000 * 60 * 60 * 24)); + const weeksAgo = Math.floor(daysAgo / 7); + + if (minutesAgo < 60) { + return `${minutesAgo} minutes ago`; + } else if (hoursAgo < 24) { + return `${hoursAgo} hours ago`; + } else if (daysAgo < 7) { + return `${daysAgo} days ago`; + } else if (weeksAgo < 4) { + return `${weeksAgo} weeks ago`; + } else { + return new Date(timestamp).toLocaleDateString(); + } + } + + openDetailModal(wid: number | undefined): void { + const modalRef = this.modal.create({ + nzTitle: "Workflow Detail", + nzContent: HubWorkflowDetailComponent, + nzData: { + wid: wid ?? 0, + }, + nzFooter: null, + nzStyle: { width: "60%" }, + nzBodyStyle: { maxHeight: "70vh", overflow: "auto" }, + }); + + const instance = modalRef.componentInstance; + if (instance) { + if (wid !== undefined) { + this.hubService + .getCounts([this.entry.type], [wid], [ActionType.View]) + .pipe(untilDestroyed(this)) + .subscribe(counts => { + const count = counts[0]; + this.viewCount = (count?.counts.view ?? 0) + 1; // hacky fix to display view correctly + }); + } + } + } + + toggleLike(): void { + const userId = this.currentUid; + if (!isDefined(userId) || !isDefined(this.entry.id)) { + return; + } + + const entryId = this.entry.id!; + + if (this.isLiked) { + this.hubService + .postUnlike(entryId, this.entry.type) + .pipe(untilDestroyed(this)) + .subscribe((success: boolean) => { + if (success) { + this.isLiked = false; + this.hubService + .getCounts([this.entry.type], [entryId], [ActionType.Like]) + .pipe(untilDestroyed(this)) + .subscribe(counts => { + this.likeCount = counts[0].counts.like ?? 0; + }); + } + }); + } else { + this.hubService + .postLike(entryId, this.entry.type) + .pipe(untilDestroyed(this)) + .subscribe((success: boolean) => { + if (success) { + this.isLiked = true; + this.hubService + .getCounts([this.entry.type], [entryId], [ActionType.Like]) + .pipe(untilDestroyed(this)) + .subscribe(counts => { + this.likeCount = counts[0].counts.like ?? 0; + }); + } + }); + } + } + + formatCount(count: number): string { + if (count >= 1000) { + return (count / 1000).toFixed(1) + "k"; + } + return count.toString(); + } + + // alias for formatSize + formatSize = formatSize; +} From 394042935297d47796a235ad7d30cc82edd8d868 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 14 Feb 2026 14:02:39 -0800 Subject: [PATCH 2/8] added card view to workflow dashboard --- .../search-results.component.ts | 1 + .../user-workflow.component.html | 21 +++++++++++++++++++ .../user-workflow/user-workflow.component.ts | 1 + 3 files changed, 23 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/search-results/search-results.component.ts b/frontend/src/app/dashboard/component/user/search-results/search-results.component.ts index f120ae8adc8..2c6702cdf44 100644 --- a/frontend/src/app/dashboard/component/user/search-results/search-results.component.ts +++ b/frontend/src/app/dashboard/component/user/search-results/search-results.component.ts @@ -40,6 +40,7 @@ export class SearchResultsComponent { @Input() editable = false; @Input() searchKeywords: string[] = []; @Input() currentUid: number | undefined; + @Input() viewType: "list" | "card" = "list"; @Output() deleted = new EventEmitter(); @Output() duplicated = new EventEmitter(); @Output() modified = new EventEmitter(); diff --git a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.html b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.html index d9d56bbda04..be8513b90a5 100644 --- a/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.html +++ b/frontend/src/app/dashboard/component/user/user-workflow/user-workflow.component.html @@ -131,6 +131,26 @@

Workflows

nzTheme="outline" nzType="minus-square"> + + Workflows Date: Sat, 14 Feb 2026 14:02:59 -0800 Subject: [PATCH 3/8] added card view to workflow dashboard --- .../search-results.component.html | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/dashboard/component/user/search-results/search-results.component.html b/frontend/src/app/dashboard/component/user/search-results/search-results.component.html index d623b981532..94b6cac8538 100644 --- a/frontend/src/app/dashboard/component/user/search-results/search-results.component.html +++ b/frontend/src/app/dashboard/component/user/search-results/search-results.component.html @@ -27,7 +27,7 @@ - + + +
+ + + + +
From 59381e653e128946976ca8df3add904040700b14 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 14 Feb 2026 14:18:59 -0800 Subject: [PATCH 4/8] format backend code --- .../card-item/card-item.component.html | 178 +++++------------- 1 file changed, 49 insertions(+), 129 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html index 199bf7e22c9..93c493a14c6 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html @@ -1,183 +1,103 @@ - + -
+
-
- +
+
-
+
- +
-
+
{{ entry.name }}
- +
-
-
-
+
+
{{ entry.description ? entry.description : (hovering ? 'Write a description...' : '') }}
-
- +
- + {{ entry.ownerName || 'User' }}
-
{{ formatTime(entry.lastModifiedTime) }}
+
+
+ {{ formatTime(entry.creationTime) + }} + {{ formatTime(entry.lastModifiedTime) + }} +
+
+ {{ formatSize(size) }} + {{ formatCount(viewCount) }} +
+
- -
- - - - -
- + \ No newline at end of file From 3b28f6b1ae491bb171f9464bde5ee23956b3293c Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 14 Feb 2026 14:45:03 -0800 Subject: [PATCH 5/8] chaged design and layout of card view (added metadata) --- .../card-item/card-item.component.html | 209 ++++++++++++++---- .../card-item/card-item.component.scss | 56 +++-- .../card-item/card-item.component.ts | 8 + .../search-results.component.scss | 14 ++ 4 files changed, 228 insertions(+), 59 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html index 93c493a14c6..4329d4cde78 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html @@ -1,65 +1,138 @@ - + -
+
-
- +
+ +
+ +
+
-
+
- +
-
+
{{ entry.name }}
- +
-
-
-
+
+
{{ entry.description ? entry.description : (hovering ? 'Write a description...' : '') }}
-
-
- - {{ entry.ownerName || 'User' }} -
- {{ formatTime(entry.creationTime) - }} - {{ formatTime(entry.lastModifiedTime) - }} + + {{ formatTime(entry.creationTime) }} + + {{ formatSize(size) }}
- {{ formatSize(size) }} - {{ formatCount(viewCount) }} + + {{ formatTime(entry.lastModifiedTime) }} + + {{ formatCount(viewCount) }}
@@ -67,37 +140,83 @@
- -
- - - - -
- \ No newline at end of file + diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss index bad6d27c588..af390efd513 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss @@ -5,6 +5,7 @@ flex-direction: column; transition: box-shadow 0.2s; overflow: hidden; + border-radius: 8px; &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); @@ -14,10 +15,16 @@ border-color: #1e90ff; background-color: #e6f7ff; } + + ::ng-deep .ant-card-body { + height: 100%; + display: flex; + flex-direction: column; + } } .card-preview { - height: 150px; + height: 130px; background-color: #f5f5f5; position: relative; cursor: pointer; @@ -35,6 +42,13 @@ z-index: 10; } +.card-user-avatar { + position: absolute; + top: 8px; + right: 8px; + z-index: 10; +} + .card-content { padding: 12px; display: flex; @@ -90,46 +104,60 @@ } .card-description { - flex: 1; - min-height: 40px; /* Ensure 2 lines roughly */ + flex: 0 0 auto; + /* Do not grow */ + height: 60px; + /* Fixed height */ cursor: pointer; + overflow: hidden; + margin-bottom: 8px; } .resource-description { font-size: 13px; color: rgba(0, 0, 0, 0.45); display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 3; + line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; + height: 100%; } .resource-description-edit-textarea { width: 100%; + height: 100%; resize: none; font-size: 13px; + border: 1px solid #d9d9d9; + border-radius: 4px; } .card-meta { display: flex; - align-items: center; - justify-content: space-between; + flex-direction: column; + gap: 8px; font-size: 12px; color: rgba(0, 0, 0, 0.45); margin-top: auto; } -.owner-info { +.meta-details { display: flex; - align-items: center; - gap: 6px; - max-width: 60%; + flex-direction: column; + gap: 4px; } -.owner-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.meta-row { + display: flex; + justify-content: space-between; + align-items: center; + + span { + display: flex; + align-items: center; + gap: 4px; + } } .card-actions { diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts index fecdd59236d..bbb9f67a1ea 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.ts @@ -256,6 +256,10 @@ export class CardItemComponent implements OnChanges { } public confirmUpdateCustomName(name: string): void { + if (this.entry.name === this.originalName) { + this.editingName = false; + return; + } const newName = this.entry.type === "workflow" ? name || DEFAULT_WORKFLOW_NAME : name || DEFAULT_DATASET_NAME; if (this.entry.type === "workflow") { @@ -276,6 +280,10 @@ export class CardItemComponent implements OnChanges { } public confirmUpdateCustomDescription(description: string | undefined): void { + if (this.entry.description === this.originalDescription) { + this.editingDescription = false; + return; + } const updatedDescription = description ?? ""; if (this.entry.type === "workflow") { diff --git a/frontend/src/app/dashboard/component/user/search-results/search-results.component.scss b/frontend/src/app/dashboard/component/user/search-results/search-results.component.scss index 73ccb3127ae..003a5b3db09 100644 --- a/frontend/src/app/dashboard/component/user/search-results/search-results.component.scss +++ b/frontend/src/app/dashboard/component/user/search-results/search-results.component.scss @@ -130,3 +130,17 @@ nz-content { margin: 0 1rem 0 0; // add space to the right } } + +.card-grid-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 16px; + padding: 8px; +} + +.workflow-card-item { + ::ng-deep .card-item { + aspect-ratio: 10/12; + margin-bottom: 0; + } +} From 8a60fedcb7e8b28f9bf2b699ab6e0601075eaecf Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 14 Feb 2026 15:07:06 -0800 Subject: [PATCH 6/8] added placeholder image until static snapshots can be captured for preview --- .../user/list-item/card-item/card-item.component.html | 5 +++++ .../user/list-item/card-item/card-item.component.scss | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html index 4329d4cde78..e8c2dd096d9 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html @@ -28,6 +28,11 @@ [userName]="entry.ownerName || 'User'" [isOwner]="entry.ownerId === this.currentUid">
+ + Workflow Preview
diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss index af390efd513..43382b8a8a2 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.scss @@ -35,6 +35,13 @@ } } +.card-preview-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + .card-checkbox { position: absolute; top: 8px; From f6f4fb18c4c61c082e2c9ca97a80232a696ba2e1 Mon Sep 17 00:00:00 2001 From: Matthew Ball Date: Mon, 16 Feb 2026 03:38:36 -0800 Subject: [PATCH 7/8] Added valid license header to card-item scripts --- .../card-item/card-item.component.html | 7 +++++++ .../card-item/card-item.component.scss | 18 ++++++++++++++++++ .../card-item/card-item.component.ts | 19 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html index e8c2dd096d9..e6eaf6b5c4a 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html @@ -1,3 +1,10 @@ +# 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. Date: Mon, 16 Feb 2026 14:01:50 -0800 Subject: [PATCH 8/8] fixed license in card-item.component.html --- .../card-item/card-item.component.html | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html index e6eaf6b5c4a..e164413c08d 100644 --- a/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html +++ b/frontend/src/app/dashboard/component/user/list-item/card-item/card-item.component.html @@ -1,10 +1,21 @@ -# 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. +