order added to flows

This commit is contained in:
Lucio Lelii 2026-01-16 17:15:57 +01:00
parent 958d3210c5
commit 76e7806d3d
9 changed files with 178 additions and 42 deletions

View File

@ -1,6 +1,6 @@
<aside class="h-full flex flex-row
bg-white border-r border-gray-200
transition-all duration-300 p-1" [class.w-72]="!collapsed()" [class.w-14]="collapsed()">
transition-all duration-300 p-1" [class.w-82]="!collapsed()" [class.w-14]="collapsed()">
<div class="flex flex-col h-full">
@ -27,7 +27,7 @@
<!-- CONTENT -->
<div class="h-full bg-white flex-1">
<div class="space-y-6 felx-1">
<div class="space-y-6 h-full">
@switch (open) {
@case ('flows') {
<app-group-holder title="Flows" icon="bi-lightning-charge-fill">

View File

@ -16,8 +16,8 @@ export class FlowsCallServiceFake extends FlowsCallServiceBase {
authorizationService = inject(Authorization);
private data: Record<string, Flow> = {
'1': { id: '1', name: 'Test Flow 1', data: new FlowData(), visibility: 'public', author: 'Alice', createdAt: new Date(), updatedAt: new Date() },
'2': { id: '2', name: 'Test Flow 2', data: new FlowData(), visibility: 'private', author: 'Bob', createdAt: new Date(), updatedAt: new Date() },
'1': { id: '1', name: 'A Flow', data: new FlowData(), visibility: 'public', author: 'Alice', createdAt: new Date("December 17, 2023 03:24:00"), updatedAt: new Date("January 7, 2026 12:24:00") },
'2': { id: '2', name: 'Test Flow', data: new FlowData(), visibility: 'private', author: 'Bob', createdAt: new Date("April 25, 2025 12:24:00"), updatedAt: new Date("April 27, 2025 18:42:00") },
};
override retrieveAllFlows() {

View File

@ -1,11 +1,11 @@
<div class="group border
px-3 py-2 cursor-pointer
px-3 py-2 cursor-pointer w-full
transition
hover:shadow-[0_0_12px_rgba(99,102,241,0.6)]
bg-indigo-50 border-indigo-200 select-none" (click)="toggleDetails()" (dblclick)="open()">
<!-- Header -->
<div class="flex items-center justify-between text-sm">
<div class="flex items-center justify-between text-sm ">
<div class="flex items-center gap-2 text-indigo-700 font-medium">
<i class="bi" [ngClass]="{
'bi-globe': flow().visibility === 'public',

View File

@ -1,4 +1,3 @@
@if (loading()) {
<div class="flex items-center justify-center h-32">
<div class="spinner-border text-indigo-500" role="status">
@ -6,36 +5,54 @@
</div>
</div>
} @else {
<div class="h-screen flex flex-col">
<div class="btn-group w-full mb-3" role="group">
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'all'"
[class.btn-outline-primary]="filter() !== 'all'" (click)="filter.set('all')">
All
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'public'"
[class.btn-outline-primary]="filter() !== 'public'" (click)="filter.set('public')">
Public
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'private'"
[class.btn-outline-primary]="filter() !== 'private'" (click)="filter.set('private')">
Private
</button>
<div class="h-screen flex flex-col">
<div class="w-full max-w-sm py-2">
<div class="relative flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="absolute w-5 h-5 top-2.5 left-2.5 text-slate-600">
<path fill-rule="evenodd"
d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z"
clip-rule="evenodd" />
</svg>
<input
class="w-full bg-transparent placeholder:text-slate-400 text-slate-700 text-sm border border-slate-200 rounded-md pl-10 pr-3 py-2 transition duration-300 ease focus:outline-none focus:border-slate-400 hover:border-slate-300 shadow-sm focus:shadow"
placeholder="Search flows..." [(ngModel)]="searchTerm"/>
</div>
</div>
<div class="btn-group w-full mb-3" role="group">
@if (filteredFlows().length === 0) {
<div class="text-gray-400 text-center mt-10 flex flex-col flex-1 justify-center">
No flows found
</div>
} @else {
<div class="flex-1 overflow-y-auto h-full py-2 gap-2 flex flex-col items-center max-h-[calc(100vh-16rem)]">
@for (flow of filteredFlows(); track flow.id) {
<app-flow-item [(detailOpenedId)]="detailOpenedId" [flow]="flow"></app-flow-item>
}
</div>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'all'"
[class.btn-outline-primary]="filter() !== 'all'" (click)="filter.set('all')">
All
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'public'"
[class.btn-outline-primary]="filter() !== 'public'" (click)="filter.set('public')">
Public
</button>
<button type="button" class="btn btn-sm w-full transition" [class.btn-primary]="filter() === 'private'"
[class.btn-outline-primary]="filter() !== 'private'" (click)="filter.set('private')">
Private
</button>
</div>
<div class="flex pt-2 pb-1 text-sm w-full items-center text-center justify-center">
<app-ordering [orderFields]="orderFields" (orderChanged)="onOrderChanged($event)"></app-ordering>
</div>
@if (filteredFlows().length === 0) {
<div class="text-gray-400 text-center mt-10 flex flex-col flex-1 pt-10">
No flows found
</div>
} @else {
<div class="flex-1 overflow-y-auto h-full w-full py-2 gap-2 flex flex-col items-stretch max-h-[calc(100vh-32rem)]">
@for (flow of filteredFlows(); track flow.id) {
<app-flow-item [(detailOpenedId)]="detailOpenedId" [flow]="flow"></app-flow-item>
}
</div>
}
}
</div>
}

View File

@ -1,26 +1,35 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject, signal, Signal, WritableSignal } from '@angular/core';
import { Component, computed, inject, model, output, signal, Signal, WritableSignal } from '@angular/core';
import { Flow } from '@models/flow';
import { FlowsService } from '@services/flows/flows';
import { FlowItem } from './flow-item/flow-item';
import { FormsModule } from '@angular/forms';
import { OrderEvent, OrderField, Ordering } from "@shared/ordering/ordering";
type FlowFilter = 'all' | 'public' | 'private';
@Component({
selector: 'app-flows-list',
imports: [FlowItem],
imports: [FlowItem, FormsModule, Ordering],
templateUrl: './flows-list.html',
styleUrl: './flows-list.css',
})
export class FlowsList {
filter = signal<FlowFilter>('all');
orderFields : OrderField[] = [
{ field: 'name', label: 'Name' },
{ field: 'updatedAt', label: 'Last Update Date' },
{ field: 'createdAt', label: 'Creation Date' }
];
searchTerm = model<string>('');
private flowsService = inject(FlowsService);
loading: WritableSignal<boolean> = signal(true);
flows: Signal<Flow[]> | undefined;
flows: Signal<Flow[] | null> = signal(null);
detailOpenedId = signal<string | null>(null);
@ -32,11 +41,24 @@ export class FlowsList {
}
filteredFlows = computed(() => {
const flows = this.flows?.();
console.log('Filtering flows', flows?.map(f => f.name));
const flows = this.flows();
if (!flows) return [];
if (this.filter() === 'all') return flows;
return flows.filter(f => f.visibility === this.filter());
const filteredFlows = flows.filter(f => f.name.toLowerCase().includes(this.searchTerm().toLowerCase()));
if (this.filter() === 'all') return filteredFlows;
return filteredFlows.filter(f => f.visibility === this.filter());
});
onOrderChanged(event: OrderEvent) {
const { orderBy, orderDir } = event;
const flows = this.filteredFlows();
if (!orderBy) return flows;
return flows.sort((a, b) => {
const aValue = (a as any)[orderBy];
const bValue = (b as any)[orderBy];
if (aValue < bValue) return orderDir === 'asc' ? -1 : 1;
if (aValue > bValue) return orderDir === 'asc' ? 1 : -1;
return 0;
});
}
}

View File

View File

@ -0,0 +1,42 @@
<div class="flex items-center w-full justify-between gap-2">
<div class="form-floating">
<select id="orderSelect"
class="form-select form-select-sm text-sm w-auto"
[(ngModel)]="orderBy"
placeholder="Order By"
>
@for (opt of orderFields(); track opt.field) {
<option [ngValue]="opt.field">
{{ opt.label || opt.field }}
</option>
}
</select>
<label for="orderSelect">Order by</label>
</div>
<!-- direzione -->
<div class="btn-group">
<button
type="button"
class="btn btn-sm px-1 w-6 h-14 flex items-center justify-center"
[class.btn-primary]="orderDir() === 'asc'"
[class.btn-outline-primary]="orderDir() !== 'asc'"
(click)="orderDir.set('asc')"
title="Ascending"
>
↑
</button>
<button
type="button"
class="btn btn-sm px-1 w-6 h-14 flex items-center justify-center"
[class.btn-primary]="orderDir() === 'desc'"
[class.btn-outline-primary]="orderDir() !== 'desc'"
(click)="orderDir.set('desc')"
title="Descending"
>
↓
</button>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Ordering } from './ordering';
describe('Ordering', () => {
let component: Ordering;
let fixture: ComponentFixture<Ordering>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Ordering]
})
.compileComponents();
fixture = TestBed.createComponent(Ordering);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Component, effect, input, model, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
export type OrderEvent = { orderBy: string | null; orderDir: orderDirType };
export type orderDirType = 'asc' | 'desc';
export type OrderField = { field: string; label?: string };
@Component({
selector: 'app-ordering',
imports: [FormsModule],
templateUrl: './ordering.html',
styleUrl: './ordering.css',
})
export class Ordering {
constructor() {
effect(() => {
this.orderChanged.emit({ orderBy: this.orderBy(), orderDir: this.orderDir() });
});
}
orderFields = input.required<OrderField[]>();
orderDir = model<orderDirType>('asc');
orderBy = model<string | null>(null);
orderChanged = output<OrderEvent>();
}