From 52dac0196c31eb8f22d421c26ff2c3a47540440e Mon Sep 17 00:00:00 2001
From: Xuan Gu <162244362+xuang7@users.noreply.github.com>
Date: Sun, 24 May 2026 22:41:13 -0700
Subject: [PATCH 1/4] update.
---
.../dashboard/DatasetSearchQueryBuilder.scala | 3 +-
.../dashboard/UnifiedResourceSchema.scala | 6 +-
frontend/src/app/common/util/format.util.ts | 37 ++++
.../search-results.component.html | 83 +++++---
.../search-results.component.scss | 18 ++
.../search-results.component.ts | 6 +
.../user-dataset-card-item.component.html | 139 ++++++++++++++
.../user-dataset-card-item.component.scss | 181 ++++++++++++++++++
.../user-dataset-card-item.component.spec.ts | 141 ++++++++++++++
.../user-dataset-card-item.component.ts | 173 +++++++++++++++++
.../user-dataset/user-dataset.component.html | 27 +++
.../user-dataset/user-dataset.component.scss | 28 +++
.../user-dataset/user-dataset.component.ts | 17 ++
.../user/dashboard-entry-actions.service.ts | 58 ++++++
frontend/src/app/hub/service/hub.service.ts | 21 ++
15 files changed, 905 insertions(+), 33 deletions(-)
create mode 100644 frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.html
create mode 100644 frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.scss
create mode 100644 frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.spec.ts
create mode 100644 frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.ts
create mode 100644 frontend/src/app/dashboard/service/user/dashboard-entry-actions.service.ts
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DatasetSearchQueryBuilder.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DatasetSearchQueryBuilder.scala
index 89fe805d58c..64c8c311068 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DatasetSearchQueryBuilder.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/DatasetSearchQueryBuilder.scala
@@ -48,7 +48,8 @@ object DatasetSearchQueryBuilder extends SearchQueryBuilder with LazyLogging {
repositoryName = DATASET.REPOSITORY_NAME,
isDatasetPublic = DATASET.IS_PUBLIC,
isDatasetDownloadable = DATASET.IS_DOWNLOADABLE,
- datasetUserAccess = DATASET_USER_ACCESS.PRIVILEGE
+ datasetUserAccess = DATASET_USER_ACCESS.PRIVILEGE,
+ datasetCoverImage = DATASET.COVER_IMAGE
)
/*
diff --git a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
index dbcf1926407..8c4ecda946b 100644
--- a/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
+++ b/amber/src/main/scala/org/apache/texera/web/resource/dashboard/UnifiedResourceSchema.scala
@@ -72,7 +72,8 @@ object UnifiedResourceSchema {
repositoryName: Field[String] = DSL.inline(""),
isDatasetPublic: Field[java.lang.Boolean] = DSL.cast(null, classOf[java.lang.Boolean]),
isDatasetDownloadable: Field[java.lang.Boolean] = DSL.cast(null, classOf[java.lang.Boolean]),
- datasetUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum])
+ datasetUserAccess: Field[PrivilegeEnum] = DSL.castNull(classOf[PrivilegeEnum]),
+ datasetCoverImage: Field[String] = DSL.cast(null, classOf[String])
): UnifiedResourceSchema = {
new UnifiedResourceSchema(
Seq(
@@ -96,7 +97,8 @@ object UnifiedResourceSchema {
repositoryName -> repositoryName.as("repository_name"),
isDatasetPublic -> isDatasetPublic.as("is_dataset_public"),
isDatasetDownloadable -> isDatasetDownloadable.as("is_dataset_downloadable"),
- datasetUserAccess -> datasetUserAccess.as("user_dataset_access")
+ datasetUserAccess -> datasetUserAccess.as("user_dataset_access"),
+ datasetCoverImage -> datasetCoverImage.as("cover_image")
)
)
}
diff --git a/frontend/src/app/common/util/format.util.ts b/frontend/src/app/common/util/format.util.ts
index 2ac5f229796..8de69332d59 100644
--- a/frontend/src/app/common/util/format.util.ts
+++ b/frontend/src/app/common/util/format.util.ts
@@ -54,3 +54,40 @@ export const formatTime = (seconds?: number): string => {
return min === 0 ? `${h}h` : `${h}h${min}m`;
};
+
+/** Format a count: "1.5k" for >= 1000, otherwise the number. */
+export const formatCount = (count: number): string => {
+ if (count >= 1000) return (count / 1000).toFixed(1) + "k";
+ return String(count);
+};
+
+/** Format a timestamp as relative time ("5 minutes ago", "3 months ago", "1 year ago"). */
+export const formatRelativeTime = (timestamp: number | undefined): string => {
+ if (timestamp === undefined) {
+ return "Unknown";
+ }
+
+ 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);
+ const monthsAgo = Math.floor(daysAgo / 30);
+ const yearsAgo = Math.floor(daysAgo / 365);
+
+ 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 if (monthsAgo < 12) {
+ return `${monthsAgo} months ago`;
+ } else {
+ return `${yearsAgo} years ago`;
+ }
+};
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..e90931c596e 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
@@ -20,36 +20,59 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -55,6 +81,7 @@
Datasets
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.scss b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.scss
index 31c95188c44..3a0cb64df5f 100644
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.scss
+++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.scss
@@ -25,3 +25,31 @@
height: 55px;
overflow-y: hidden;
}
+
+.view-toggle {
+ display: inline-flex;
+ background: #f5f5f5;
+ border-radius: 6px;
+ padding: 2px;
+ margin-left: 8px;
+
+ button {
+ height: 28px;
+ padding: 0 10px;
+ border: none;
+ background: transparent;
+ color: #8c8c8c;
+ border-radius: 4px;
+
+ &:hover {
+ color: #595959;
+ background: rgba(255, 255, 255, 0.6);
+ }
+
+ &.active {
+ background: #fff;
+ color: #1f1f1f;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
+ }
+ }
+}
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts
index 165270c0537..821eeab7aea 100644
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts
+++ b/frontend/src/app/dashboard/component/user/user-dataset/user-dataset.component.ts
@@ -43,6 +43,15 @@ import { NzIconDirective } from "ng-zorro-antd/icon";
import { FiltersInstructionsComponent } from "../filters-instructions/filters-instructions.component";
import { NzSelectComponent } from "ng-zorro-antd/select";
import { FormsModule } from "@angular/forms";
+import { NzTooltipModule } from "ng-zorro-antd/tooltip";
+
+export type ViewMode = "list" | "card";
+export const DATASET_VIEW_MODE_STORAGE_KEY = "texera.dataset.viewMode";
+
+/** Read the persisted view mode, falling back to "list" if absent or invalid. */
+export function readStoredDatasetViewMode(): ViewMode {
+ return localStorage.getItem(DATASET_VIEW_MODE_STORAGE_KEY) === "card" ? "card" : "list";
+}
@UntilDestroy()
@Component({
@@ -56,6 +65,7 @@ import { FormsModule } from "@angular/forms";
NzWaveDirective,
ɵNzTransitionPatchDirective,
NzIconDirective,
+ NzTooltipModule,
FiltersComponent,
FiltersInstructionsComponent,
NzSelectComponent,
@@ -69,6 +79,13 @@ export class UserDatasetComponent implements AfterViewInit {
public isLogin = this.userService.isLogin();
public currentUid = this.userService.getCurrentUser()?.uid;
public hasMismatch = false; // Display warning when there are mismatched datasets
+ public viewMode: ViewMode = readStoredDatasetViewMode();
+
+ setViewMode(mode: ViewMode): void {
+ if (this.viewMode === mode) return;
+ this.viewMode = mode;
+ localStorage.setItem(DATASET_VIEW_MODE_STORAGE_KEY, mode);
+ }
private _searchResultsComponent?: SearchResultsComponent;
@ViewChild(SearchResultsComponent) get searchResultsComponent(): SearchResultsComponent {
diff --git a/frontend/src/app/dashboard/service/user/dashboard-entry-actions.service.ts b/frontend/src/app/dashboard/service/user/dashboard-entry-actions.service.ts
new file mode 100644
index 00000000000..cdd5cc0230a
--- /dev/null
+++ b/frontend/src/app/dashboard/service/user/dashboard-entry-actions.service.ts
@@ -0,0 +1,58 @@
+/**
+ * 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 { Injectable } from "@angular/core";
+import { NzModalRef, NzModalService } from "ng-zorro-antd/modal";
+import { firstValueFrom } from "rxjs";
+import { ShareAccessComponent } from "../../component/user/share-access/share-access.component";
+import { DashboardEntry } from "../../type/dashboard-entry";
+import { DatasetService } from "./dataset/dataset.service";
+import { DownloadService } from "./download/download.service";
+
+/** Dataset actions shared by dashboard list and card views. */
+@Injectable({ providedIn: "root" })
+export class DashboardEntryActionsService {
+ constructor(
+ private modalService: NzModalService,
+ private datasetService: DatasetService,
+ private downloadService: DownloadService
+ ) {}
+
+ async openShareAccess(entry: DashboardEntry): Promise | undefined> {
+ if (entry.type !== "dataset") return undefined;
+ return this.modalService.create({
+ nzContent: ShareAccessComponent,
+ nzData: {
+ writeAccess: entry.accessLevel === "WRITE",
+ type: "dataset",
+ id: entry.id,
+ allOwners: await firstValueFrom(this.datasetService.retrieveOwners()),
+ },
+ nzFooter: null,
+ nzTitle: "Share this dataset with others",
+ nzCentered: true,
+ nzWidth: "700px",
+ });
+ }
+
+ download(entry: DashboardEntry): void {
+ if (entry.type !== "dataset" || !entry.id) return;
+ this.downloadService.downloadDataset(entry.id, entry.name).subscribe();
+ }
+}
diff --git a/frontend/src/app/hub/service/hub.service.ts b/frontend/src/app/hub/service/hub.service.ts
index 66cd08a9149..7979bcbaca6 100644
--- a/frontend/src/app/hub/service/hub.service.ts
+++ b/frontend/src/app/hub/service/hub.service.ts
@@ -20,6 +20,7 @@
import { HttpClient, HttpHeaders, HttpParams } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
+import { map, switchMap } from "rxjs/operators";
import { AppSettings } from "../../common/app-setting";
import { SearchResultItem } from "../../dashboard/type/search-result";
@@ -102,6 +103,26 @@ export class HubService {
});
}
+ /** Like/unlike then fetch updated count; emits the final {liked, likeCount}. */
+ public toggleLike(
+ entityId: number,
+ entityType: EntityType,
+ currentlyLiked: boolean
+ ): Observable<{ liked: boolean; likeCount: number }> {
+ const action$ = currentlyLiked ? this.postUnlike(entityId, entityType) : this.postLike(entityId, entityType);
+ return action$.pipe(
+ switchMap(success =>
+ this.getCounts([entityType], [entityId], [ActionType.Like]).pipe(
+ map(counts => {
+ const likeCount = counts[0]?.counts.like ?? 0;
+ const liked = success ? !currentlyLiked : currentlyLiked;
+ return { liked, likeCount };
+ })
+ )
+ )
+ );
+ }
+
public postView(entityId: number, userId: number, entityType: EntityType): Observable {
const body = { entityId, userId, entityType };
return this.http.post(`${this.BASE_URL}/view`, body, {
From c277c3fa5322e36aac45c06e4825f5c5628e173d Mon Sep 17 00:00:00 2001
From: Xuan Gu <162244362+xuang7@users.noreply.github.com>
Date: Mon, 25 May 2026 18:19:10 -0700
Subject: [PATCH 2/4] add card view to Hub datasets list.
---
.../dataset-card-item.component.html} | 47 +++++++++----
.../dataset-card-item.component.scss} | 70 +++++++++++++++----
.../dataset-card-item.component.spec.ts} | 36 +++++-----
.../dataset-card-item.component.ts} | 65 +++++++++--------
.../search-results.component.html | 15 ++--
.../search-results.component.ts | 9 +--
.../user-dataset/user-dataset.component.html | 27 -------
.../user-dataset/user-dataset.component.scss | 28 --------
.../user-dataset/user-dataset.component.ts | 17 -----
.../user/dashboard-entry-actions.service.ts | 58 ---------------
.../hub-search-result.component.html | 41 ++++++++++-
.../hub-search-result.component.scss | 35 ++++++++++
.../hub-search-result.component.ts | 31 +++++++-
13 files changed, 259 insertions(+), 220 deletions(-)
rename frontend/src/app/dashboard/component/user/{user-dataset/user-dataset-card-item/user-dataset-card-item.component.html => dataset-card-item/dataset-card-item.component.html} (80%)
rename frontend/src/app/dashboard/component/user/{user-dataset/user-dataset-card-item/user-dataset-card-item.component.scss => dataset-card-item/dataset-card-item.component.scss} (77%)
rename frontend/src/app/dashboard/component/user/{user-dataset/user-dataset-card-item/user-dataset-card-item.component.spec.ts => dataset-card-item/dataset-card-item.component.spec.ts} (78%)
rename frontend/src/app/dashboard/component/user/{user-dataset/user-dataset-card-item/user-dataset-card-item.component.ts => dataset-card-item/dataset-card-item.component.ts} (70%)
delete mode 100644 frontend/src/app/dashboard/service/user/dashboard-entry-actions.service.ts
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.html b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.html
similarity index 80%
rename from frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.html
rename to frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.html
index 8aa68163234..0d333bd5e88 100644
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.html
+++ b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.html
@@ -47,38 +47,57 @@
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.scss b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.scss
similarity index 77%
rename from frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.scss
rename to frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.scss
index 0d0fe2bef26..c72c2e50cd0 100644
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.scss
+++ b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.scss
@@ -55,7 +55,7 @@
font-size: 12px;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
- background: rgba(0, 0, 0, 0.55);
+ background: rgba(15, 14, 12, 0.72);
color: white;
}
}
@@ -109,8 +109,7 @@
flex-direction: column;
gap: 5px;
margin-top: auto;
- padding-top: 10px;
- border-top: 1px solid #f0f0f0;
+ padding-top: 4px;
min-width: 0;
.meta-line {
@@ -127,7 +126,6 @@
display: inline-flex;
align-items: center;
gap: 6px;
- flex: 1;
min-width: 0;
}
@@ -144,33 +142,64 @@
font-size: 9px;
}
}
+
+ .meta-dot {
+ flex-shrink: 0;
+ color: #bfbfbf;
+ font-size: 13px;
+ line-height: 1;
+ user-select: none;
+ }
+
+ .meta-updated {
+ flex-shrink: 0;
+ font-size: 12px;
+ color: #8c8c8c;
+ white-space: nowrap;
+ }
}
- &--stats,
- &--activity {
+ &--stats {
justify-content: space-between;
font-size: 12px;
color: #8c8c8c;
}
- &--stats .meta-size {
- white-space: nowrap;
- }
-
- .meta-stat--like {
+ .meta-stat {
display: inline-flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
- cursor: pointer;
+ white-space: nowrap;
i {
font-size: 12px;
}
+ }
+
+ .meta-stat--like {
+ padding: 0 10px;
+ border: 1px solid #e8e8e8;
+ border-radius: 999px;
+ background: transparent;
+ color: inherit;
+ font-size: 12px;
+ gap: 8px;
+ cursor: pointer;
+ transition: border-color 0.15s;
+
+ i {
+ font-size: 11px;
+ transition: color 0.15s;
+ }
+
+ &.liked i,
+ &:not(.disabled):not(.liked):hover i {
+ color: #e0506e;
+ }
- &.liked,
&:not(.disabled):hover {
- color: #ff4d4f;
+ border-color: #e0506e;
}
&.disabled {
@@ -178,4 +207,17 @@
}
}
}
+
+ .meta-stats-left {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 0;
+ }
+
+ .meta-hr {
+ height: 1px;
+ background: #f0f0f0;
+ margin: 2px 0;
+ }
}
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.spec.ts b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts
similarity index 78%
rename from frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.spec.ts
rename to frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts
index 195bef1c323..bc3ffd11a4f 100644
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.spec.ts
+++ b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts
@@ -25,15 +25,17 @@ import { RouterTestingModule } from "@angular/router/testing";
import { of } from "rxjs";
import type { Mocked } from "vitest";
-import { UserDatasetCardItemComponent } from "./user-dataset-card-item.component";
+import { DatasetCardItemComponent } from "./dataset-card-item.component";
import { DashboardEntry } from "src/app/dashboard/type/dashboard-entry";
-import { DashboardEntryActionsService } from "../../../../service/user/dashboard-entry-actions.service";
-import { HubService } from "../../../../../hub/service/hub.service";
-import { UserService } from "../../../../../common/service/user/user.service";
-import { StubUserService } from "../../../../../common/service/user/stub-user.service";
-import { AppSettings } from "../../../../../common/app-setting";
-import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_USER_DATASET } from "../../../../../app-routing.constant";
-import { commonTestProviders } from "../../../../../common/testing/test-utils";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { DatasetService } from "../../../service/user/dataset/dataset.service";
+import { DownloadService } from "../../../service/user/download/download.service";
+import { HubService } from "../../../../hub/service/hub.service";
+import { UserService } from "../../../../common/service/user/user.service";
+import { StubUserService } from "../../../../common/service/user/stub-user.service";
+import { AppSettings } from "../../../../common/app-setting";
+import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_USER_DATASET } from "../../../../app-routing.constant";
+import { commonTestProviders } from "../../../../common/testing/test-utils";
function makeDatasetEntry(overrides: Partial = {}): DashboardEntry {
// Only includes fields read by the component's logic; template fields are skipped
@@ -48,24 +50,22 @@ function makeDatasetEntry(overrides: Partial = {}): DashboardEntry {
} as unknown as DashboardEntry;
}
-describe("UserDatasetCardItemComponent", () => {
- let component: UserDatasetCardItemComponent;
- let fixture: ComponentFixture;
+describe("DatasetCardItemComponent", () => {
+ let component: DatasetCardItemComponent;
+ let fixture: ComponentFixture;
let hubService: Mocked;
beforeEach(async () => {
- const entryActionsSpy = {
- openShareAccess: vi.fn().mockResolvedValue(undefined),
- download: vi.fn(),
- };
const hubServiceSpy = {
toggleLike: vi.fn().mockReturnValue(of({ liked: true, likeCount: 7 })),
};
await TestBed.configureTestingModule({
- imports: [UserDatasetCardItemComponent, HttpClientTestingModule, BrowserAnimationsModule, RouterTestingModule],
+ imports: [DatasetCardItemComponent, HttpClientTestingModule, BrowserAnimationsModule, RouterTestingModule],
providers: [
- { provide: DashboardEntryActionsService, useValue: entryActionsSpy },
+ { provide: NzModalService, useValue: { create: vi.fn() } },
+ { provide: DatasetService, useValue: { retrieveOwners: vi.fn().mockReturnValue(of([])) } },
+ { provide: DownloadService, useValue: { downloadDataset: vi.fn().mockReturnValue(of(new Blob())) } },
{ provide: HubService, useValue: hubServiceSpy },
{ provide: UserService, useClass: StubUserService },
...commonTestProviders,
@@ -73,7 +73,7 @@ describe("UserDatasetCardItemComponent", () => {
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
- fixture = TestBed.createComponent(UserDatasetCardItemComponent);
+ fixture = TestBed.createComponent(DatasetCardItemComponent);
component = fixture.componentInstance;
hubService = TestBed.inject(HubService) as unknown as Mocked;
});
diff --git a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.ts b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts
similarity index 70%
rename from frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.ts
rename to frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts
index 3d01e48bced..7d04e056368 100644
--- a/frontend/src/app/dashboard/component/user/user-dataset/user-dataset-card-item/user-dataset-card-item.component.ts
+++ b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts
@@ -36,21 +36,25 @@ import { NzPopconfirmDirective } from "ng-zorro-antd/popconfirm";
import { NzTooltipModule } from "ng-zorro-antd/tooltip";
import { NzDropdownDirective, NzDropdownMenuComponent } from "ng-zorro-antd/dropdown";
import { NzMenuDirective, NzMenuItemComponent } from "ng-zorro-antd/menu";
-import { DashboardEntry } from "../../../../type/dashboard-entry";
-import { UserAvatarComponent } from "../../user-avatar/user-avatar.component";
-import { DashboardEntryActionsService } from "../../../../service/user/dashboard-entry-actions.service";
-import { HubService } from "../../../../../hub/service/hub.service";
-import { AppSettings } from "../../../../../common/app-setting";
-import { formatSize } from "../../../../../common/util/size-formatter.util";
-import { formatCount, formatRelativeTime } from "../../../../../common/util/format.util";
-import { isDefined } from "../../../../../common/util/predicate";
-import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_USER_DATASET } from "../../../../../app-routing.constant";
+import { NzModalService } from "ng-zorro-antd/modal";
+import { firstValueFrom } from "rxjs";
+import { DashboardEntry } from "../../../type/dashboard-entry";
+import { UserAvatarComponent } from "../user-avatar/user-avatar.component";
+import { ShareAccessComponent } from "../share-access/share-access.component";
+import { DatasetService } from "../../../service/user/dataset/dataset.service";
+import { DownloadService } from "../../../service/user/download/download.service";
+import { HubService } from "../../../../hub/service/hub.service";
+import { AppSettings } from "../../../../common/app-setting";
+import { formatSize } from "../../../../common/util/size-formatter.util";
+import { formatCount, formatRelativeTime } from "../../../../common/util/format.util";
+import { isDefined } from "../../../../common/util/predicate";
+import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_USER_DATASET } from "../../../../app-routing.constant";
@UntilDestroy()
@Component({
selector: "texera-dataset-card-item",
- templateUrl: "./user-dataset-card-item.component.html",
- styleUrls: ["./user-dataset-card-item.component.scss"],
+ templateUrl: "./dataset-card-item.component.html",
+ styleUrls: ["./dataset-card-item.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
NgIf,
@@ -66,24 +70,13 @@ import { DASHBOARD_HUB_DATASET_RESULT_DETAIL, DASHBOARD_USER_DATASET } from "../
UserAvatarComponent,
],
})
-export class UserDatasetCardItemComponent implements OnChanges {
+export class DatasetCardItemComponent implements OnChanges {
@Input() editable = false;
@Input() currentUid: number | undefined;
+ @Input() entry!: DashboardEntry;
@Output() deleted = new EventEmitter();
@Output() refresh = new EventEmitter();
- private _entry?: DashboardEntry;
- @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;
- }
-
entryLink: string[] = [];
coverImageSrc: string = "";
readonly defaultCover = "assets/card_background.jpg";
@@ -92,7 +85,9 @@ export class UserDatasetCardItemComponent implements OnChanges {
isLiked = false;
constructor(
- private entryActions: DashboardEntryActionsService,
+ private modalService: NzModalService,
+ private datasetService: DatasetService,
+ private downloadService: DownloadService,
private hubService: HubService,
private cdr: ChangeDetectorRef
) {}
@@ -130,12 +125,26 @@ export class UserDatasetCardItemComponent implements OnChanges {
}
public async onClickOpenShareAccess(): Promise {
- const modal = await this.entryActions.openShareAccess(this.entry);
- modal?.componentInstance?.refresh.pipe(untilDestroyed(this)).subscribe(() => this.refresh.emit());
+ if (this.entry.type !== "dataset") return;
+ const 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",
+ });
+ modal.componentInstance?.refresh.pipe(untilDestroyed(this)).subscribe(() => this.refresh.emit());
}
public onClickDownload = (): void => {
- this.entryActions.download(this.entry);
+ if (this.entry.type !== "dataset" || !this.entry.id) return;
+ this.downloadService.downloadDataset(this.entry.id, this.entry.name).subscribe();
};
toggleLike(): void {
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 e90931c596e..ff4a31fb7a5 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
@@ -52,18 +52,15 @@
-
-
+
+
+
+
+
+
+
+ [currentUid]="this.currentUid"
+ [viewMode]="searchType === 'dataset' ? viewMode : 'list'"
+ [cardTemplate]="searchType === 'dataset' ? datasetCardTpl : undefined">
diff --git a/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.scss b/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.scss
index e172c5b40bd..15c67772751 100644
--- a/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.scss
+++ b/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.scss
@@ -18,3 +18,38 @@
*/
@import "../../../dashboard/component/user/search/search.component";
+
+.filter {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.view-toggle {
+ display: inline-flex;
+ background: #f5f5f5;
+ border-radius: 6px;
+ padding: 2px;
+ margin-left: auto;
+
+ button {
+ height: 28px;
+ padding: 0 10px;
+ border: none;
+ background: transparent;
+ color: #8c8c8c;
+ border-radius: 4px;
+
+ &:hover {
+ color: #595959;
+ background: rgba(255, 255, 255, 0.6);
+ }
+
+ &.active {
+ background: #fff;
+ color: #1f1f1f;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
+ }
+ }
+}
diff --git a/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.ts b/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.ts
index 067e9205625..1f69bdcff71 100644
--- a/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.ts
+++ b/frontend/src/app/hub/component/hub-search-result/hub-search-result.component.ts
@@ -19,8 +19,16 @@
import { AfterViewInit, Component, Input, OnInit, ViewChild } from "@angular/core";
import { Router } from "@angular/router";
-import { SearchResultsComponent } from "../../../dashboard/component/user/search-results/search-results.component";
+import { NgIf } from "@angular/common";
+import { NzButtonComponent } from "ng-zorro-antd/button";
+import { NzIconDirective } from "ng-zorro-antd/icon";
+import { NzTooltipModule } from "ng-zorro-antd/tooltip";
+import {
+ SearchResultsComponent,
+ SearchResultsViewMode,
+} from "../../../dashboard/component/user/search-results/search-results.component";
import { FiltersComponent } from "../../../dashboard/component/user/filters/filters.component";
+import { DatasetCardItemComponent } from "../../../dashboard/component/user/dataset-card-item/dataset-card-item.component";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { SortMethod } from "../../../dashboard/type/sort-method";
import { UserService } from "../../../common/service/user/user.service";
@@ -30,17 +38,36 @@ import { firstValueFrom } from "rxjs";
import { map } from "rxjs/operators";
import { SortButtonComponent } from "../../../dashboard/component/user/sort-button/sort-button.component";
+const HUB_DATASET_VIEW_MODE_STORAGE_KEY = "texera.hub.dataset.viewMode";
+
@UntilDestroy()
@Component({
selector: "texera-hub-search",
templateUrl: "./hub-search-result.component.html",
styleUrls: ["./hub-search-result.component.scss"],
- imports: [SortButtonComponent, FiltersComponent, SearchResultsComponent],
+ imports: [
+ NgIf,
+ NzButtonComponent,
+ NzIconDirective,
+ NzTooltipModule,
+ SortButtonComponent,
+ FiltersComponent,
+ SearchResultsComponent,
+ DatasetCardItemComponent,
+ ],
})
export class HubSearchResultComponent implements OnInit, AfterViewInit {
public searchType: "dataset" | "workflow" = "workflow";
public searchKeywords: string[] = [];
currentUid = this.userService.getCurrentUser()?.uid;
+ public viewMode: SearchResultsViewMode =
+ localStorage.getItem(HUB_DATASET_VIEW_MODE_STORAGE_KEY) === "card" ? "card" : "list";
+
+ setViewMode(mode: SearchResultsViewMode): void {
+ if (this.viewMode === mode) return;
+ this.viewMode = mode;
+ localStorage.setItem(HUB_DATASET_VIEW_MODE_STORAGE_KEY, mode);
+ }
private isLogin = false;
private includePublic = true;
From b386fccdd4d6891cb6b1147fd09c79de5c72f3b9 Mon Sep 17 00:00:00 2001
From: Xuan Gu <162244362+xuang7@users.noreply.github.com>
Date: Mon, 25 May 2026 18:22:01 -0700
Subject: [PATCH 3/4] update.
---
.../user/search-results/search-results.component.html | 4 ++++
1 file changed, 4 insertions(+)
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 ff4a31fb7a5..084ccb0d9fc 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
@@ -22,6 +22,10 @@
[nzBodyStyle]="{ height: '100%'}">
+
From 8f3282f900f9dea14664394d888536222b62561c Mon Sep 17 00:00:00 2001
From: Xuan Gu <162244362+xuang7@users.noreply.github.com>
Date: Mon, 25 May 2026 18:52:05 -0700
Subject: [PATCH 4/4] lint.
---
.../user/dataset-card-item/dataset-card-item.component.spec.ts | 2 --
.../user/dataset-card-item/dataset-card-item.component.ts | 2 +-
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts
index bc3ffd11a4f..f7261b9e772 100644
--- a/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts
+++ b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.spec.ts
@@ -18,7 +18,6 @@
*/
import { ComponentFixture, TestBed } from "@angular/core/testing";
-import { NO_ERRORS_SCHEMA } from "@angular/core";
import { HttpClientTestingModule } from "@angular/common/http/testing";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { RouterTestingModule } from "@angular/router/testing";
@@ -70,7 +69,6 @@ describe("DatasetCardItemComponent", () => {
{ provide: UserService, useClass: StubUserService },
...commonTestProviders,
],
- schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
fixture = TestBed.createComponent(DatasetCardItemComponent);
diff --git a/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts
index 7d04e056368..c5d33bbcbe5 100644
--- a/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts
+++ b/frontend/src/app/dashboard/component/user/dataset-card-item/dataset-card-item.component.ts
@@ -144,7 +144,7 @@ export class DatasetCardItemComponent implements OnChanges {
public onClickDownload = (): void => {
if (this.entry.type !== "dataset" || !this.entry.id) return;
- this.downloadService.downloadDataset(this.entry.id, this.entry.name).subscribe();
+ this.downloadService.downloadDataset(this.entry.id, this.entry.name).pipe(untilDestroyed(this)).subscribe();
};
toggleLike(): void {