Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ object UnifiedResourceSchema {
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]),
datasetCoverImage: Field[String] = DSL.cast(null, classOf[String])
datasetCoverImage: Field[String] = DSL.cast(null, classOf[String]),
workflowCoverImage: Field[String] = DSL.cast(null, classOf[String])
): UnifiedResourceSchema = {
new UnifiedResourceSchema(
Seq(
Expand All @@ -102,7 +103,8 @@ object UnifiedResourceSchema {
isDatasetPublic -> isDatasetPublic.as("is_dataset_public"),
isDatasetDownloadable -> isDatasetDownloadable.as("is_dataset_downloadable"),
datasetUserAccess -> datasetUserAccess.as("user_dataset_access"),
datasetCoverImage -> datasetCoverImage.as("cover_image")
datasetCoverImage -> datasetCoverImage.as("cover_image"),
workflowCoverImage -> workflowCoverImage.as("workflow_cover_image")
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
uid = WORKFLOW_OF_USER.UID,
ownerId = WORKFLOW_OF_USER.UID,
userName = USER.NAME,
projectsOfWorkflow = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID)
projectsOfWorkflow = groupConcatDistinct(WORKFLOW_OF_PROJECT.PID),
workflowCoverImage = DSL.max(WORKFLOW_COVER_IMAGE.IMAGE).as("workflow_cover_image")
)
}

Expand All @@ -72,6 +73,8 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
.on(WORKFLOW_OF_PROJECT.WID.eq(WORKFLOW.WID))
.leftJoin(PROJECT_USER_ACCESS)
.on(PROJECT_USER_ACCESS.PID.eq(WORKFLOW_OF_PROJECT.PID))
.leftJoin(WORKFLOW_COVER_IMAGE)
.on(WORKFLOW_COVER_IMAGE.WID.eq(WORKFLOW.WID))

var condition: Condition = DSL.trueCondition()
if (uid == null) {
Expand Down Expand Up @@ -162,7 +165,8 @@ object WorkflowSearchQueryBuilder extends SearchQueryBuilder {
.map(number => Integer.valueOf(number))
.toList
},
record.into(USER).getUid
record.into(USER).getUid,
Option(record.get("workflow_cover_image", classOf[String]))
)
DashboardClickableFileEntry(SearchQueryBuilder.WORKFLOW_RESOURCE_TYPE, workflow = Some(dw))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import org.apache.texera.web.resource.dashboard.hub.EntityType
import org.apache.texera.web.resource.dashboard.hub.HubResource.recordCloneAction
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowAccessResource.hasReadAccess
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource._
import org.jooq.impl.DSL.{groupConcatDistinct, noCondition}
import org.jooq.{Condition, DSLContext, Record9, Result, SelectOnConditionStep}
import org.jooq.impl.DSL.{groupConcatDistinct, noCondition, max}
import org.jooq.{Condition, DSLContext, Record10, Result, SelectOnConditionStep}

import java.sql.Timestamp
import java.util
Expand Down Expand Up @@ -125,7 +125,8 @@ object WorkflowResource {
ownerName: String,
workflow: Workflow,
projectIDs: List[Integer],
ownerId: Integer
ownerId: Integer,
coverImage: Option[String]
)

case class WorkflowWithPrivilege(
Expand Down Expand Up @@ -191,7 +192,7 @@ object WorkflowResource {
}
}

def baseWorkflowSelect(): SelectOnConditionStep[Record9[
def baseWorkflowSelect(): SelectOnConditionStep[Record10[
Integer,
String,
String,
Expand All @@ -200,6 +201,7 @@ object WorkflowResource {
PrivilegeEnum,
Integer,
String,
String,
String
]] = {
context
Expand All @@ -212,7 +214,8 @@ object WorkflowResource {
WORKFLOW_USER_ACCESS.PRIVILEGE,
WORKFLOW_OF_USER.UID,
USER.NAME,
groupConcatDistinct(WORKFLOW_OF_PROJECT.PID).as("projects")
groupConcatDistinct(WORKFLOW_OF_PROJECT.PID).as("projects"),
max(WORKFLOW_COVER_IMAGE.IMAGE).as("cover_image")
)
.from(WORKFLOW)
.leftJoin(WORKFLOW_USER_ACCESS)
Expand All @@ -223,10 +226,12 @@ object WorkflowResource {
.on(USER.UID.eq(WORKFLOW_OF_USER.UID))
.leftJoin(WORKFLOW_OF_PROJECT)
.on(WORKFLOW.WID.eq(WORKFLOW_OF_PROJECT.WID))
.leftJoin(WORKFLOW_COVER_IMAGE)
.on(WORKFLOW.WID.eq(WORKFLOW_COVER_IMAGE.WID))
}

def mapWorkflowEntries(
workflowEntries: Result[Record9[
workflowEntries: Result[Record10[
Integer,
String,
String,
Expand All @@ -235,6 +240,7 @@ object WorkflowResource {
PrivilegeEnum,
Integer,
String,
String,
String
]],
uid: Integer
Expand All @@ -255,7 +261,8 @@ object WorkflowResource {
if (workflowRecord.component9() == null) List[Integer]()
else
workflowRecord.component9().split(',').map(str => Integer.valueOf(str)).toList,
workflowRecord.into(WORKFLOW_OF_USER).getUid
workflowRecord.into(WORKFLOW_OF_USER).getUid,
Option(workflowRecord.get("cover_image", classOf[String]))
)
)
.asScala
Expand Down Expand Up @@ -584,7 +591,8 @@ class WorkflowResource extends LazyLogging {
user.getName,
workflowDao.fetchOneByWid(workflow.getWid),
List[Integer](),
user.getUid
user.getUid,
None
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.apache.texera.dao.jooq.generated.enums.UserRoleEnum
import org.apache.texera.dao.jooq.generated.tables.daos.UserDao
import org.apache.texera.dao.jooq.generated.tables.pojos.{Project, User, Workflow}
import org.apache.texera.web.resource.dashboard.DashboardResource.SearchQueryParams
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.CoverImageRequest
import org.apache.texera.web.resource.dashboard.user.project.ProjectResource
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource
import org.apache.texera.web.resource.dashboard.user.workflow.WorkflowResource.{
Expand Down Expand Up @@ -806,4 +807,40 @@ class WorkflowResourceSpec
assert(resources.results.length == 3)
}

it should "include workflow cover image in search results" in {
// Create workflow
workflowResource.persistWorkflow(testWorkflow1, sessionUser1)

// Set cover image
val workflowId =
workflowResource.retrieveWorkflowsBySessionUser(sessionUser1).head.workflow.getWid

val coverImage = "data:image/jpeg;base64,/9j/4AAQSkZJRg=="
workflowResource.setCoverImage(
workflowId,
CoverImageRequest(coverImage),
sessionUser1
)

// Search workflows
val results =
dashboardResource.searchAllResourcesCall(
sessionUser1,
SearchQueryParams(resourceType = "workflow")
)

// Verify cover image is included in response
assert(results.results.length == 1)

val workflowEntry = results.results.head.workflow.get
assert(workflowEntry.coverImage.contains(coverImage))
}

it should "create a workflow with coverImage set to None" in {
val result = workflowResource.createWorkflow(testWorkflow1, sessionUser1)

assert(result.workflow.getName == "test_workflow1")
assert(result.coverImage.isEmpty)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export const testWorkflowFileNameConflictEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [1],
ownerId: 1,
coverImage: null,
}),
new DashboardEntry({
workflow: testDownloadWorkflow2,
Expand All @@ -146,6 +147,7 @@ export const testWorkflowFileNameConflictEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [1, 2],
ownerId: 1,
coverImage: null,
}),
new DashboardEntry({
workflow: testDownloadWorkflow3,
Expand All @@ -154,6 +156,7 @@ export const testWorkflowFileNameConflictEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [1],
ownerId: 2,
coverImage: null,
}),
];

Expand All @@ -165,6 +168,7 @@ export const testWorkflowEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [1],
ownerId: 1,
coverImage: null,
}),
new DashboardEntry({
workflow: testWorkflow2,
Expand All @@ -173,6 +177,7 @@ export const testWorkflowEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [1, 2],
ownerId: 1,
coverImage: null,
}),
new DashboardEntry({
workflow: testWorkflow3,
Expand All @@ -181,6 +186,7 @@ export const testWorkflowEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [1],
ownerId: 2,
coverImage: null,
}),
new DashboardEntry({
workflow: testWorkflow4,
Expand All @@ -189,6 +195,7 @@ export const testWorkflowEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [3],
ownerId: 2,
coverImage: null,
}),
new DashboardEntry({
workflow: testWorkflow5,
Expand All @@ -197,6 +204,7 @@ export const testWorkflowEntries: DashboardEntry[] = [
accessLevel: "Write",
projectIDs: [3],
ownerId: 3,
coverImage: null,
}),
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,24 +202,27 @@ describe("CardItemComponent", () => {
expect(component.canEditCover).toBe(false);
});

it("should load the stored cover on initialization and use it as the preview image", () => {
it("should use the stored cover image on initialization", () => {
const cover = "data:image/jpeg;base64,abc";
workflowCoverService.getCover.mockReturnValue(of(cover));
component.entry = makeWorkflowEntry({ id: 7 });
const entry = makeWorkflowEntry({ id: 7 });
entry.coverImageUrl = cover;

component.entry = entry;
component.ngOnChanges({ entry: { currentValue: component.entry } as any });

expect(workflowCoverService.getCover).toHaveBeenCalledWith(7);
expect(workflowCoverService.getCover).not.toHaveBeenCalled();
expect(component.hasCustomImage).toBe(true);
expect(component.coverImageSrc).toBe(cover);
});

it("should fall back to the default preview image when no cover is set", () => {
workflowCoverService.getCover.mockReturnValue(of(undefined));
component.entry = makeWorkflowEntry({ id: 7 });
const entry = makeWorkflowEntry({ id: 7 });
entry.coverImageUrl = undefined;

component.entry = entry;
component.ngOnChanges({ entry: { currentValue: component.entry } as any });

expect(workflowCoverService.getCover).not.toHaveBeenCalled();
expect(component.hasCustomImage).toBe(false);
expect(component.coverImageSrc).toBe(CardItemComponent.DEFAULT_PREVIEW_IMAGE);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export class CardItemComponent implements OnChanges {

initializeEntry() {
this.coverImageSrc = CardItemComponent.DEFAULT_PREVIEW_IMAGE;
this.customImage = undefined;
if (this.entry.type === "workflow") {
if (typeof this.entry.id === "number") {
Comment thread
VuMartin marked this conversation as resolved.
this.disableDelete = !this.entry.workflow.isOwner;
Expand All @@ -205,14 +206,8 @@ export class CardItemComponent implements OnChanges {
this.entryLink = [HUB_WORKFLOW_RESULT_DETAIL, String(this.entry.id)];
}
this.size = this.entry.size;
this.workflowCoverService
.getCover(this.entry.id)
.pipe(untilDestroyed(this))
.subscribe(image => {
this.customImage = image;
this.coverImageSrc = image ?? CardItemComponent.DEFAULT_PREVIEW_IMAGE;
this.cdr.markForCheck();
});
this.coverImageSrc = this.entry.coverImageUrl ?? CardItemComponent.DEFAULT_PREVIEW_IMAGE;
this.customImage = this.entry.coverImageUrl ?? undefined;
}
this.iconType = "project";
} else if (this.entry.type === "project") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function makeWorkflowItem(wid: number, ownerId: number): SearchResultItem {
projectIDs: [],
accessLevel: "WRITE",
ownerId,
coverImage: null,
};
return { resourceType: "workflow", workflow };
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/dashboard/type/dashboard-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export class DashboardEntry {
this.likeCount = 0;
this.isLiked = false;
this.accessibleUserIds = [];
this.coverImageUrl = value.coverImage ?? undefined;
} else if (isDashboardProject(value)) {
this.type = EntityType.Project;
this.id = value.pid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export interface DashboardWorkflow {
projectIDs: number[];
accessLevel: string;
ownerId: number;
coverImage: string | null;
}
Loading