diff --git a/.github/workflows/ci-deploy.yml b/.github/workflows/ci-deploy.yml index b5598bab..46d5840a 100644 --- a/.github/workflows/ci-deploy.yml +++ b/.github/workflows/ci-deploy.yml @@ -2,9 +2,9 @@ name: CI / Build and Deploy on: push: - branches: [ main ] + branches: [dev] pull_request: - branches: [ main ] + branches: [dev] jobs: build: @@ -15,7 +15,7 @@ jobs: - name: Use Node.js 24 uses: actions/setup-node@v6 with: - node-version: '24' + node-version: "24" - name: Install dependencies run: | diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e25ce3a1..c54462f9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -2,9 +2,9 @@ name: Playwright E2E on: push: - branches: [ main ] + branches: [dev] pull_request: - branches: [ main ] + branches: [dev] jobs: e2e: @@ -16,7 +16,7 @@ jobs: - name: Use Node.js 24 uses: actions/setup-node@v6 with: - node-version: '24' + node-version: "24" - name: Install dependencies run: npm ci diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fe2301c..8dc30a2a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,9 +3,9 @@ name: Code Style/Lint on: push: - branches: ["main"] + branches: ["dev"] pull_request: - branches: ["main"] + branches: ["dev"] jobs: build: diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index 343ec145..5c9e3b4c 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -4,9 +4,9 @@ name: Coverage on: push: - branches: ["main", "test"] + branches: ["dev", "test"] pull_request: - branches: ["main", "test"] + branches: ["dev", "test"] jobs: build: @@ -37,4 +37,4 @@ jobs: hide_complexity: true indicators: true output: console - thresholds: "80 80" \ No newline at end of file + thresholds: "80 80" diff --git a/src/app/core/configs/themes.config.ts b/src/app/core/configs/themes.config.ts index 76bebca5..6eb8e085 100644 --- a/src/app/core/configs/themes.config.ts +++ b/src/app/core/configs/themes.config.ts @@ -1,22 +1,22 @@ -export interface WorkflowItem { +export interface ToolItem { id: string; label: string; href: string; disabled?: boolean; } -export interface ToolItem { +export interface WorkflowItem { id: string; label: string; href: string; disabled?: boolean; + tools: ToolItem[]; } export interface ThemeConfig { id: string; label: string; workflows: WorkflowItem[]; - tools: ToolItem[]; } export const THEMES: ThemeConfig[] = [ @@ -28,19 +28,47 @@ export const THEMES: ThemeConfig[] = [ id: "de-novo-design", label: "De Novo Design", href: "/binder-design/de-novo-design", + tools: [ + { + id: "bindcraft", + label: "BindCraft", + href: "/binder-design/de-novo-design", + }, + { + id: "rfdiffusion", + label: "RFdiffusion", + href: "/binder-design/de-novo-design", + disabled: true, + }, + ], }, - ], - tools: [ { - id: "bindcraft", - label: "BindCraft", - href: "/binder-design/de-novo-design", + id: "partial-diffusion", + label: "Partial Diffusion", + href: "/binder-design/partial-diffusion", + disabled: true, + tools: [ + { + id: "rfdiffusion", + label: "RFdiffusion", + href: "/binder-design/partial-diffusion", + disabled: true, + }, + ], }, { - id: "rfdiffusion", - label: "RFdiffusion", - href: "/tools/rfdiffusion", + id: "motif-scaffolding", + label: "Motif Scaffolding", + href: "/binder-design/motif-scaffolding", disabled: true, + tools: [ + { + id: "rfdiffusion", + label: "RFdiffusion", + href: "/binder-design/motif-scaffolding", + disabled: true, + }, + ], }, ], }, @@ -52,33 +80,57 @@ export const THEMES: ThemeConfig[] = [ id: "single-prediction", label: "Single Prediction", href: "/structure-prediction/single-prediction", + tools: [ + { + id: "colabfold", + label: "ColabFold", + href: "/structure-prediction/single-prediction", + }, + { + id: "alphafold2", + label: "AlphaFold2", + href: "/structure-prediction/single-prediction", + }, + { + id: "boltz", + label: "Boltz", + href: "/structure-prediction/single-prediction", + }, + ], }, { id: "bulk-prediction", label: "Bulk Prediction", href: "/structure-prediction/bulk-prediction", + tools: [ + { + id: "boltz", + label: "Boltz", + href: "/structure-prediction/bulk-prediction", + }, + { + id: "colabfold", + label: "ColabFold", + href: "/structure-prediction/bulk-prediction", + }, + ], }, { id: "interaction-screening", label: "Interaction Screening", href: "/structure-prediction/interaction-screening", - }, - ], - tools: [ - { - id: "boltz", - label: "Boltz", - href: "/structure-prediction/single-prediction", - }, - { - id: "colabfold", - label: "ColabFold", - href: "/structure-prediction/single-prediction", - }, - { - id: "alphafold2", - label: "AlphaFold2", - href: "/structure-prediction/single-prediction", + tools: [ + { + id: "boltz", + label: "Boltz", + href: "/structure-prediction/interaction-screening", + }, + { + id: "colabfold", + label: "ColabFold", + href: "/structure-prediction/interaction-screening", + }, + ], }, ], }, diff --git a/src/app/features/jobs/jobs-list/jobs-list.spec.ts b/src/app/features/jobs/jobs-list/jobs-list.spec.ts index aff45d8e..fea8c502 100644 --- a/src/app/features/jobs/jobs-list/jobs-list.spec.ts +++ b/src/app/features/jobs/jobs-list/jobs-list.spec.ts @@ -182,7 +182,7 @@ describe("JobsListComponent", () => { it("should load jobs on init", () => { expect(mockJobsService.listJobs).toHaveBeenCalledWith({ - limit: 50, + limit: 10, offset: 0, }); expect(component.jobs()).toEqual([mockJob]); @@ -280,8 +280,8 @@ describe("JobsListComponent", () => { component.loadJobs(); expect(mockJobsService.listJobs).toHaveBeenCalledWith({ - limit: 50, - offset: 50, + limit: 10, + offset: 10, search: "binder", status: ["Completed", "Failed"], }); diff --git a/src/app/features/jobs/jobs-list/jobs-list.ts b/src/app/features/jobs/jobs-list/jobs-list.ts index e85a2e2d..063d45ec 100644 --- a/src/app/features/jobs/jobs-list/jobs-list.ts +++ b/src/app/features/jobs/jobs-list/jobs-list.ts @@ -94,7 +94,7 @@ export default class JobsListComponent implements OnInit, OnDestroy { searchQuery = signal(""); selectedStatuses = signal([]); currentPage = signal(1); - pageSize = signal(50); + pageSize = signal(10); scoreSortDirection = signal<"asc" | "desc">("desc"); submittedSortDirection = signal<"asc" | "desc">("desc"); activeSort = signal<"score" | "submitted">("submitted"); diff --git a/src/app/features/themes/binder-design/binder-design.html b/src/app/features/themes/binder-design/binder-design.html index c7f2233c..4adda111 100644 --- a/src/app/features/themes/binder-design/binder-design.html +++ b/src/app/features/themes/binder-design/binder-design.html @@ -1,8 +1,4 @@ - +

Synthetic protein binders are emerging as powerful tools in therapeutics, diagnostics, and fundamental research, offering a highly tunable alternative diff --git a/src/app/features/themes/binder-design/binder-design.spec.ts b/src/app/features/themes/binder-design/binder-design.spec.ts index 7dd58305..0b856174 100644 --- a/src/app/features/themes/binder-design/binder-design.spec.ts +++ b/src/app/features/themes/binder-design/binder-design.spec.ts @@ -27,7 +27,7 @@ describe("BinderDesignComponent", () => { describe("workflows", () => { it("should have correct workflow structure", () => { expect(component.workflows()).toBeDefined(); - expect(component.workflows().length).toBe(1); + expect(component.workflows().length).toBe(3); }); it("workflows should contain de novo design workflow", () => { @@ -39,6 +39,22 @@ describe("BinderDesignComponent", () => { expect(deNovoWorkflow?.href).toBe("/binder-design/de-novo-design"); }); + it("should contain disabled partial diffusion and motif scaffolding workflows", () => { + const partial = component + .workflows() + .find((w) => w.id === "partial-diffusion"); + expect(partial?.label).toBe("Partial Diffusion"); + expect(partial?.disabled).toBeTrue(); + expect(partial?.tools.map((t) => t.id)).toEqual(["rfdiffusion"]); + + const motif = component + .workflows() + .find((w) => w.id === "motif-scaffolding"); + expect(motif?.label).toBe("Motif Scaffolding"); + expect(motif?.disabled).toBeTrue(); + expect(motif?.tools.map((t) => t.id)).toEqual(["rfdiffusion"]); + }); + it("should have all workflows with required properties", () => { component.workflows().forEach((workflow) => { expect(workflow.id).toBeDefined(); @@ -52,32 +68,32 @@ describe("BinderDesignComponent", () => { }); describe("tools", () => { - it("tools should have correct tools structure", () => { - expect(component.tools()).toBeDefined(); - expect(component.tools().length).toBe(2); + const deNovoTools = () => + component.workflows().find((w) => w.id === "de-novo-design")?.tools ?? []; + + it("de novo design workflow should have correct tools structure", () => { + expect(deNovoTools().length).toBe(2); }); it("tools should contain BindCraft tool", () => { - const bindCraftTool = component - .tools() - .find((t) => t.label === "BindCraft"); + const bindCraftTool = deNovoTools().find((t) => t.label === "BindCraft"); expect(bindCraftTool).toBeDefined(); expect(bindCraftTool?.id).toBe("bindcraft"); expect(bindCraftTool?.href).toBe("/binder-design/de-novo-design"); }); it("tools should contain RFdiffusion tool", () => { - const rfdiffusionTool = component - .tools() - .find((t) => t.label === "RFdiffusion"); + const rfdiffusionTool = deNovoTools().find( + (t) => t.label === "RFdiffusion" + ); expect(rfdiffusionTool).toBeDefined(); expect(rfdiffusionTool?.id).toBe("rfdiffusion"); - expect(rfdiffusionTool?.href).toBe("/tools/rfdiffusion"); + expect(rfdiffusionTool?.href).toBe("/binder-design/de-novo-design"); expect(rfdiffusionTool?.disabled).toBeTrue(); }); it("should have all tools with required properties", () => { - component.tools().forEach((tool) => { + deNovoTools().forEach((tool) => { expect(tool.id).toBeDefined(); expect(tool.id).not.toBe(""); expect(tool.label).toBeDefined(); @@ -86,7 +102,7 @@ describe("BinderDesignComponent", () => { }); it("should have unique tool IDs", () => { - const ids = component.tools().map((tool) => tool.id); + const ids = deNovoTools().map((tool) => tool.id); const uniqueIds = [...new Set(ids)]; expect(ids.length).toBe(uniqueIds.length); }); @@ -153,11 +169,11 @@ describe("BinderDesignComponent", () => { component.workflows().forEach((workflow) => { expect(typeof workflow.id).toBe("string"); expect(typeof workflow.label).toBe("string"); - }); - component.tools().forEach((tool) => { - expect(typeof tool.id).toBe("string"); - expect(typeof tool.label).toBe("string"); + workflow.tools.forEach((tool) => { + expect(typeof tool.id).toBe("string"); + expect(typeof tool.label).toBe("string"); + }); }); component.communityResources().forEach((resource) => { @@ -168,33 +184,26 @@ describe("BinderDesignComponent", () => { it("should have proper data types", () => { expect(Array.isArray(component.workflows())).toBe(true); - expect(Array.isArray(component.tools())).toBe(true); expect(Array.isArray(component.communityResources())).toBe(true); }); }); describe("link rendering", () => { - it("should render enabled workflows and tools as routerLink anchors", () => { - const linkTexts = fixture.debugElement + it("should render an enabled card linking to the workflow", () => { + const hrefs = fixture.debugElement .queryAll(By.directive(RouterLink)) - .map((link) => link.nativeElement.textContent.trim()); + .map((link) => link.nativeElement.getAttribute("href")); - expect(linkTexts).toContain("De Novo Design"); - expect(linkTexts).toContain("BindCraft"); + expect(hrefs).toContain("/binder-design/de-novo-design"); }); - it("should render disabled workflows and tools as non-clickable spans", () => { - const disabledText = fixture.debugElement - .queryAll(By.css("span.cursor-not-allowed")) + it("should render workflow tools as badges", () => { + const badgeTexts = fixture.debugElement + .queryAll(By.css("li span")) .map((el) => el.nativeElement.textContent.trim()); - const linkTexts = fixture.debugElement - .queryAll(By.directive(RouterLink)) - .map((link) => link.nativeElement.textContent.trim()); - ["RFdiffusion"].forEach((label) => { - expect(disabledText).toContain(label); - expect(linkTexts).not.toContain(label); - }); + expect(badgeTexts).toContain("BindCraft"); + expect(badgeTexts).toContain("RFdiffusion"); }); }); }); diff --git a/src/app/features/themes/binder-design/binder-design.ts b/src/app/features/themes/binder-design/binder-design.ts index 0b883834..31696f16 100644 --- a/src/app/features/themes/binder-design/binder-design.ts +++ b/src/app/features/themes/binder-design/binder-design.ts @@ -11,7 +11,6 @@ import { THEMES } from "../../../core/configs/themes.config"; export class BinderDesignComponent { private readonly theme = THEMES.find((t) => t.id === "binder-design")!; workflows = signal(this.theme.workflows); - tools = signal(this.theme.tools); communityResources = signal([ { diff --git a/src/app/features/themes/layout/theme-layout/theme-layout.component.html b/src/app/features/themes/layout/theme-layout/theme-layout.component.html index 38b1037f..e559d088 100644 --- a/src/app/features/themes/layout/theme-layout/theme-layout.component.html +++ b/src/app/features/themes/layout/theme-layout/theme-layout.component.html @@ -1,38 +1,65 @@

-

{{ title() }}

+

{{ title() }}

- - @for (section of sections(); track section.heading) { +
-

- {{ section.heading }} -

-
- @for (item of section.items; track item.id) { @if (item.disabled) { - - {{ item.label }} - - } @else { +

Workflows

+
- } diff --git a/src/app/features/themes/layout/theme-layout/theme-layout.component.ts b/src/app/features/themes/layout/theme-layout/theme-layout.component.ts index bbc14652..605fbb1e 100644 --- a/src/app/features/themes/layout/theme-layout/theme-layout.component.ts +++ b/src/app/features/themes/layout/theme-layout/theme-layout.component.ts @@ -1,15 +1,13 @@ -import { Component, computed, input } from "@angular/core"; +import { Component, input } from "@angular/core"; import { RouterLink } from "@angular/router"; -import { ToolItem, WorkflowItem } from "../../../../core/configs/themes.config"; - -interface ThemeSection { - heading: string; - items: (WorkflowItem | ToolItem)[]; -} +import { NgIconComponent, provideIcons } from "@ng-icons/core"; +import { heroChevronRight } from "@ng-icons/heroicons/outline"; +import { WorkflowItem } from "../../../../core/configs/themes.config"; @Component({ selector: "app-theme-layout", - imports: [RouterLink], + imports: [RouterLink, NgIconComponent], + providers: [provideIcons({ heroChevronRight })], templateUrl: "./theme-layout.component.html", styleUrl: "./theme-layout.component.scss", host: { class: "block w-full" }, @@ -17,10 +15,4 @@ interface ThemeSection { export class ThemeLayoutComponent { readonly title = input.required(); readonly workflows = input.required(); - readonly tools = input.required(); - - protected readonly sections = computed(() => [ - { heading: "Workflows", items: this.workflows() }, - { heading: "Tools", items: this.tools() }, - ]); } diff --git a/src/app/features/themes/structure-prediction/structure-prediction.html b/src/app/features/themes/structure-prediction/structure-prediction.html index 5b56e181..bbdf7915 100644 --- a/src/app/features/themes/structure-prediction/structure-prediction.html +++ b/src/app/features/themes/structure-prediction/structure-prediction.html @@ -1,8 +1,4 @@ - +

AlphaFold2 brought about a revolution in structural biology by enabling high quality predictions of protein structure using evolutionary information diff --git a/src/app/features/themes/structure-prediction/structure-prediction.spec.ts b/src/app/features/themes/structure-prediction/structure-prediction.spec.ts index b6b00671..e2b81b89 100644 --- a/src/app/features/themes/structure-prediction/structure-prediction.spec.ts +++ b/src/app/features/themes/structure-prediction/structure-prediction.spec.ts @@ -24,54 +24,45 @@ describe("StructurePredictionComponent", () => { it("should define workflows", () => { expect(component.workflows.length).toBe(3); - expect(component.workflows[0]).toEqual({ - id: "single-prediction", - label: "Single Prediction", - href: "/structure-prediction/single-prediction", - }); - expect(component.workflows[1]).toEqual({ - id: "bulk-prediction", - label: "Bulk Prediction", - href: "/structure-prediction/bulk-prediction", - }); - expect(component.workflows[2]).toEqual({ - id: "interaction-screening", - label: "Interaction Screening", - href: "/structure-prediction/interaction-screening", - }); + expect(component.workflows.map((w) => w.id)).toEqual([ + "single-prediction", + "bulk-prediction", + "interaction-screening", + ]); }); - it("should define tools", () => { - expect(component.tools.length).toBe(3); - expect(component.tools[0]).toEqual({ - id: "boltz", - label: "Boltz", - href: "/structure-prediction/single-prediction", - }); - expect(component.tools[1]).toEqual({ - id: "colabfold", - label: "ColabFold", - href: "/structure-prediction/single-prediction", - }); - expect(component.tools[2]).toEqual({ - id: "alphafold2", - label: "AlphaFold2", - href: "/structure-prediction/single-prediction", - }); + it("should map tools onto each workflow", () => { + const toolIds = (id: string) => + component.workflows.find((w) => w.id === id)?.tools.map((t) => t.id); + + expect(toolIds("single-prediction")).toEqual([ + "colabfold", + "alphafold2", + "boltz", + ]); + expect(toolIds("bulk-prediction")).toEqual(["boltz", "colabfold"]); + expect(toolIds("interaction-screening")).toEqual(["boltz", "colabfold"]); }); - it("should render enabled workflows and tools as routerLink anchors", () => { - const linkTexts = fixture.debugElement + it("should render an enabled card linking to each workflow", () => { + const hrefs = fixture.debugElement .queryAll(By.directive(RouterLink)) - .map((link) => link.nativeElement.textContent.trim()); + .map((link) => link.nativeElement.getAttribute("href")); [ - "Single Prediction", - "Bulk Prediction", - "Interaction Screening", - "Boltz", - "ColabFold", - "AlphaFold2", - ].forEach((label) => expect(linkTexts).toContain(label)); + "/structure-prediction/single-prediction", + "/structure-prediction/bulk-prediction", + "/structure-prediction/interaction-screening", + ].forEach((href) => expect(hrefs).toContain(href)); + }); + + it("should render workflow tools as badges", () => { + const badgeTexts = fixture.debugElement + .queryAll(By.css("li span")) + .map((el) => el.nativeElement.textContent.trim()); + + ["Boltz", "ColabFold", "AlphaFold2"].forEach((label) => + expect(badgeTexts).toContain(label) + ); }); }); diff --git a/src/app/features/themes/structure-prediction/structure-prediction.ts b/src/app/features/themes/structure-prediction/structure-prediction.ts index d0fe5a4e..2cabb98d 100644 --- a/src/app/features/themes/structure-prediction/structure-prediction.ts +++ b/src/app/features/themes/structure-prediction/structure-prediction.ts @@ -11,5 +11,4 @@ import { THEMES } from "../../../core/configs/themes.config"; export class StructurePredictionComponent { private readonly theme = THEMES.find((t) => t.id === "structure-prediction")!; workflows = this.theme.workflows; - tools = this.theme.tools; } diff --git a/src/app/features/workflows/bulk-prediction/bulk-prediction.html b/src/app/features/workflows/bulk-prediction/bulk-prediction.html index 4ddfb0e4..4b27375e 100644 --- a/src/app/features/workflows/bulk-prediction/bulk-prediction.html +++ b/src/app/features/workflows/bulk-prediction/bulk-prediction.html @@ -173,6 +173,7 @@

- -
- -
-
-
-

Workflow

-

- {{ workflowName() || "—" }} -

-
-
-

Tool

-
-

- {{ selectedTool() || "—" }} -

- @if (hasParameters()) { - - Has Parameters - - } @else { - - No Parameters - - } -
-
-
-
- -
- - -
-
Input Configuration
-
- @for (item of inputItems(); track item.fieldName) { -
-

{{ item.label }}

- @if (item.url && item.value) { - - {{ item.value }} - - } @else { -

{{ item.value || "—" }}

- } -
- } -
- @if (inputEntities().length > 0) { -
-

Entities

-
    - @for (entity of inputEntities(); track $index) { -
  • - {{ entity.type }} — {{ entity.name }} @if (entity.sequence) { — - {{ entity.sequence }} - } -
  • - } -
-
- } -
- -
- - -
-
Tool Settings
- @if (toolSettingItems().length > 0) { -
- @for (item of toolSettingItems(); track item.fieldName) { -
-

{{ item.label }}

-

{{ item.value || "—" }}

-
- } -
- } @else { -

No tool settings

- } -
-
-

diff --git a/src/app/features/workflows/components/configuration-summary/configuration-summary.component.spec.ts b/src/app/features/workflows/components/configuration-summary/configuration-summary.component.spec.ts deleted file mode 100644 index 96e66fa9..00000000 --- a/src/app/features/workflows/components/configuration-summary/configuration-summary.component.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { - ConfigurationSummaryComponent, - SummaryItem, -} from "./configuration-summary.component"; - -describe("ConfigurationSummaryComponent", () => { - let component: ConfigurationSummaryComponent; - let fixture: ComponentFixture; - - const mockInputItems: SummaryItem[] = [ - { label: "Tool Name", value: "Test Tool", fieldName: "toolName" }, - { label: "Input File", value: "test.txt", fieldName: "inputFile" }, - ]; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ConfigurationSummaryComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ConfigurationSummaryComponent); - component = fixture.componentInstance; - - // Set required inputs - fixture.componentRef.setInput("workflowName", "Test Workflow"); - fixture.componentRef.setInput("selectedTool", "Test Tool"); - fixture.componentRef.setInput("inputItems", mockInputItems); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - it("should display selected tool", () => { - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.textContent).toContain("Test Tool"); - }); - - it("should display summary items", () => { - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.textContent).toContain("Tool Name"); - expect(compiled.textContent).toContain("test.txt"); - }); - - it("should not render a validation status (form is validated before preview)", () => { - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.textContent).not.toContain("Configuration Valid"); - expect(compiled.textContent).not.toContain("Configuration Incomplete"); - }); - - it("should display parameter status correctly", () => { - fixture.componentRef.setInput("hasParameters", true); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.textContent).toContain("Has Parameters"); - - fixture.componentRef.setInput("hasParameters", false); - fixture.detectChanges(); - expect(compiled.textContent).toContain("No Parameters"); - }); - - it("should not render a 'Not provided' fallback for empty values", () => { - fixture.componentRef.setInput("inputItems", [ - { label: "Optional Field", value: "", fieldName: "optional" }, - ]); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.textContent).not.toContain("Not provided"); - expect(compiled.textContent).toContain("Optional Field"); - }); - - it("should display workflow name and tool settings", () => { - fixture.componentRef.setInput("toolSettingItems", [ - { label: "num_recycles", value: "3", fieldName: "num_recycles" }, - ]); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.textContent).toContain("Test Workflow"); - expect(compiled.textContent).toContain("num_recycles"); - }); -}); diff --git a/src/app/features/workflows/components/configuration-summary/configuration-summary.component.ts b/src/app/features/workflows/components/configuration-summary/configuration-summary.component.ts deleted file mode 100644 index ed12b044..00000000 --- a/src/app/features/workflows/components/configuration-summary/configuration-summary.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component, input } from "@angular/core"; - -export interface SummaryItem { - label: string; - value: string; - fieldName: string; - url?: string; -} - -export interface EntitySummaryItem { - type: string; - name: string; - sequence: string; -} - -@Component({ - selector: "app-configuration-summary", - imports: [], - templateUrl: "./configuration-summary.component.html", - host: { class: "block" }, -}) -export class ConfigurationSummaryComponent { - readonly workflowName = input(""); - readonly selectedTool = input.required(); - readonly hasParameters = input(false); - readonly inputItems = input.required(); - readonly inputEntities = input([]); - readonly toolSettingItems = input([]); -} diff --git a/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.html b/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.html index 4a7292c5..0565d43c 100644 --- a/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.html +++ b/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.html @@ -21,14 +21,113 @@

- +
+
+ +
+
+
+

Workflow

+

+ {{ workflowName() || "—" }} +

+
+
+

Tool

+
+

+ {{ selectedTool() || "—" }} +

+ @if (hasParameters()) { + + Has Parameters + + } @else { + + No Parameters + + } +
+
+
+
+ +
+ + +
+
Input Configuration
+
+ @for (item of inputItems(); track item.fieldName) { +
+

{{ item.label }}

+ @if (item.url && item.value) { + + {{ item.value }} + + } @else { +

+ {{ item.value || "—" }} +

+ } +
+ } +
+ @if (inputEntities().length > 0) { +
+

Entities

+
    + @for (entity of inputEntities(); track $index) { +
  • + {{ entity.type }} — {{ entity.name }} @if (entity.sequence) { — + {{ entity.sequence }} + } +
  • + } +
+
+ } +
+ + @if (credits() !== null) { + + } + +
+ + +
+
Tool Settings
+ @if (toolSettingItems().length > 0) { +
+ @for (item of toolSettingItems(); track item.fieldName) { +
+

{{ item.label }}

+

+ {{ item.value || "—" }} +

+
+ } +
+ } @else { +

No tool settings

+ } +
+
+
diff --git a/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.spec.ts b/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.spec.ts index 98f92592..c52d1f1e 100644 --- a/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.spec.ts +++ b/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.spec.ts @@ -73,4 +73,23 @@ describe("WorkflowPreviewModalComponent", () => { expect(text).toContain("BindCraft"); expect(text).toContain("my-job"); }); + + it("should render the credit summary when credits are known", () => { + fixture.componentRef.setInput("isOpen", true); + fixture.componentRef.setInput("credits", 3); + fixture.componentRef.setInput("creditsRemaining", 10); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain("Total: 3 Credits"); + }); + + it("should omit the credit summary when credits are unknown", () => { + fixture.componentRef.setInput("isOpen", true); + fixture.componentRef.setInput("credits", null); + fixture.detectChanges(); + + expect( + fixture.nativeElement.querySelector("app-credit-summary") + ).toBeNull(); + }); }); diff --git a/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.ts b/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.ts index 3c5be63e..3f2b135f 100644 --- a/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.ts +++ b/src/app/features/workflows/components/workflow-preview-modal/workflow-preview-modal.component.ts @@ -4,11 +4,20 @@ import { heroXMark } from "@ng-icons/heroicons/outline"; import { ButtonComponent } from "../../../../components/button/button.component"; import { ModalComponent } from "../../../../components/modal/modal.component"; -import { - ConfigurationSummaryComponent, - EntitySummaryItem, - SummaryItem, -} from "../configuration-summary/configuration-summary.component"; +import { CreditSummaryComponent } from "../credit-summary/credit-summary.component"; + +export interface SummaryItem { + label: string; + value: string; + fieldName: string; + url?: string; +} + +export interface EntitySummaryItem { + type: string; + name: string; + sequence: string; +} @Component({ selector: "app-workflow-preview-modal", @@ -16,7 +25,7 @@ import { ModalComponent, ButtonComponent, NgIconComponent, - ConfigurationSummaryComponent, + CreditSummaryComponent, ], providers: [provideIcons({ heroXMark })], templateUrl: "./workflow-preview-modal.component.html", @@ -26,6 +35,7 @@ export class WorkflowPreviewModalComponent { readonly isOpen = input(false); readonly title = input("Review & Submit"); readonly credits = input(null); + readonly creditsRemaining = input(null); readonly isSubmitting = input(false); readonly workflowName = input(""); diff --git a/src/app/features/workflows/de-novo-design/de-novo-design.html b/src/app/features/workflows/de-novo-design/de-novo-design.html index 6b47f67c..83450486 100644 --- a/src/app/features/workflows/de-novo-design/de-novo-design.html +++ b/src/app/features/workflows/de-novo-design/de-novo-design.html @@ -378,6 +378,7 @@

@if (showAlert()) { diff --git a/src/app/features/workflows/single-prediction/single-prediction.html b/src/app/features/workflows/single-prediction/single-prediction.html index a33b3d00..ca91b4dc 100644 --- a/src/app/features/workflows/single-prediction/single-prediction.html +++ b/src/app/features/workflows/single-prediction/single-prediction.html @@ -97,7 +97,7 @@ cdkDragPreviewContainer="parent" >
@@ -163,41 +163,6 @@ }
-
- - - @if (shouldShowRowFieldError(row, 'name') && - getRowErrors(i).name) { - - } -
-